refactor(ui): replace custom StatusCard with the shadcn Empty primitive

Consolidate every empty / loading / error / not-found / error-boundary
state off the bespoke StatusCard onto the canonical shadcn Empty (already
used by DataTable) and delete components/ui/status-card.tsx. Empty renders
borderless and centered, so the wide-viewport "letterbox" look is fixed at
the root instead of capping a solid card.

Also in this pass:
- DataTable: truncate column headers so plain-string headers in narrow
  fixed-size columns (e.g. "System Prompt") stop wrapping to two lines.
- Settings > Providers / API Tokens: move the Create action into the page
  header (icon-collapses on mobile) and the API-token Playground/Swagger
  links into a header overflow menu; drop the cramped description row that
  needed the earlier truncate/flex-wrap workarounds. Those links use a
  relative href since the API base path is root-relative.
- Settings > Prompts: drop the fixed size on the Name column of both tables
  so it flexes to fill (matching Providers) and truncates.
- Settings > Account: keep "Member since" on one line at mobile widths
  (min-w-0 + truncate, badge shrink-0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-16 17:48:22 +07:00
co-authored by Claude Opus 4.8
parent 001c6c0d9d
commit a3dcd053bc
12 changed files with 281 additions and 241 deletions
@@ -3,7 +3,7 @@ import { useEffect } from 'react';
import { useRouteError } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { StatusCard } from '@/components/ui/status-card';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { isChunkLoadError, isDomDesyncError, reloadOnce } from '@/lib/chunk-reload';
/**
@@ -29,30 +29,29 @@ function RouteErrorBoundary() {
className="grid min-h-svh w-full place-items-center p-4"
role="alert"
>
<StatusCard
action={
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<TriangleAlert />
</EmptyMedia>
<EmptyTitle>Something went wrong</EmptyTitle>
<EmptyDescription>
{isChunk
? 'A new version was likely just deployed. Reloading will load the latest one.'
: isDesync
? 'The page hit a display glitch. Reloading usually clears it.'
: 'The page ran into an unexpected error. Reloading usually clears it.'}
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button
onClick={() => window.location.reload()}
variant="secondary"
>
Reload
</Button>
}
description={
isChunk
? 'A new version was likely just deployed. Reloading will load the latest one.'
: isDesync
? 'The page hit a display glitch. Reloading usually clears it.'
: 'The page ran into an unexpected error. Reloading usually clears it.'
}
icon={
<TriangleAlert
aria-hidden
className="text-muted-foreground size-10"
/>
}
title="Something went wrong"
/>
</EmptyContent>
</Empty>
</div>
);
}
+1 -1
View File
@@ -764,7 +764,7 @@ function DataTable<TData, TValue = unknown>({
>
{headerGroup.headers.map((header) => (
<TableHead
className={header.column.columnDef.meta?.headerClassName}
className={cn('truncate', header.column.columnDef.meta?.headerClassName)}
key={header.id}
style={
header.column.columnDef.size
@@ -1,25 +0,0 @@
import { type ReactNode } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';
interface StatusCardProps {
action?: ReactNode;
className?: string;
description?: ReactNode;
icon?: ReactNode;
title: ReactNode;
}
export function StatusCard({ action, className, description, icon, title }: StatusCardProps) {
return (
<Card className={cn('', className)}>
<CardContent className="flex flex-col items-center justify-center px-4 py-8 text-center">
{icon && <div className="mb-4 flex items-center justify-center">{icon}</div>}
<h3 className="text-foreground text-lg font-semibold">{title}</h3>
{description && <div className="text-muted-foreground mt-2 max-w-sm text-sm">{description}</div>}
{action && <div className="mt-4">{action}</div>}
</CardContent>
</Card>
);
}
+23 -13
View File
@@ -29,7 +29,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { StatusCard } from '@/components/ui/status-card';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { Toggle } from '@/components/ui/toggle';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { RenameFlowDocument, ResultType, StatusType, type TerminalFragmentFragment } from '@/graphql/types';
@@ -589,11 +589,17 @@ function Flows() {
<>
{pageHeader}
<div className="flex flex-col gap-4 p-4">
<StatusCard
description="Please wait while we fetch your conversation flows"
icon={<Loader2 className="text-muted-foreground size-16 animate-spin" />}
title="Loading flows..."
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
</EmptyMedia>
<EmptyTitle>Loading flows...</EmptyTitle>
<EmptyDescription>
Please wait while we fetch your conversation flows
</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -604,8 +610,15 @@ function Flows() {
<>
{pageHeader}
<div className="flex flex-col gap-4 p-4">
<StatusCard
action={
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<GitFork />
</EmptyMedia>
<EmptyTitle>No flows found</EmptyTitle>
<EmptyDescription>Get started by creating your first conversation flow</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button
onClick={() => navigate(routes.newFlow)}
variant="secondary"
@@ -613,11 +626,8 @@ function Flows() {
<Plus />
New Flow
</Button>
}
description="Get started by creating your first conversation flow"
icon={<GitFork className="text-muted-foreground size-8" />}
title="No flows found"
/>
</EmptyContent>
</Empty>
</div>
</>
);
+25 -13
View File
@@ -27,8 +27,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { InputSearch } from '@/components/ui/input-search';
import { StatusCard } from '@/components/ui/status-card';
import { KnowledgeDocType } from '@/graphql/types';
import { useTableState } from '@/hooks/use-table-state';
import { routes } from '@/lib/routes';
@@ -404,11 +404,17 @@ function Knowledges() {
<>
{pageHeader}
<div className="flex flex-col gap-4 p-4">
<StatusCard
description="Please wait while we fetch your knowledge documents"
icon={<Loader2 className="text-muted-foreground size-16 animate-spin" />}
title="Loading knowledges..."
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
</EmptyMedia>
<EmptyTitle>Loading knowledges...</EmptyTitle>
<EmptyDescription>
Please wait while we fetch your knowledge documents
</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -419,8 +425,17 @@ function Knowledges() {
<>
{pageHeader}
<div className="flex flex-col gap-4 p-4">
<StatusCard
action={
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<LibraryBig />
</EmptyMedia>
<EmptyTitle>No knowledge documents yet</EmptyTitle>
<EmptyDescription>
Create your first knowledge document to enrich the vector store
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button
onClick={() => navigate(routes.newKnowledge)}
variant="secondary"
@@ -428,11 +443,8 @@ function Knowledges() {
<Plus />
New Knowledge
</Button>
}
description="Create your first knowledge document to enrich the vector store"
icon={<LibraryBig className="text-muted-foreground size-8" />}
title="No knowledge documents yet"
/>
</EmptyContent>
</Empty>
</div>
</>
);
@@ -62,11 +62,11 @@ function SettingsAccount() {
<div className="bg-muted text-foreground flex size-12 shrink-0 items-center justify-center rounded-lg text-lg font-semibold">
{initial}
</div>
<div className="flex flex-1 flex-col gap-0.5">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<CardTitle className="truncate">{displayName}</CardTitle>
{memberSince && <CardDescription>Member since {memberSince}</CardDescription>}
{memberSince && <CardDescription className="truncate">Member since {memberSince}</CardDescription>}
</div>
<Badge variant="secondary">{accountLabel}</Badge>
<Badge className="shrink-0" variant="secondary">{accountLabel}</Badge>
</CardHeader>
</Card>
@@ -24,7 +24,7 @@ import * as z from 'zod';
import type { ApiTokenFragmentFragment } from '@/graphql/types';
import { AppHeader, AppHeaderContent, AppHeaderTitle } from '@/components/layouts/app/app-header';
import { AppHeader, AppHeaderAction, AppHeaderActions, AppHeaderContent, AppHeaderTitle } from '@/components/layouts/app/app-header';
import ConfirmationDialog from '@/components/shared/confirmation-dialog';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
@@ -40,10 +40,10 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { Input } from '@/components/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { StatusCard } from '@/components/ui/status-card';
import {
ApiTokenCreatedDocument,
ApiTokenDeletedDocument,
@@ -144,45 +144,6 @@ const copyToClipboard = async (text: string): Promise<boolean> => {
}
};
function SettingsAPITokensHeader({ onCreateClick }: { onCreateClick: () => void }) {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 flex-1 flex-col gap-2">
<p className="text-muted-foreground truncate">Manage API tokens for programmatic access</p>
<div className="flex gap-4 text-sm">
<a
className="text-primary inline-flex items-center gap-1 underline hover:no-underline"
href={`${window.location.origin}${baseUrl}/graphql/playground`}
rel="noopener noreferrer"
target="_blank"
>
GraphQL Playground
<ExternalLink className="size-3" />
</a>
<a
className="text-primary inline-flex items-center gap-1 underline hover:no-underline"
href={`${window.location.origin}${baseUrl}/swagger/index.html`}
rel="noopener noreferrer"
target="_blank"
>
Swagger UI
<ExternalLink className="size-3" />
</a>
</div>
</div>
<Button
className="shrink-0"
onClick={onCreateClick}
variant="secondary"
>
<Plus className="size-4" />
Create Token
</Button>
</div>
);
}
const createNewTokenPlaceholder: APIToken = {
createdAt: new Date().toISOString(),
id: 'create-new',
@@ -848,6 +809,51 @@ function SettingsAPITokens() {
<AppHeaderContent>
<AppHeaderTitle icon={<Key className="size-4 shrink-0" />}>API Tokens</AppHeaderTitle>
</AppHeaderContent>
<AppHeaderActions>
<AppHeaderAction
icon={<Plus />}
label="Create Token"
onClick={handleCreateNew}
variant="secondary"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Developer tools"
className="size-8 p-0"
size="sm"
variant="ghost"
>
<Ellipsis />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="min-w-44"
>
<DropdownMenuItem asChild>
<a
href={`${baseUrl}/graphql/playground`}
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink className="size-4" />
GraphQL Playground
</a>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a
href={`${baseUrl}/swagger/index.html`}
rel="noopener noreferrer"
target="_blank"
>
<ExternalLink className="size-4" />
Swagger UI
</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</AppHeaderActions>
</AppHeader>
);
@@ -856,12 +862,15 @@ function SettingsAPITokens() {
<>
{pageHeader}
<div className="flex flex-1 flex-col gap-4 p-4">
<SettingsAPITokensHeader onCreateClick={handleCreateNew} />
<StatusCard
description="Please wait while we fetch your API tokens"
icon={<Loader2 className="text-muted-foreground size-16 animate-spin" />}
title="Loading tokens..."
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
</EmptyMedia>
<EmptyTitle>Loading tokens...</EmptyTitle>
<EmptyDescription>Please wait while we fetch your API tokens</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -872,7 +881,6 @@ function SettingsAPITokens() {
<>
{pageHeader}
<div className="flex flex-1 flex-col gap-4 p-4">
<SettingsAPITokensHeader onCreateClick={handleCreateNew} />
<Alert variant="destructive">
<AlertCircle className="size-4" />
<AlertTitle>Error loading tokens</AlertTitle>
@@ -890,9 +898,17 @@ function SettingsAPITokens() {
<>
{pageHeader}
<div className="flex flex-1 flex-col gap-4 p-4">
<SettingsAPITokensHeader onCreateClick={handleCreateNew} />
<StatusCard
action={
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<Key />
</EmptyMedia>
<EmptyTitle>No API tokens configured</EmptyTitle>
<EmptyDescription>
Create your first API token to access PentAGI programmatically
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button
onClick={handleCreateNew}
variant="secondary"
@@ -900,11 +916,8 @@ function SettingsAPITokens() {
<Plus className="size-4" />
Create Token
</Button>
}
description="Create your first API token to access PentAGI programmatically"
icon={<Key className="text-muted-foreground size-8" />}
title="No API tokens configured"
/>
</EmptyContent>
</Empty>
</div>
</>
);
@@ -914,8 +927,6 @@ function SettingsAPITokens() {
<>
{pageHeader}
<div className="flex flex-1 flex-col gap-4 p-4">
<SettingsAPITokensHeader onCreateClick={handleCreateNew} />
{(createError || updateError || deleteError || deleteErrorMessage) && (
<Alert variant="destructive">
<AlertCircle className="size-4" />
+28 -16
View File
@@ -68,9 +68,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { Form, FormControl, FormItem, FormMessage } from '@/components/ui/form';
import { FormSubmitButton } from '@/components/ui/form-submit-button';
import { StatusCard } from '@/components/ui/status-card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
CreatePromptDocument,
@@ -733,11 +733,15 @@ function SettingsPrompt() {
<>
{pageHeader}
<div className="flex flex-1 items-center justify-center p-4">
<StatusCard
description="Please wait while we fetch prompt information"
icon={<Loader2 className="text-muted-foreground size-16 animate-spin" />}
title="Loading prompt data..."
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
</EmptyMedia>
<EmptyTitle>Loading prompt data...</EmptyTitle>
<EmptyDescription>Please wait while we fetch prompt information</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -748,11 +752,15 @@ function SettingsPrompt() {
<>
{pageHeader}
<div className="flex flex-1 items-center justify-center p-4">
<StatusCard
description={error.message}
icon={<AlertCircle className="text-destructive size-16" />}
title="Error loading prompt data"
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<AlertCircle className="text-destructive size-12" />
</EmptyMedia>
<EmptyTitle>Error loading prompt data</EmptyTitle>
<EmptyDescription>{error.message}</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -763,11 +771,15 @@ function SettingsPrompt() {
<>
{pageHeader}
<div className="flex flex-1 items-center justify-center p-4">
<StatusCard
description={`The prompt "${promptId}" could not be found or is not supported for editing.`}
icon={<AlertCircle className="text-destructive size-16" />}
title="Prompt not found"
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<AlertCircle className="text-destructive size-12" />
</EmptyMedia>
<EmptyTitle>Prompt not found</EmptyTitle>
<EmptyDescription>{`The prompt "${promptId}" could not be found or is not supported for editing.`}</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -39,7 +39,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { StatusCard } from '@/components/ui/status-card';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { DeletePromptDocument, SettingsPromptsDocument } from '@/graphql/types';
import { usePageStorageKeys } from '@/hooks/use-page-storage-keys';
import { routes } from '@/lib/routes';
@@ -323,8 +323,8 @@ function SettingsPrompts() {
{
accessorKey: 'displayName',
cell: ({ row }) => (
<div className="flex items-center gap-2">
<span className="font-medium">{row.original.displayName}</span>
<div className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium">{row.original.displayName}</span>
</div>
),
enableHiding: false,
@@ -347,7 +347,6 @@ function SettingsPrompts() {
);
},
meta: { columnMenuLabel: 'Agent Name', searchable: true },
size: 200,
},
{
accessorKey: 'systemStatus',
@@ -495,8 +494,8 @@ function SettingsPrompts() {
{
accessorKey: 'displayName',
cell: ({ row }) => (
<div className="flex items-center gap-2">
<span className="font-medium">{row.original.displayName}</span>
<div className="flex min-w-0 items-center gap-2">
<span className="truncate font-medium">{row.original.displayName}</span>
</div>
),
enableHiding: false,
@@ -519,7 +518,6 @@ function SettingsPrompts() {
);
},
meta: { columnMenuLabel: 'Tool Name', searchable: true },
size: 300,
},
{
accessorKey: 'status',
@@ -793,11 +791,15 @@ function SettingsPrompts() {
{pageHeader}
<div className="flex flex-1 flex-col gap-6 p-4">
<SettingsPromptsHeader />
<StatusCard
description="Please wait while we fetch your prompt templates"
icon={<Loader2 className="text-muted-foreground size-16 animate-spin" />}
title="Loading prompts..."
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
</EmptyMedia>
<EmptyTitle>Loading prompts...</EmptyTitle>
<EmptyDescription>Please wait while we fetch your prompt templates</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -828,11 +830,15 @@ function SettingsPrompts() {
{pageHeader}
<div className="flex flex-1 flex-col gap-6 p-4">
<SettingsPromptsHeader />
<StatusCard
description="Prompt templates could not be loaded"
icon={<Settings className="text-muted-foreground size-8" />}
title="No prompts available"
/>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<Settings />
</EmptyMedia>
<EmptyTitle>No prompts available</EmptyTitle>
<EmptyDescription>Prompt templates could not be loaded</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -51,13 +51,13 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { FormSubmitButton } from '@/components/ui/form-submit-button';
import { Input } from '@/components/ui/input';
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { StatusCard } from '@/components/ui/status-card';
import {
AgentConfigType,
CreateProviderDocument,
@@ -1558,11 +1558,15 @@ function SettingsProvider() {
</AppHeaderContent>
</AppHeader>
<div className="flex flex-1 items-center justify-center p-4">
<StatusCard
description="Please wait while we fetch provider configuration"
icon={<Loader2 className="text-muted-foreground size-16 animate-spin" />}
title="Loading provider data..."
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
</EmptyMedia>
<EmptyTitle>Loading provider data...</EmptyTitle>
<EmptyDescription>Please wait while we fetch provider configuration</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -1579,11 +1583,15 @@ function SettingsProvider() {
</AppHeaderContent>
</AppHeader>
<div className="flex flex-1 items-center justify-center p-4">
<StatusCard
description={error.message}
icon={<AlertCircle className="text-destructive size-16" />}
title="Error loading provider data"
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<AlertCircle className="text-destructive size-12" />
</EmptyMedia>
<EmptyTitle>Error loading provider data</EmptyTitle>
<EmptyDescription>{error.message}</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom';
import type { ProviderConfigFragmentFragment } from '@/graphql/types';
import { providerIcons } from '@/components/icons/provider-icon';
import { AppHeader, AppHeaderContent, AppHeaderTitle } from '@/components/layouts/app/app-header';
import { AppHeader, AppHeaderActions, AppHeaderContent, AppHeaderTitle } from '@/components/layouts/app/app-header';
import ConfirmationDialog from '@/components/shared/confirmation-dialog';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
@@ -22,7 +22,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { StatusCard } from '@/components/ui/status-card';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { DeleteProviderDocument, ProviderType, SettingsProvidersDocument } from '@/graphql/types';
import { useTableState } from '@/hooks/use-table-state';
import { routes } from '@/lib/routes';
@@ -65,46 +65,39 @@ export function SettingsProvidersHeader() {
};
return (
<div className="flex items-center justify-between gap-4">
<p className="text-muted-foreground min-w-0 flex-1 truncate">Manage language model providers</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Create provider — choose type"
className="shrink-0"
variant="secondary"
>
Create Provider
<ChevronDown className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
style={{
width: 'var(--radix-dropdown-menu-trigger-width)',
}}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Create provider — choose type"
className="w-8 shrink-0 px-0 md:w-auto md:px-3"
size="sm"
variant="secondary"
>
{availableTypes.length === 0 ? (
<DropdownMenuItem disabled>No available provider types</DropdownMenuItem>
) : (
availableTypes.map(({ label, type }) => {
const Icon = providerIcons[type]?.icon;
<Plus />
<span className="hidden md:inline">Create Provider</span>
<ChevronDown className="hidden size-4 md:inline-flex" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{availableTypes.length === 0 ? (
<DropdownMenuItem disabled>No available provider types</DropdownMenuItem>
) : (
availableTypes.map(({ label, type }) => {
const Icon = providerIcons[type]?.icon;
return (
<DropdownMenuItem
key={type}
onClick={() => handleProviderCreate(type)}
>
{Icon && <Icon className="size-4" />}
{label}
</DropdownMenuItem>
);
})
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
return (
<DropdownMenuItem
key={type}
onClick={() => handleProviderCreate(type)}
>
{Icon && <Icon className="size-4" />}
{label}
</DropdownMenuItem>
);
})
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -404,6 +397,9 @@ function SettingsProviders() {
<AppHeaderContent>
<AppHeaderTitle icon={<Plug className="size-4 shrink-0" />}>Providers</AppHeaderTitle>
</AppHeaderContent>
<AppHeaderActions>
<SettingsProvidersHeader />
</AppHeaderActions>
</AppHeader>
);
@@ -412,12 +408,17 @@ function SettingsProviders() {
<>
{pageHeader}
<div className="flex flex-1 flex-col gap-4 p-4">
<SettingsProvidersHeader />
<StatusCard
description="Please wait while we fetch your provider configurations"
icon={<Loader2 className="text-muted-foreground size-16 animate-spin" />}
title="Loading providers..."
/>
<Empty>
<EmptyHeader>
<EmptyMedia>
<Loader2 className="text-muted-foreground size-10 animate-spin" />
</EmptyMedia>
<EmptyTitle>Loading providers...</EmptyTitle>
<EmptyDescription>
Please wait while we fetch your provider configurations
</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
</>
);
@@ -428,7 +429,6 @@ function SettingsProviders() {
<>
{pageHeader}
<div className="flex flex-1 flex-col gap-4 p-4">
<SettingsProvidersHeader />
<Alert variant="destructive">
<AlertCircle className="size-4" />
<AlertTitle>Error loading providers</AlertTitle>
@@ -446,9 +446,17 @@ function SettingsProviders() {
<>
{pageHeader}
<div className="flex flex-1 flex-col gap-4 p-4">
<SettingsProvidersHeader />
<StatusCard
action={
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<Settings />
</EmptyMedia>
<EmptyTitle>No providers configured</EmptyTitle>
<EmptyDescription>
Get started by adding your first language model provider
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button
onClick={() => navigate(routes.settings.newProvider())}
variant="secondary"
@@ -456,11 +464,8 @@ function SettingsProviders() {
<Plus className="size-4" />
Add Provider
</Button>
}
description="Get started by adding your first language model provider"
icon={<Settings className="text-muted-foreground size-8" />}
title="No providers configured"
/>
</EmptyContent>
</Empty>
</div>
</>
);
@@ -470,8 +475,6 @@ function SettingsProviders() {
<>
{pageHeader}
<div className="flex flex-1 flex-col gap-4 p-4">
<SettingsProvidersHeader />
{(deleteError || deleteErrorMessage) && (
<Alert variant="destructive">
<AlertCircle className="size-4" />
+12 -8
View File
@@ -24,7 +24,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { StatusCard } from '@/components/ui/status-card';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty';
import { useTableState } from '@/hooks/use-table-state';
import { routes } from '@/lib/routes';
import { mergeHrefWithSearchParams } from '@/lib/url-params';
@@ -268,8 +268,15 @@ function Templates() {
<>
{pageHeader}
<div className="flex flex-col gap-4 p-4">
<StatusCard
action={
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<FileText />
</EmptyMedia>
<EmptyTitle>No templates yet</EmptyTitle>
<EmptyDescription>Create your first template to get started</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button
onClick={() => navigate(routes.newTemplate)}
variant="secondary"
@@ -277,11 +284,8 @@ function Templates() {
<Plus className="size-4" />
New Template
</Button>
}
description="Create your first template to get started"
icon={<FileText className="text-muted-foreground size-8" />}
title="No templates yet"
/>
</EmptyContent>
</Empty>
</div>
</>
);