mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-19 17:00:59 +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:
@@ -48,7 +48,6 @@ interface PipelineOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RoutingRulesEditorProps {
|
interface RoutingRulesEditorProps {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
form: UseFormReturn<any>;
|
form: UseFormReturn<any>;
|
||||||
pipelineNameList: PipelineOption[];
|
pipelineNameList: PipelineOption[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
|
|||||||
import { systemInfo } from '@/app/infra/http';
|
import { systemInfo } from '@/app/infra/http';
|
||||||
import { Loader2, ExternalLink, KeyRound, Layers } from 'lucide-react';
|
import { Loader2, ExternalLink, KeyRound, Layers } from 'lucide-react';
|
||||||
import PasswordChangeDialog from '../password-change-dialog/PasswordChangeDialog';
|
import PasswordChangeDialog from '../password-change-dialog/PasswordChangeDialog';
|
||||||
|
import { PanelBody } from '../settings-dialog/panel-layout';
|
||||||
|
|
||||||
interface AccountSettingsPanelProps {
|
interface AccountSettingsPanelProps {
|
||||||
// True when this panel is the active section and the dialog is open.
|
// True when this panel is the active section and the dialog is open.
|
||||||
@@ -86,7 +87,7 @@ export default function AccountSettingsPanel({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6 py-5">
|
<PanelBody>
|
||||||
{userEmail && (
|
{userEmail && (
|
||||||
<p className="mb-4 text-sm text-muted-foreground">{userEmail}</p>
|
<p className="mb-4 text-sm text-muted-foreground">{userEmail}</p>
|
||||||
)}
|
)}
|
||||||
@@ -165,6 +166,6 @@ export default function AccountSettingsPanel({
|
|||||||
onOpenChange={handlePasswordDialogClose}
|
onOpenChange={handlePasswordDialogClose}
|
||||||
hasPassword={hasPassword}
|
hasPassword={hasPassword}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PanelBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
|
||||||
import { backendClient } from '@/app/infra/http';
|
import { backendClient } from '@/app/infra/http';
|
||||||
|
import { PanelToolbar } from '../settings-dialog/panel-layout';
|
||||||
|
|
||||||
interface ApiKey {
|
interface ApiKey {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -252,216 +253,209 @@ export default function ApiIntegrationPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-full min-h-0 flex-col px-6 py-5">
|
<Tabs
|
||||||
<Tabs
|
value={activeTab}
|
||||||
value={activeTab}
|
onValueChange={setActiveTab}
|
||||||
onValueChange={setActiveTab}
|
className="flex h-full min-h-0 w-full flex-col overflow-hidden"
|
||||||
className="flex w-full flex-1 flex-col overflow-hidden"
|
>
|
||||||
>
|
<PanelToolbar>
|
||||||
<TabsList className="shadow-md py-3 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
<TabsList>
|
||||||
<TabsTrigger className="px-5 py-4 cursor-pointer" value="apikeys">
|
<TabsTrigger value="apikeys">{t('common.apiKeys')}</TabsTrigger>
|
||||||
{t('common.apiKeys')}
|
<TabsTrigger value="webhooks">{t('common.webhooks')}</TabsTrigger>
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger className="px-5 py-4 cursor-pointer" value="webhooks">
|
|
||||||
{t('common.webhooks')}
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
</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 */}
|
{/* API Keys Tab */}
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="apikeys"
|
value="apikeys"
|
||||||
className="flex flex-1 flex-col space-y-4 overflow-hidden"
|
className="min-h-0 flex-1 space-y-4 overflow-auto px-6 py-5"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-2 text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t('common.apiKeyHint')}
|
{t('common.apiKeyHint')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
{t('common.loading')}
|
||||||
</div>
|
</div>
|
||||||
|
) : apiKeys.length === 0 ? (
|
||||||
<div className="flex justify-end">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
<Button
|
{t('common.noApiKeys')}
|
||||||
onClick={() => setShowCreateDialog(true)}
|
|
||||||
size="sm"
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
{t('common.createApiKey')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
{loading ? (
|
<div className="flex-1 overflow-auto rounded-md border">
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<Table>
|
||||||
{t('common.loading')}
|
<TableHeader>
|
||||||
</div>
|
<TableRow>
|
||||||
) : apiKeys.length === 0 ? (
|
<TableHead className="min-w-[120px]">
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
{t('common.name')}
|
||||||
{t('common.noApiKeys')}
|
</TableHead>
|
||||||
</div>
|
<TableHead className="min-w-[200px]">
|
||||||
) : (
|
{t('common.apiKeyValue')}
|
||||||
<div className="flex-1 overflow-auto rounded-md border">
|
</TableHead>
|
||||||
<Table>
|
<TableHead className="w-[100px]">
|
||||||
<TableHeader>
|
{t('common.actions')}
|
||||||
<TableRow>
|
</TableHead>
|
||||||
<TableHead className="min-w-[120px]">
|
</TableRow>
|
||||||
{t('common.name')}
|
</TableHeader>
|
||||||
</TableHead>
|
<TableBody>
|
||||||
<TableHead className="min-w-[200px]">
|
{apiKeys.map((item) => (
|
||||||
{t('common.apiKeyValue')}
|
<TableRow key={item.id}>
|
||||||
</TableHead>
|
<TableCell>
|
||||||
<TableHead className="w-[100px]">
|
<div>
|
||||||
{t('common.actions')}
|
<div className="font-medium">{item.name}</div>
|
||||||
</TableHead>
|
{item.description && (
|
||||||
</TableRow>
|
<div className="text-sm text-muted-foreground">
|
||||||
</TableHeader>
|
{item.description}
|
||||||
<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>
|
</div>
|
||||||
{webhook.description && (
|
)}
|
||||||
<div
|
</div>
|
||||||
className="text-sm text-muted-foreground truncate"
|
</TableCell>
|
||||||
title={webhook.description}
|
<TableCell>
|
||||||
>
|
<code className="text-sm bg-muted px-2 py-1 rounded">
|
||||||
{webhook.description}
|
{maskApiKey(item.key)}
|
||||||
</div>
|
</code>
|
||||||
)}
|
</TableCell>
|
||||||
</div>
|
<TableCell>
|
||||||
</TableCell>
|
<div className="flex gap-2">
|
||||||
<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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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')}
|
title={t('common.delete')}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
</TableCell>
|
||||||
))}
|
</TableRow>
|
||||||
</TableBody>
|
))}
|
||||||
</Table>
|
</TableBody>
|
||||||
</div>
|
</Table>
|
||||||
)}
|
</div>
|
||||||
</TabsContent>
|
)}
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
</div>
|
|
||||||
|
{/* 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 */}
|
{/* Create API Key Dialog */}
|
||||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export function parseDynamicFormItemType(value: string): DynamicFormItemType {
|
|||||||
|
|
||||||
export function getDefaultValues(
|
export function getDefaultValues(
|
||||||
itemConfigList: IDynamicFormItemSchema[],
|
itemConfigList: IDynamicFormItemSchema[],
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
): Record<string, any> {
|
): Record<string, any> {
|
||||||
return itemConfigList.reduce(
|
return itemConfigList.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
@@ -59,7 +58,7 @@ export function getDefaultValues(
|
|||||||
acc[item.name] = item.default;
|
acc[item.name] = item.default;
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
{} as Record<string, any>,
|
{} as Record<string, any>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
LANGBOT_MODELS_PROVIDER_REQUESTER,
|
LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { CustomApiError } from '@/app/infra/entities/common';
|
import { CustomApiError } from '@/app/infra/entities/common';
|
||||||
|
import { PanelBody } from '../settings-dialog/panel-layout';
|
||||||
|
|
||||||
interface ModelsPanelProps {
|
interface ModelsPanelProps {
|
||||||
// True when this panel is the active section and the dialog is open.
|
// True when this panel is the active section and the dialog is open.
|
||||||
@@ -611,12 +612,13 @@ export default function ModelsPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex-1 overflow-auto px-6 py-5">
|
<PanelBody>
|
||||||
{/* LangBot Models Card */}
|
{/* LangBot Models (Space) provider card is intentionally pinned to the
|
||||||
|
top, above the "add custom provider" action row. */}
|
||||||
{langbotProvider && renderProviderCard(langbotProvider, true)}
|
{langbotProvider && renderProviderCard(langbotProvider, true)}
|
||||||
|
|
||||||
{/* Add Provider Button */}
|
{/* Add-provider row: stays below the pinned card by design. */}
|
||||||
<div className="mb-3 flex justify-between items-center sticky top-0 bg-background py-2 z-10">
|
<div className="mb-3 flex items-center justify-between gap-3">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{otherProviders.length === 0
|
{otherProviders.length === 0
|
||||||
? t(
|
? t(
|
||||||
@@ -626,12 +628,10 @@ export default function ModelsPanel({
|
|||||||
)
|
)
|
||||||
: t('models.providerCount', { count: otherProviders.length })}
|
: t('models.providerCount', { count: otherProviders.length })}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<Button size="sm" variant="outline" onClick={handleCreateProvider}>
|
||||||
<Button size="sm" variant="outline" onClick={handleCreateProvider}>
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
{t('models.addProvider')}
|
||||||
{t('models.addProvider')}
|
</Button>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Provider List */}
|
{/* Provider List */}
|
||||||
@@ -643,7 +643,7 @@ export default function ModelsPanel({
|
|||||||
) : (
|
) : (
|
||||||
otherProviders.map((p) => renderProviderCard(p))
|
otherProviders.map((p) => renderProviderCard(p))
|
||||||
)}
|
)}
|
||||||
</div>
|
</PanelBody>
|
||||||
|
|
||||||
<Dialog open={providerFormOpen} onOpenChange={setProviderFormOpen}>
|
<Dialog open={providerFormOpen} onOpenChange={setProviderFormOpen}>
|
||||||
<DialogContent className="w-[600px] p-6">
|
<DialogContent className="w-[600px] p-6">
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared layout primitives for the settings-dialog panels.
|
||||||
|
*
|
||||||
|
* Every section renders under the dialog's unified header, so the panels
|
||||||
|
* themselves should share the same vertical rhythm: an optional top toolbar
|
||||||
|
* (meta on the left, primary action on the right) followed by a scrollable
|
||||||
|
* body with consistent padding. Keeping these in one place is what makes the
|
||||||
|
* tabs feel like one cohesive surface instead of four separately-styled views.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function PanelToolbar({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex shrink-0 items-center justify-between gap-3 border-b px-6 py-3',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PanelBody({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn('min-h-0 flex-1 overflow-auto px-6 py-5', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { backendClient } from '@/app/infra/http';
|
import { backendClient } from '@/app/infra/http';
|
||||||
|
import { PanelToolbar } from '../settings-dialog/panel-layout';
|
||||||
|
|
||||||
interface StorageSection {
|
interface StorageSection {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -137,7 +138,7 @@ export default function StorageAnalysisPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
<div className="flex shrink-0 items-center justify-between gap-3 border-b px-6 py-4">
|
<PanelToolbar>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{analysis
|
{analysis
|
||||||
? t('storageAnalysis.generatedAt', {
|
? t('storageAnalysis.generatedAt', {
|
||||||
@@ -156,7 +157,7 @@ export default function StorageAnalysisPanel({
|
|||||||
/>
|
/>
|
||||||
{t('storageAnalysis.refresh')}
|
{t('storageAnalysis.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</PanelToolbar>
|
||||||
|
|
||||||
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
|
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
|
||||||
<div className="space-y-5 px-6 py-5">
|
<div className="space-y-5 px-6 py-5">
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ export default function SystemStatusCard({
|
|||||||
fetchStatus();
|
fetchStatus();
|
||||||
const interval = setInterval(fetchStatus, 30_000);
|
const interval = setInterval(fetchStatus, 30_000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [fetchStatus, refreshKey]);
|
}, [fetchStatus, refreshKey]);
|
||||||
|
|
||||||
const pluginOk = pluginStatus
|
const pluginOk = pluginStatus
|
||||||
|
|||||||
@@ -323,7 +323,6 @@ export default function PipelineFormComponent({
|
|||||||
const isFirstEmission = !initializedStagesRef.current.has(stageKey);
|
const isFirstEmission = !initializedStagesRef.current.has(stageKey);
|
||||||
|
|
||||||
const currentValues =
|
const currentValues =
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(form.getValues(formName) as Record<string, any>) || {};
|
(form.getValues(formName) as Record<string, any>) || {};
|
||||||
form.setValue(formName, {
|
form.setValue(formName, {
|
||||||
...currentValues,
|
...currentValues,
|
||||||
@@ -368,7 +367,6 @@ export default function PipelineFormComponent({
|
|||||||
<DynamicFormComponent
|
<DynamicFormComponent
|
||||||
itemConfigList={stage.config}
|
itemConfigList={stage.config}
|
||||||
initialValues={
|
initialValues={
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
@@ -402,7 +400,6 @@ export default function PipelineFormComponent({
|
|||||||
<N8nAuthFormComponent
|
<N8nAuthFormComponent
|
||||||
itemConfigList={stage.config}
|
itemConfigList={stage.config}
|
||||||
initialValues={
|
initialValues={
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
@@ -445,7 +442,7 @@ export default function PipelineFormComponent({
|
|||||||
// make the locked selector display a scope that is NOT the one actually in
|
// make the locked selector display a scope that is NOT the one actually in
|
||||||
// effect. Coerce the displayed/saved value to the forced template so the UI
|
// effect. Coerce the displayed/saved value to the forced template so the UI
|
||||||
// truthfully reflects runtime behavior.
|
// truthfully reflects runtime behavior.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const stageInitialValues: Record<string, any> =
|
const stageInitialValues: Record<string, any> =
|
||||||
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
|
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
|
||||||
const effectiveInitialValues =
|
const effectiveInitialValues =
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface IPluginCardVO {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
priority: number;
|
priority: number;
|
||||||
install_source: string;
|
install_source: string;
|
||||||
install_info: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
install_info: Record<string, any>;
|
||||||
status: string;
|
status: string;
|
||||||
components: PluginComponent[];
|
components: PluginComponent[];
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
@@ -27,7 +27,7 @@ export class PluginCardVO implements IPluginCardVO {
|
|||||||
priority: number;
|
priority: number;
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
install_source: string;
|
install_source: string;
|
||||||
install_info: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
install_info: Record<string, any>;
|
||||||
status: string;
|
status: string;
|
||||||
components: PluginComponent[];
|
components: PluginComponent[];
|
||||||
hasUpdate?: boolean;
|
hasUpdate?: boolean;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface ComponentManifest {
|
|||||||
version?: string;
|
version?: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
};
|
};
|
||||||
spec: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
spec: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomApiError {
|
export interface CustomApiError {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const SYSTEM_FIELD_PREFIX = '__system.';
|
|||||||
export interface IShowIfCondition {
|
export interface IShowIfCondition {
|
||||||
field: string;
|
field: string;
|
||||||
operator: 'eq' | 'neq' | 'in';
|
operator: 'eq' | 'neq' | 'in';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
value: any;
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface Plugin {
|
|||||||
debug: boolean;
|
debug: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
install_source: string;
|
install_source: string;
|
||||||
install_info: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
install_info: Record<string, any>;
|
||||||
components: PluginComponent[];
|
components: PluginComponent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default function WizardPage() {
|
|||||||
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
|
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
|
||||||
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
|
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
|
||||||
const [botName, setBotName] = useState('');
|
const [botName, setBotName] = useState('');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [botDescription, _setBotDescription] = useState('');
|
const [botDescription, _setBotDescription] = useState('');
|
||||||
const [adapterConfig, setAdapterConfig] = useState<Record<string, unknown>>(
|
const [adapterConfig, setAdapterConfig] = useState<Record<string, unknown>>(
|
||||||
{},
|
{},
|
||||||
|
|||||||
Reference in New Issue
Block a user