refactor(web): use shadcn processor navigation

This commit is contained in:
RockChinQ
2026-08-25 12:36:57 +08:00
parent ac31f1f006
commit e3150e66a4
5 changed files with 78 additions and 108 deletions
@@ -14,6 +14,7 @@ import {
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { extractI18nObject } from '@/i18n/I18nProvider'; import { extractI18nObject } from '@/i18n/I18nProvider';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
@@ -430,40 +431,26 @@ export default function AgentFormComponent({
className="mb-2 flex h-full min-h-0 min-w-0 flex-1 flex-col" className="mb-2 flex h-full min-h-0 min-w-0 flex-1 flex-col"
> >
<nav className="mb-4 shrink-0 space-y-2 border-b pb-4"> <nav className="mb-4 shrink-0 space-y-2 border-b pb-4">
<div className="overflow-x-auto"> <Tabs
<ol className="grid min-w-[44rem] grid-cols-4 gap-2"> value={activeSection}
{primarySections.map((section, index) => { onValueChange={(value) =>
const Icon = section.icon; setActiveSection(value as AgentConfigSection)
return ( }
<li key={section.name} className="min-w-0"> >
<button <div className="overflow-x-auto">
type="button" <TabsList className="grid min-w-[44rem] w-full grid-cols-4">
onClick={() => setActiveSection(section.name)} {primarySections.map((section) => {
className={`flex w-full min-w-0 items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm font-medium transition-colors ${ const Icon = section.icon;
activeSection === section.name return (
? 'border-primary/50 bg-primary/5 text-foreground shadow-sm' <TabsTrigger key={section.name} value={section.name}>
: 'border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground' <Icon />
}`} {section.label}
> </TabsTrigger>
<span );
className={`flex size-7 shrink-0 items-center justify-center rounded-full text-xs ${ })}
activeSection === section.name </TabsList>
? 'bg-primary text-primary-foreground' </div>
: 'bg-muted text-muted-foreground' </Tabs>
}`}
>
{index + 1}
</span>
<Icon className="hidden size-4 shrink-0 xl:block" />
<span className="min-w-0 leading-tight">
{section.label}
</span>
</button>
</li>
);
})}
</ol>
</div>
</nav> </nav>
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"> <div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
@@ -1,5 +1,6 @@
import { ReactNode, useState } from 'react'; import { ReactNode, useState } from 'react';
import { BarChart3, Bug, Settings } from 'lucide-react'; import { BarChart3, Bug, Settings } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Tooltip, Tooltip,
@@ -69,12 +70,13 @@ export default function ProcessorDetailWorkbench({
{status && ( {status && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span <Badge
variant="outline"
role="status" role="status"
aria-label={status.label} aria-label={status.label}
tabIndex={0} tabIndex={0}
className={cn( className={cn(
'inline-flex shrink-0 items-center gap-1.5 rounded-full border px-2 py-1 text-xs font-medium', 'rounded-full',
status.tone === 'success' && status.tone === 'success' &&
'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300', 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300',
status.tone === 'warning' && status.tone === 'warning' &&
@@ -96,7 +98,7 @@ export default function ProcessorDetailWorkbench({
)} )}
/> />
{status.label} {status.label}
</span> </Badge>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom" className="max-w-72"> <TooltipContent side="bottom" className="max-w-72">
<p className="font-medium">{status.label}</p> <p className="font-medium">{status.label}</p>
@@ -8,6 +8,7 @@ import {
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { getDefaultValues } from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; import { getDefaultValues } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
@@ -31,7 +32,6 @@ import {
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { extractI18nObject } from '@/i18n/I18nProvider'; import { extractI18nObject } from '@/i18n/I18nProvider';
import { cn } from '@/lib/utils';
import { import {
Card, Card,
CardContent, CardContent,
@@ -580,64 +580,45 @@ export default function PipelineFormComponent({
{/* Keep the primary pipeline flow visible while editing. */} {/* Keep the primary pipeline flow visible while editing. */}
{formLabelList.length > 1 && ( {formLabelList.length > 1 && (
<nav className="mb-4 shrink-0 space-y-2 border-b pb-4"> <nav className="mb-4 shrink-0 space-y-2 border-b pb-4">
<div className="overflow-x-auto"> <Tabs value={activeSection} onValueChange={setActiveSection}>
<ol className="grid min-w-[34rem] grid-cols-3 gap-2"> <div className="overflow-x-auto">
{primarySections.map((section, index) => { <TabsList className="grid min-w-[34rem] w-full grid-cols-3">
{primarySections.map((section) => {
const Icon = section.icon;
return (
<TabsTrigger
key={section.name}
value={section.name}
>
<Icon />
{section.label}
</TabsTrigger>
);
})}
</TabsList>
</div>
<div className="flex flex-wrap gap-1">
{secondarySections.map((section) => {
const Icon = section.icon; const Icon = section.icon;
return ( return (
<li key={section.name} className="relative min-w-0"> <Button
<button key={section.name}
type="button" type="button"
onClick={() => setActiveSection(section.name)} variant={
className={cn( activeSection === section.name
'flex w-full min-w-0 items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm font-medium transition-colors', ? 'secondary'
activeSection === section.name : 'ghost'
? 'border-primary/50 bg-primary/5 text-foreground shadow-sm' }
: 'border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground', size="sm"
)} onClick={() => setActiveSection(section.name)}
> >
<span <Icon />
className={cn( {section.label}
'flex size-7 shrink-0 items-center justify-center rounded-full text-xs', </Button>
activeSection === section.name
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground',
)}
>
{index + 1}
</span>
<Icon className="hidden size-4 shrink-0 xl:block" />
<span className="min-w-0 leading-tight">
{section.label}
</span>
</button>
</li>
); );
})} })}
</ol> </div>
</div> </Tabs>
<ul className="flex flex-wrap gap-1">
{secondarySections.map((section) => {
const Icon = section.icon;
return (
<li key={section.name}>
<button
type="button"
onClick={() => setActiveSection(section.name)}
className={cn(
'flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors',
activeSection === section.name
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
)}
>
<Icon className="size-3.5" />
{section.label}
</button>
</li>
);
})}
</ul>
</nav> </nav>
)} )}
@@ -33,15 +33,15 @@ test.describe('processor detail workbench', () => {
.toBe(0); .toBe(0);
expect(debugBox!.y).toBeGreaterThanOrEqual(0); expect(debugBox!.y).toBeGreaterThanOrEqual(0);
const flow = configPanel.locator('ol'); const flow = configPanel.getByRole('tablist');
await expect(flow.getByRole('button').nth(0)).toContainText( await expect(flow.getByRole('tab').nth(0)).toContainText(
'Basic Information', 'Basic Information',
); );
await expect(flow.getByRole('button').nth(1)).toContainText( await expect(flow.getByRole('tab').nth(1)).toContainText(
'Bindable Event Range', 'Bindable Event Range',
); );
await expect(flow.getByRole('button').nth(2)).toContainText('Runner'); await expect(flow.getByRole('tab').nth(2)).toContainText('Runner');
await expect(flow.getByRole('button').nth(3)).toContainText('Local Agent'); await expect(flow.getByRole('tab').nth(3)).toContainText('Local Agent');
await expect(configPanel.getByLabel('Name')).toBeVisible(); await expect(configPanel.getByLabel('Name')).toBeVisible();
await expect(configPanel.getByLabel('Icon')).toBeVisible(); await expect(configPanel.getByLabel('Icon')).toBeVisible();
@@ -56,11 +56,11 @@ test.describe('processor detail workbench', () => {
), ),
).toBeVisible(); ).toBeVisible();
await flow.getByRole('button').nth(1).click(); await flow.getByRole('tab').nth(1).click();
await expect( await expect(
configPanel.getByText('Bindable Event Range', { exact: true }).last(), configPanel.getByText('Bindable Event Range', { exact: true }).last(),
).toBeVisible(); ).toBeVisible();
await flow.getByRole('button').nth(3).click(); await flow.getByRole('tab').nth(3).click();
await expect( await expect(
configPanel.getByText('Local Agent', { exact: true }).last(), configPanel.getByText('Local Agent', { exact: true }).last(),
).toBeVisible(); ).toBeVisible();
@@ -91,18 +91,16 @@ test.describe('processor detail workbench', () => {
.toBe(0); .toBe(0);
expect(debugBox!.y).toBeGreaterThanOrEqual(0); expect(debugBox!.y).toBeGreaterThanOrEqual(0);
const flow = configPanel.locator('ol'); const flow = configPanel.getByRole('tablist');
await expect(flow.getByRole('button').nth(0)).toContainText( await expect(flow.getByRole('tab').nth(0)).toContainText(
'Trigger Conditions', 'Trigger Conditions',
); );
await expect(flow.getByRole('button').nth(1)).toContainText( await expect(flow.getByRole('tab').nth(1)).toContainText('AI Capabilities');
'AI Capabilities', await expect(flow.getByRole('tab').nth(2)).toContainText(
);
await expect(flow.getByRole('button').nth(2)).toContainText(
'Output Processing', 'Output Processing',
); );
await flow.getByRole('button').nth(1).click(); await flow.getByRole('tab').nth(1).click();
await expect( await expect(
configPanel.getByText('Runtime', { exact: true }).last(), configPanel.getByText('Runtime', { exact: true }).last(),
).toBeVisible(); ).toBeVisible();
@@ -65,6 +65,8 @@ test('processor forms expose their primary orchestration flow horizontally', ()
pipelineForm, pipelineForm,
/const primarySectionNames = \['trigger', 'ai', 'output'\]/, /const primarySectionNames = \['trigger', 'ai', 'output'\]/,
); );
assert.match(agentForm, /grid min-w-\[44rem\] grid-cols-4/); assert.match(agentForm, /<TabsList[^>]*grid-cols-4/);
assert.match(pipelineForm, /grid min-w-\[34rem\] grid-cols-3/); assert.match(pipelineForm, /<TabsList[^>]*grid-cols-3/);
assert.doesNotMatch(agentForm, /<ol className=/);
assert.doesNotMatch(pipelineForm, /<ol className=/);
}); });