mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-18 02:16:07 +00:00
refactor(web): unify settings panel layouts with shared toolbar/body
- Add PanelToolbar/PanelBody primitives so all four settings tabs share the same top-toolbar + scrollable-body rhythm under the unified header. - API panel: drop the heavy gray shadowed TabsList; move the create action into the toolbar next to the tabs, lighten per-tab hints. - Storage panel: reuse PanelToolbar for the generated-at/refresh bar. - Account panel: wrap content in PanelBody for consistent padding. - Models panel: keep the pinned LangBot Models (Space) card at the very top, above the add-custom-provider row (intentional pin), using PanelBody instead of a top toolbar.
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
import { PanelToolbar } from '../settings-dialog/panel-layout';
|
||||
|
||||
interface ApiKey {
|
||||
id: number;
|
||||
@@ -252,216 +253,209 @@ export default function ApiIntegrationPanel({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full min-h-0 flex-col px-6 py-5">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex w-full flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<TabsList className="shadow-md py-3 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
||||
<TabsTrigger className="px-5 py-4 cursor-pointer" value="apikeys">
|
||||
{t('common.apiKeys')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="px-5 py-4 cursor-pointer" value="webhooks">
|
||||
{t('common.webhooks')}
|
||||
</TabsTrigger>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex h-full min-h-0 w-full flex-col overflow-hidden"
|
||||
>
|
||||
<PanelToolbar>
|
||||
<TabsList>
|
||||
<TabsTrigger value="apikeys">{t('common.apiKeys')}</TabsTrigger>
|
||||
<TabsTrigger value="webhooks">{t('common.webhooks')}</TabsTrigger>
|
||||
</TabsList>
|
||||
{activeTab === 'apikeys' ? (
|
||||
<Button
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.createApiKey')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => setShowCreateWebhookDialog(true)}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.createWebhook')}
|
||||
</Button>
|
||||
)}
|
||||
</PanelToolbar>
|
||||
|
||||
{/* API Keys Tab */}
|
||||
<TabsContent
|
||||
value="apikeys"
|
||||
className="flex flex-1 flex-col space-y-4 overflow-hidden"
|
||||
>
|
||||
<div className="flex items-start gap-2 text-sm text-muted-foreground">
|
||||
{t('common.apiKeyHint')}
|
||||
{/* API Keys Tab */}
|
||||
<TabsContent
|
||||
value="apikeys"
|
||||
className="min-h-0 flex-1 space-y-4 overflow-auto px-6 py-5"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('common.apiKeyHint')}
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={() => setShowCreateDialog(true)}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.createApiKey')}
|
||||
</Button>
|
||||
) : apiKeys.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('common.noApiKeys')}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
) : apiKeys.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('common.noApiKeys')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="min-w-[120px]">
|
||||
{t('common.name')}
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[200px]">
|
||||
{t('common.apiKeyValue')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px]">
|
||||
{t('common.actions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{item.name}</div>
|
||||
{item.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded">
|
||||
{maskApiKey(item.key)}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => handleCopyKey(item.key)}
|
||||
title={t('common.copyApiKey')}
|
||||
>
|
||||
{copiedKey === item.key ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setDeleteKeyId(item.id)}
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Webhooks Tab */}
|
||||
<TabsContent
|
||||
value="webhooks"
|
||||
className="flex flex-1 flex-col space-y-4 overflow-hidden"
|
||||
>
|
||||
<div className="flex items-start gap-2 text-sm text-muted-foreground">
|
||||
{t('common.webhookHint')}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={() => setShowCreateWebhookDialog(true)}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('common.createWebhook')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
) : webhooks.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('common.noWebhooks')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-full flex-1 overflow-auto rounded-md border">
|
||||
<Table className="table-fixed w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[150px]">
|
||||
{t('common.name')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[380px]">
|
||||
{t('common.webhookUrl')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[80px]">
|
||||
{t('common.webhookEnabled')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[80px]">
|
||||
{t('common.actions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{webhooks.map((webhook) => (
|
||||
<TableRow key={webhook.id}>
|
||||
<TableCell className="truncate">
|
||||
<div className="truncate">
|
||||
<div
|
||||
className="font-medium truncate"
|
||||
title={webhook.name}
|
||||
>
|
||||
{webhook.name}
|
||||
) : (
|
||||
<div className="flex-1 overflow-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="min-w-[120px]">
|
||||
{t('common.name')}
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[200px]">
|
||||
{t('common.apiKeyValue')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[100px]">
|
||||
{t('common.actions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeys.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{item.name}</div>
|
||||
{item.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</div>
|
||||
{webhook.description && (
|
||||
<div
|
||||
className="text-sm text-muted-foreground truncate"
|
||||
title={webhook.description}
|
||||
>
|
||||
{webhook.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="overflow-x-auto max-w-[380px]">
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded whitespace-nowrap inline-block">
|
||||
{webhook.url}
|
||||
</code>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={webhook.enabled}
|
||||
onCheckedChange={() => handleToggleWebhook(webhook)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded">
|
||||
{maskApiKey(item.key)}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setDeleteWebhookId(webhook.id)}
|
||||
type="button"
|
||||
onClick={() => handleCopyKey(item.key)}
|
||||
title={t('common.copyApiKey')}
|
||||
>
|
||||
{copiedKey === item.key ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setDeleteKeyId(item.id)}
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Webhooks Tab */}
|
||||
<TabsContent
|
||||
value="webhooks"
|
||||
className="min-h-0 flex-1 space-y-4 overflow-auto px-6 py-5"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('common.webhookHint')}
|
||||
</p>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
) : webhooks.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('common.noWebhooks')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-full flex-1 overflow-auto rounded-md border">
|
||||
<Table className="table-fixed w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[150px]">
|
||||
{t('common.name')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[380px]">
|
||||
{t('common.webhookUrl')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[80px]">
|
||||
{t('common.webhookEnabled')}
|
||||
</TableHead>
|
||||
<TableHead className="w-[80px]">
|
||||
{t('common.actions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{webhooks.map((webhook) => (
|
||||
<TableRow key={webhook.id}>
|
||||
<TableCell className="truncate">
|
||||
<div className="truncate">
|
||||
<div
|
||||
className="font-medium truncate"
|
||||
title={webhook.name}
|
||||
>
|
||||
{webhook.name}
|
||||
</div>
|
||||
{webhook.description && (
|
||||
<div
|
||||
className="text-sm text-muted-foreground truncate"
|
||||
title={webhook.description}
|
||||
>
|
||||
{webhook.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="overflow-x-auto max-w-[380px]">
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded whitespace-nowrap inline-block">
|
||||
{webhook.url}
|
||||
</code>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
checked={webhook.enabled}
|
||||
onCheckedChange={() => handleToggleWebhook(webhook)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setDeleteWebhookId(webhook.id)}
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Create API Key Dialog */}
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
|
||||
Reference in New Issue
Block a user