mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 00:17:47 +00:00
feat(ui): migrate cliproxy page to design system Config archetype
Wrap master-detail layout in PageShell + PageHeader + ConfigLayout. PageHeader carries the brand strip (Zap icon + CLIProxy title + account-management description) and refresh button. The existing sidebar (Quick Setup CTA, sectioned providers list, variants list, ProxyStatusWidget, footer connection summary) becomes ConfigLayout's left rail; ProviderEditor and EmptyProviderState become the form pane. Pure layout migration: every sub-flow still mounts correctly -- QuickSetupWizard dialog, AddAccountDialog (URL action=auth deep-link), AccountSafetyWarningCard for gemini/agy, provider selection persisted to localStorage and URL deep-link, all bulk pause/resume/solo/delete mutations untouched. ProviderEditor (which contains the form + raw JSON internally) remains as a single 'form' slot.
This commit is contained in:
+224
-218
@@ -11,6 +11,8 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Check, X, RefreshCw, Sparkles, Zap, GitBranch, Trash2 } from 'lucide-react';
|
import { Check, X, RefreshCw, Sparkles, Zap, GitBranch, Trash2 } from 'lucide-react';
|
||||||
|
import { PageShell, PageHeader } from '@/components/page-shell';
|
||||||
|
import { ConfigLayout } from '@/components/config-layout';
|
||||||
import { QuickSetupWizard } from '@/components/quick-setup-wizard';
|
import { QuickSetupWizard } from '@/components/quick-setup-wizard';
|
||||||
import { AddAccountDialog } from '@/components/account/add-account-dialog';
|
import { AddAccountDialog } from '@/components/account/add-account-dialog';
|
||||||
import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card';
|
import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card';
|
||||||
@@ -352,234 +354,238 @@ export function CliproxyPage() {
|
|||||||
setSelectedProvider(null);
|
setSelectedProvider(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const sidebar = (
|
||||||
<div className="flex h-full min-h-0 overflow-hidden">
|
<div className="flex h-full flex-col bg-muted/30">
|
||||||
{/* Left Sidebar */}
|
{/* Header inside the rail: Quick Setup CTA */}
|
||||||
<div className="w-80 border-r flex flex-col bg-muted/30">
|
<div className="border-b bg-background p-3">
|
||||||
{/* Header */}
|
<Button
|
||||||
<div className="p-4 border-b bg-background">
|
variant="default"
|
||||||
<div className="flex items-center justify-between mb-1">
|
size="sm"
|
||||||
<div className="flex items-center gap-2">
|
className="w-full gap-2"
|
||||||
<Zap className="w-5 h-5 text-primary" />
|
onClick={() => setWizardOpen(true)}
|
||||||
<h1 className="font-semibold">{updateCheck?.backendLabel ?? 'CLIProxy'}</h1>
|
>
|
||||||
</div>
|
<Sparkles className="w-4 h-4" />
|
||||||
<Button
|
{t('cliproxyPage.quickSetup')}
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</div>
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={handleRefresh}
|
{/* Providers List */}
|
||||||
disabled={isFetching}
|
<ScrollArea className="flex-1">
|
||||||
>
|
<div className="p-2">
|
||||||
<RefreshCw className={cn('w-4 h-4', isFetching && 'animate-spin')} />
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide px-3 py-2">
|
||||||
</Button>
|
{t('cliproxyPage.providers')}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mb-3">
|
{authLoading ? (
|
||||||
{t('cliproxyPage.accountManagement')}
|
<div className="space-y-2 px-2">
|
||||||
</p>
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-14 w-full rounded-lg" />
|
||||||
<Button
|
))}
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="w-full gap-2"
|
|
||||||
onClick={() => setWizardOpen(true)}
|
|
||||||
>
|
|
||||||
<Sparkles className="w-4 h-4" />
|
|
||||||
{t('cliproxyPage.quickSetup')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Providers List */}
|
|
||||||
<ScrollArea className="flex-1">
|
|
||||||
<div className="p-2">
|
|
||||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide px-3 py-2">
|
|
||||||
{t('cliproxyPage.providers')}
|
|
||||||
</div>
|
</div>
|
||||||
{authLoading ? (
|
) : (
|
||||||
<div className="space-y-2 px-2">
|
<div className="space-y-4">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{providerSections.map((section) => (
|
||||||
<Skeleton key={i} className="h-14 w-full rounded-lg" />
|
<div key={section.id} className="space-y-1">
|
||||||
))}
|
<div className="px-3">
|
||||||
</div>
|
<div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
) : (
|
{t(section.labelKey)}
|
||||||
<div className="space-y-4">
|
|
||||||
{providerSections.map((section) => (
|
|
||||||
<div key={section.id} className="space-y-1">
|
|
||||||
<div className="px-3">
|
|
||||||
<div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
||||||
{t(section.labelKey)}
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-[11px] leading-relaxed text-muted-foreground">
|
|
||||||
{t(section.hintKey)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{section.items.map((status) => (
|
|
||||||
<ProviderSidebarItem
|
|
||||||
key={status.provider}
|
|
||||||
status={status}
|
|
||||||
isSelected={effectiveProvider === status.provider}
|
|
||||||
onSelect={() => handleSelectProvider(status.provider)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-1 text-[11px] leading-relaxed text-muted-foreground">
|
||||||
|
{t(section.hintKey)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{section.items.map((status) => (
|
||||||
|
<ProviderSidebarItem
|
||||||
|
key={status.provider}
|
||||||
|
status={status}
|
||||||
|
isSelected={effectiveProvider === status.provider}
|
||||||
|
onSelect={() => handleSelectProvider(status.provider)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Variants Section */}
|
||||||
|
{variants.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide px-3 py-2 mt-4 flex items-center gap-1.5">
|
||||||
|
<GitBranch className="w-3 h-3" />
|
||||||
|
{t('cliproxyPage.variants')}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{variants.map((variant) => (
|
||||||
|
<VariantSidebarItem
|
||||||
|
key={variant.name}
|
||||||
|
variant={variant}
|
||||||
|
parentAuth={providers.find((p) => p.provider === variant.provider)}
|
||||||
|
isSelected={selectedVariant === variant.name}
|
||||||
|
onSelect={() => handleSelectVariant(variant.name)}
|
||||||
|
onDelete={() => deleteMutation.mutate(variant.name)}
|
||||||
|
isDeleting={deleteMutation.isPending}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
{/* Variants Section */}
|
|
||||||
{variants.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide px-3 py-2 mt-4 flex items-center gap-1.5">
|
|
||||||
<GitBranch className="w-3 h-3" />
|
|
||||||
{t('cliproxyPage.variants')}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{variants.map((variant) => (
|
|
||||||
<VariantSidebarItem
|
|
||||||
key={variant.name}
|
|
||||||
variant={variant}
|
|
||||||
parentAuth={providers.find((p) => p.provider === variant.provider)}
|
|
||||||
isSelected={selectedVariant === variant.name}
|
|
||||||
onSelect={() => handleSelectVariant(variant.name)}
|
|
||||||
onDelete={() => deleteMutation.mutate(variant.name)}
|
|
||||||
isDeleting={deleteMutation.isPending}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{/* Proxy Status Widget */}
|
|
||||||
<div className="p-3 border-t">
|
|
||||||
<ProxyStatusWidget />
|
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Footer Stats */}
|
{/* Proxy Status Widget */}
|
||||||
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
|
<div className="border-t p-3">
|
||||||
<div className="flex items-center justify-between">
|
<ProxyStatusWidget />
|
||||||
<span>{t('cliproxyPage.providerCount', { count: providers.length })}</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Check className="w-3 h-3 text-green-600" />
|
|
||||||
{t('cliproxyPage.connectedCount', {
|
|
||||||
count: providers.filter((p) => p.authenticated).length,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Panel */}
|
{/* Footer Stats */}
|
||||||
<div className="flex-1 flex min-w-0 flex-col overflow-hidden bg-background">
|
<div className="border-t bg-background p-3 text-xs text-muted-foreground">
|
||||||
{selectedVariantData && parentAuthForVariant ? (
|
<div className="flex items-center justify-between">
|
||||||
<>
|
<span>{t('cliproxyPage.providerCount', { count: providers.length })}</span>
|
||||||
<ProviderEditor
|
<span className="flex items-center gap-1">
|
||||||
provider={selectedVariantData.name}
|
<Check className="w-3 h-3 text-green-600" />
|
||||||
displayName={t('cliproxyPage.variantDisplay', {
|
{t('cliproxyPage.connectedCount', {
|
||||||
name: selectedVariantData.name,
|
count: providers.filter((p) => p.authenticated).length,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const detail = (
|
||||||
|
<div className="flex h-full min-w-0 flex-col overflow-hidden bg-background">
|
||||||
|
{selectedVariantData && parentAuthForVariant ? (
|
||||||
|
<>
|
||||||
|
<ProviderEditor
|
||||||
|
provider={selectedVariantData.name}
|
||||||
|
displayName={t('cliproxyPage.variantDisplay', {
|
||||||
|
name: selectedVariantData.name,
|
||||||
|
provider: selectedVariantData.provider,
|
||||||
|
})}
|
||||||
|
authStatus={parentAuthForVariant}
|
||||||
|
catalog={catalogs[selectedVariantData.provider]}
|
||||||
|
routing={routingHints[selectedVariantData.provider]}
|
||||||
|
logoProvider={selectedVariantData.provider}
|
||||||
|
baseProvider={selectedVariantData.provider}
|
||||||
|
defaultTarget={selectedVariantData.target}
|
||||||
|
isRemoteMode={isRemoteMode}
|
||||||
|
port={selectedVariantData.port}
|
||||||
|
topNotice={
|
||||||
|
showAccountSafetyWarning ? (
|
||||||
|
<AccountSafetyWarningCard compact showProxySettingsLink />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
onAddAccount={() =>
|
||||||
|
setAddAccountProvider({
|
||||||
provider: selectedVariantData.provider,
|
provider: selectedVariantData.provider,
|
||||||
})}
|
displayName: parentAuthForVariant.displayName,
|
||||||
authStatus={parentAuthForVariant}
|
isFirstAccount: (parentAuthForVariant.accounts?.length || 0) === 0,
|
||||||
catalog={catalogs[selectedVariantData.provider]}
|
})
|
||||||
routing={routingHints[selectedVariantData.provider]}
|
}
|
||||||
logoProvider={selectedVariantData.provider}
|
onSetDefault={(accountId) =>
|
||||||
baseProvider={selectedVariantData.provider}
|
setDefaultMutation.mutate({
|
||||||
defaultTarget={selectedVariantData.target}
|
provider: selectedVariantData.provider,
|
||||||
isRemoteMode={isRemoteMode}
|
accountId,
|
||||||
port={selectedVariantData.port}
|
})
|
||||||
topNotice={
|
}
|
||||||
showAccountSafetyWarning ? (
|
onRemoveAccount={(accountId) =>
|
||||||
<AccountSafetyWarningCard compact showProxySettingsLink />
|
removeMutation.mutate({
|
||||||
) : undefined
|
provider: selectedVariantData.provider,
|
||||||
}
|
accountId,
|
||||||
onAddAccount={() =>
|
})
|
||||||
setAddAccountProvider({
|
}
|
||||||
provider: selectedVariantData.provider,
|
onPauseToggle={(accountId, paused) =>
|
||||||
displayName: parentAuthForVariant.displayName,
|
handlePauseToggle(selectedVariantData.provider, accountId, paused)
|
||||||
isFirstAccount: (parentAuthForVariant.accounts?.length || 0) === 0,
|
}
|
||||||
})
|
onSoloMode={(accountId) => handleSoloMode(selectedVariantData.provider, accountId)}
|
||||||
}
|
onBulkPause={(accountIds) => handleBulkPause(selectedVariantData.provider, accountIds)}
|
||||||
onSetDefault={(accountId) =>
|
onBulkResume={(accountIds) =>
|
||||||
setDefaultMutation.mutate({
|
handleBulkResume(selectedVariantData.provider, accountIds)
|
||||||
provider: selectedVariantData.provider,
|
}
|
||||||
accountId,
|
isRemovingAccount={removeMutation.isPending}
|
||||||
})
|
isPausingAccount={pauseMutation.isPending || resumeMutation.isPending}
|
||||||
}
|
isSoloingAccount={soloMutation.isPending}
|
||||||
onRemoveAccount={(accountId) =>
|
isBulkPausing={bulkPauseMutation.isPending}
|
||||||
removeMutation.mutate({
|
isBulkResuming={bulkResumeMutation.isPending}
|
||||||
provider: selectedVariantData.provider,
|
/>
|
||||||
accountId,
|
</>
|
||||||
})
|
) : selectedStatus ? (
|
||||||
}
|
<>
|
||||||
onPauseToggle={(accountId, paused) =>
|
<ProviderEditor
|
||||||
handlePauseToggle(selectedVariantData.provider, accountId, paused)
|
provider={selectedStatus.provider}
|
||||||
}
|
displayName={selectedStatus.displayName}
|
||||||
onSoloMode={(accountId) => handleSoloMode(selectedVariantData.provider, accountId)}
|
authStatus={selectedStatus}
|
||||||
onBulkPause={(accountIds) =>
|
catalog={catalogs[selectedStatus.provider]}
|
||||||
handleBulkPause(selectedVariantData.provider, accountIds)
|
routing={routingHints[selectedStatus.provider]}
|
||||||
}
|
isRemoteMode={isRemoteMode}
|
||||||
onBulkResume={(accountIds) =>
|
topNotice={
|
||||||
handleBulkResume(selectedVariantData.provider, accountIds)
|
showAccountSafetyWarning ? (
|
||||||
}
|
<AccountSafetyWarningCard compact showProxySettingsLink />
|
||||||
isRemovingAccount={removeMutation.isPending}
|
) : undefined
|
||||||
isPausingAccount={pauseMutation.isPending || resumeMutation.isPending}
|
}
|
||||||
isSoloingAccount={soloMutation.isPending}
|
onAddAccount={() =>
|
||||||
isBulkPausing={bulkPauseMutation.isPending}
|
setAddAccountProvider({
|
||||||
isBulkResuming={bulkResumeMutation.isPending}
|
provider: selectedStatus.provider,
|
||||||
/>
|
displayName: selectedStatus.displayName,
|
||||||
</>
|
isFirstAccount: (selectedStatus.accounts?.length || 0) === 0,
|
||||||
) : selectedStatus ? (
|
})
|
||||||
<>
|
}
|
||||||
<ProviderEditor
|
onSetDefault={(accountId) =>
|
||||||
provider={selectedStatus.provider}
|
setDefaultMutation.mutate({
|
||||||
displayName={selectedStatus.displayName}
|
provider: selectedStatus.provider,
|
||||||
authStatus={selectedStatus}
|
accountId,
|
||||||
catalog={catalogs[selectedStatus.provider]}
|
})
|
||||||
routing={routingHints[selectedStatus.provider]}
|
}
|
||||||
isRemoteMode={isRemoteMode}
|
onRemoveAccount={(accountId) =>
|
||||||
topNotice={
|
removeMutation.mutate({
|
||||||
showAccountSafetyWarning ? (
|
provider: selectedStatus.provider,
|
||||||
<AccountSafetyWarningCard compact showProxySettingsLink />
|
accountId,
|
||||||
) : undefined
|
})
|
||||||
}
|
}
|
||||||
onAddAccount={() =>
|
onPauseToggle={(accountId, paused) =>
|
||||||
setAddAccountProvider({
|
handlePauseToggle(selectedStatus.provider, accountId, paused)
|
||||||
provider: selectedStatus.provider,
|
}
|
||||||
displayName: selectedStatus.displayName,
|
onSoloMode={(accountId) => handleSoloMode(selectedStatus.provider, accountId)}
|
||||||
isFirstAccount: (selectedStatus.accounts?.length || 0) === 0,
|
onBulkPause={(accountIds) => handleBulkPause(selectedStatus.provider, accountIds)}
|
||||||
})
|
onBulkResume={(accountIds) => handleBulkResume(selectedStatus.provider, accountIds)}
|
||||||
}
|
isRemovingAccount={removeMutation.isPending}
|
||||||
onSetDefault={(accountId) =>
|
isPausingAccount={pauseMutation.isPending || resumeMutation.isPending}
|
||||||
setDefaultMutation.mutate({
|
isSoloingAccount={soloMutation.isPending}
|
||||||
provider: selectedStatus.provider,
|
isBulkPausing={bulkPauseMutation.isPending}
|
||||||
accountId,
|
isBulkResuming={bulkResumeMutation.isPending}
|
||||||
})
|
/>
|
||||||
}
|
</>
|
||||||
onRemoveAccount={(accountId) =>
|
) : (
|
||||||
removeMutation.mutate({
|
<EmptyProviderState onSetup={() => setWizardOpen(true)} />
|
||||||
provider: selectedStatus.provider,
|
)}
|
||||||
accountId,
|
</div>
|
||||||
})
|
);
|
||||||
}
|
|
||||||
onPauseToggle={(accountId, paused) =>
|
return (
|
||||||
handlePauseToggle(selectedStatus.provider, accountId, paused)
|
<PageShell>
|
||||||
}
|
<PageHeader
|
||||||
onSoloMode={(accountId) => handleSoloMode(selectedStatus.provider, accountId)}
|
title={
|
||||||
onBulkPause={(accountIds) => handleBulkPause(selectedStatus.provider, accountIds)}
|
<span className="flex items-center gap-2">
|
||||||
onBulkResume={(accountIds) => handleBulkResume(selectedStatus.provider, accountIds)}
|
<Zap className="w-5 h-5 text-primary" />
|
||||||
isRemovingAccount={removeMutation.isPending}
|
{updateCheck?.backendLabel ?? 'CLIProxy'}
|
||||||
isPausingAccount={pauseMutation.isPending || resumeMutation.isPending}
|
</span>
|
||||||
isSoloingAccount={soloMutation.isPending}
|
}
|
||||||
isBulkPausing={bulkPauseMutation.isPending}
|
description={t('cliproxyPage.accountManagement')}
|
||||||
isBulkResuming={bulkResumeMutation.isPending}
|
actions={
|
||||||
/>
|
<Button
|
||||||
</>
|
variant="ghost"
|
||||||
) : (
|
size="icon"
|
||||||
<EmptyProviderState onSetup={() => setWizardOpen(true)} />
|
className="h-8 w-8"
|
||||||
)}
|
onClick={handleRefresh}
|
||||||
</div>
|
disabled={isFetching}
|
||||||
|
aria-label="Refresh"
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn('w-4 h-4', isFetching && 'animate-spin')} />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ConfigLayout left={sidebar} form={detail} />
|
||||||
|
|
||||||
{/* Dialogs */}
|
{/* Dialogs */}
|
||||||
<QuickSetupWizard open={wizardOpen} onClose={() => setWizardOpen(false)} />
|
<QuickSetupWizard open={wizardOpen} onClose={() => setWizardOpen(false)} />
|
||||||
@@ -595,6 +601,6 @@ export function CliproxyPage() {
|
|||||||
}
|
}
|
||||||
isFirstAccount={addAccountProvider?.isFirstAccount || false}
|
isFirstAccount={addAccountProvider?.isFirstAccount || false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PageShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user