Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { useQueryTime } from "@/providers/query-time-provider";
import { RefreshButton } from "@unkey/ui";
import { useFilters } from "../../../hooks/use-filters";

export const LogsRefresh = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { useQueryTime } from "@/providers/query-time-provider";
import { RefreshButton } from "@unkey/ui";
import { useFilters } from "../../../hooks/use-filters";

export const LogsRefresh = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { RefreshButton } from "@unkey/ui";
import { useRouter } from "next/navigation";
import { useFilters } from "../../hooks/use-filters";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { RefreshButton } from "@unkey/ui";
import { useFilters } from "../../../hooks/use-filters";

export const LogsRefresh = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { useQueryTime } from "@/providers/query-time-provider";
import { RefreshButton } from "@unkey/ui";
import { useLogsContext } from "../../../context/logs";
import { useFilters } from "../../../hooks/use-filters";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { useQueryTime } from "@/providers/query-time-provider";
import { RefreshButton } from "@unkey/ui";
import { useFilters } from "../../../hooks/use-filters";

export const LogsRefresh = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { useQueryTime } from "@/providers/query-time-provider";
import { RefreshButton } from "@unkey/ui";
import { useRatelimitLogsContext } from "../../../context/logs";
import { useFilters } from "../../../hooks/use-filters";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { RefreshButton } from "@unkey/ui";
import { useRouter } from "next/navigation";
import { useFilters } from "../../hooks/use-filters";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ The Button component accepts all standard HTML button attributes plus the follow

<AutoTypeTable
name="DocumentedButtonProps"
path="../../internal/ui/src/components/button.tsx"
path="../../internal/ui/src/components/buttons/button.tsx"
/>

## Usage Guidelines
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"use client";
import { RenderComponentWithSnippet } from "@/app/components/render";
import { Button, RefreshButton } from "@unkey/ui";
import { useState } from "react";

export const Default = () => {
const [refreshCount, setRefreshCount] = useState(0);

const handleRefresh = () => {
setRefreshCount((prev) => prev + 1);
};

return (
<RenderComponentWithSnippet>
<div className="flex flex-col gap-6">
<div>
<h4 className="text-sm font-medium mb-2">Basic Refresh Button</h4>
<div className="flex items-center gap-4">
<RefreshButton onRefresh={handleRefresh} isEnabled={true} />
<span className="text-sm text-gray-600">Refresh count: {refreshCount}</span>
</div>
</div>

<div>
<h4 className="text-sm font-medium mb-2">With Custom Styling</h4>
<div className="flex items-center gap-4">
<RefreshButton onRefresh={handleRefresh} isEnabled={true} />
<span className="text-xs text-gray-500">Try clicking or pressing ⌥+⇧+R</span>
</div>
</div>
</div>
</RenderComponentWithSnippet>
);
};

export const WithLiveMode = () => {
const [refreshCount, setRefreshCount] = useState(0);
const [isLive, setIsLive] = useState(true);
const [liveData, setLiveData] = useState(`Live data: ${Date.now()}`);

const handleRefresh = () => {
setRefreshCount((prev) => prev + 1);
setLiveData(`Live data: ${Date.now()}`);
};

return (
<RenderComponentWithSnippet>
<div className="flex flex-col gap-6">
<div>
<h4 className="text-sm font-medium mb-2">With Live Mode Integration</h4>
<div className="flex items-center gap-4">
<RefreshButton
onRefresh={handleRefresh}
isEnabled={true}
isLive={isLive}
toggleLive={setIsLive}
/>
<div className="flex flex-col gap-1">
<span className="text-sm text-gray-600">Refresh count: {refreshCount}</span>
<span className="text-sm text-gray-600">{liveData}</span>
<span className="text-xs text-gray-500">Live mode: {isLive ? "ON" : "OFF"}</span>
</div>
</div>
</div>

<div>
<h4 className="text-sm font-medium mb-2">Live Mode Toggle</h4>
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={() => setIsLive(!isLive)}
className="px-3 py-1 text-sm border rounded hover:bg-gray-50"
>
Toggle Live Mode
</Button>
<span className="text-xs text-gray-500">
Live mode will be temporarily disabled during refresh
</span>
</div>
</div>
</div>
</RenderComponentWithSnippet>
);
};

export const DisabledState = () => {
const [timeFilter, setTimeFilter] = useState("1h");
const [refreshCount, setRefreshCount] = useState(0);

const handleRefresh = () => {
setRefreshCount((prev) => prev + 1);
};

// Disable refresh when "all" time filter is selected
const isEnabled = timeFilter !== "all";

return (
<RenderComponentWithSnippet>
<div className="flex flex-col gap-6">
<div>
<h4 className="text-sm font-medium mb-2">Conditional Enablement</h4>
<div className="flex items-center gap-4">
<select
value={timeFilter}
onChange={(e) => setTimeFilter(e.target.value)}
className="px-3 py-1 text-sm border rounded"
>
<option value="1h">Last 1 hour</option>
<option value="24h">Last 24 hours</option>
<option value="7d">Last 7 days</option>
<option value="all">All time</option>
</select>
<RefreshButton onRefresh={handleRefresh} isEnabled={isEnabled} />
<span className="text-sm text-gray-600">Refresh count: {refreshCount}</span>
</div>
</div>

<div>
<h4 className="text-sm font-medium mb-2">Disabled State</h4>
<div className="flex items-center gap-4">
<RefreshButton onRefresh={handleRefresh} isEnabled={false} />
<span className="text-xs text-gray-500">
Hover over the disabled button to see the tooltip
</span>
</div>
</div>

<div>
<h4 className="text-sm font-medium mb-2">State Comparison</h4>
<div className="flex items-center gap-4">
<div className="flex flex-col gap-2">
<span className="text-xs font-medium">Enabled:</span>
<RefreshButton onRefresh={handleRefresh} isEnabled={true} />
</div>
<div className="flex flex-col gap-2">
<span className="text-xs font-medium">Disabled:</span>
<RefreshButton onRefresh={handleRefresh} isEnabled={false} />
</div>
</div>
</div>
</div>
</RenderComponentWithSnippet>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: "RefreshButton"
description: "A button component for refreshing data with keyboard shortcuts and live mode support"
---
import { Default, WithLiveMode, DisabledState } from "./refresh-button.examples";

## Features

- **Keyboard Shortcut**: Built-in support for ⌥+⇧+R (Option+Shift+R) shortcut
- **Loading States**: Visual feedback during refresh operations with a 1-second timeout
- **Live Mode Integration**: Automatic handling of live mode toggling during refresh
- **Accessibility**: Full keyboard navigation and screen reader support
- **Tooltip Integration**: Contextual help when refresh is unavailable
- **Visual Feedback**: Clear indication of refresh status and availability

## Basic Usage

The most common use case is a simple refresh button that calls a refresh function when clicked or when the keyboard shortcut is pressed.

<Default />

## With Live Mode

When used in contexts with live data streaming, the RefreshButton can automatically manage live mode state during refresh operations.

<WithLiveMode />

## Disabled State

The button can be disabled when refresh is not available, typically when no relative time filter is selected.

<DisabledState />

## Props

| Prop | Type | Description |
|------|------|-------------|
| `onRefresh` | `() => void` | Function called when refresh is triggered |
| `isEnabled` | `boolean` | Whether the refresh functionality is available |
| `isLive` | `boolean?` | Current live mode state (optional) |
| `toggleLive?` | `(value: boolean) => void` | Function to toggle live mode |

## Keyboard Shortcuts

The RefreshButton automatically registers the following keyboard shortcut:

- **⌥+⇧+R** (Option+Shift+R): Triggers the refresh operation

The shortcut is only active when `isEnabled` is true and the button is not in a loading state.

## Behavior

### Refresh Flow

1. **Click or Shortcut**: User clicks the button or presses ⌥+⇧+R
2. **Live Mode Handling**: If live mode is enabled, it's temporarily disabled
3. **Loading State**: Button shows loading indicator for 1 second
4. **Refresh Execution**: The `onRefresh` function is called
5. **State Restoration**: After timeout, live mode is restored if it was previously enabled

### Disabled State

When `isEnabled` is false:
- Button appears disabled
- Tooltip shows "Refresh unavailable - please select a relative time filter in the 'Since' dropdown"
- Keyboard shortcut is disabled
- No refresh operations can be triggered

### Loading State

During refresh operations:
- Button shows a loading spinner
- Text remains visible, but the button is non-interactive
- Keyboard shortcut is disabled
- Live mode is temporarily disabled if enabled
1 change: 1 addition & 0 deletions internal/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"author": "Andreas Thomas",
"license": "AGPL-3.0",
"devDependencies": {
"@testing-library/react": "^16.2.0",
"@types/node": "^20.14.9",
"@types/react": "^18.3.11",
"@unkey/tsconfig": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "../lib/utils";
import { AnimatedLoadingSpinner } from "./animated-loading-spinner";
import { cn } from "../../lib/utils";
import { AnimatedLoadingSpinner } from "../animated-loading-spinner";

// Hack to populate fumadocs' AutoTypeTable
export type DocumentedButtonProps = VariantProps<typeof buttonVariants> & {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { TaskChecked, TaskUnchecked } from "@unkey/icons";
import * as React from "react";
import { cn } from "../lib/utils";
import { cn } from "../../lib/utils";
import { Button, type ButtonProps } from "./button";

type CopyButtonProps = ButtonProps & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// biome-ignore lint: React in this context is used throughout, so biome will change to types because no APIs are used even though React is needed.
import * as React from "react";
import type { ComponentProps } from "react";
import { cn } from "../lib/utils";
import { cn } from "../../lib/utils";

type ModifierKey = "⌘" | "⇧" | "CTRL" | "⌥";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut";
"use client";
import { Refresh3 } from "@unkey/icons";
import { Button, InfoTooltip, KeyboardButton } from "@unkey/ui";
// biome-ignore lint: React in this context is used throughout, so biome will change to types because no APIs are used even though React is needed.
import * as React from "react";
import { useState } from "react";
import { useKeyboardShortcut } from "../../hooks/use-keyboard-shortcut";
import { InfoTooltip } from "../info-tooltip";
import { Button } from "./button";
import { KeyboardButton } from "./keyboard-button";

type RefreshButtonProps = {
onRefresh: () => void;
Expand All @@ -12,7 +17,7 @@ type RefreshButtonProps = {

const REFRESH_TIMEOUT_MS = 1000;

export const RefreshButton = ({ onRefresh, isEnabled, isLive, toggleLive }: RefreshButtonProps) => {
const RefreshButton = ({ onRefresh, isEnabled, isLive, toggleLive }: RefreshButtonProps) => {
const [isLoading, setIsLoading] = useState(false);
const [refreshTimeout, setRefreshTimeout] = useState<NodeJS.Timeout | null>(null);

Expand Down Expand Up @@ -70,3 +75,6 @@ export const RefreshButton = ({ onRefresh, isEnabled, isLive, toggleLive }: Refr
</InfoTooltip>
);
};

RefreshButton.displayName = "RefreshButton";
export { RefreshButton, type RefreshButtonProps };
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Eye, EyeSlash } from "@unkey/icons";
// biome-ignore lint: React in this context is used throughout, so biome will change to types because no APIs are used even though React is needed.
import * as React from "react";
import { cn } from "../lib/utils";
import { cn } from "../../lib/utils";
import { Button, type ButtonProps } from "./button";

type VisibleButtonProps = ButtonProps & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useNavigation,
} from "react-day-picker";
import { cn } from "../../../lib/utils";
import { buttonVariants } from "../../button";
import { buttonVariants } from "../../buttons/button";
import { useDateTimeContext } from "../date-time";

function CustomCaptionComponent(props: CaptionProps) {
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/src/components/dialog/navigable-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import type { FC, ReactNode } from "react";
import { cn } from "../../lib/utils";
import { Button } from "../button";
import { Button } from "../buttons/button";
import { Dialog, DialogContent } from "./dialog";
import {
DefaultDialogContentArea,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from "react";
import { cn } from "../../../lib/utils";
import { XMark } from "@unkey/icons";
import { TimestampInfo } from "../../timestamp-info";
import { Button } from "../../button";
import { Button } from "../../buttons/button";
import type { FilterValue } from "../../../validation/filter.types";
import { formatOperator } from "./utils";
import { useEffect, useRef } from "react";
Expand Down
Loading
Loading