refactor(web): simplify AddAgentSheet by removing Stepper and updating layout
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m39s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Removed the Stepper component from AddAgentSheet, transitioning to a single form layout for agent creation.
- Updated the component's description and adjusted the UI elements for improved clarity and user experience.
- Enhanced the input fields and layout to streamline the agent installation invite process.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-21 04:18:38 +07:00
co-authored by Cursor
parent d862899bba
commit 919c1d0f95
2 changed files with 81 additions and 208 deletions
@@ -23,23 +23,12 @@ import {
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from '@evofw/ui/components/sheet' } from '@evofw/ui/components/sheet'
import {
Stepper,
StepperContent,
StepperIndicator,
StepperItem,
StepperNav,
StepperPanel,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from '@/components/reui/stepper'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
/** /**
* Add agent wizard — Stepper in Sheet. * Create agent install invite — Sheet (single form, no wizard).
* Preview: https://reui.io/preview/base/solution-agents-6 · sheet-8 * Preview: https://reui.io/preview/base/sheet-8 · sheet-1
* Docs: https://reui.io/docs/components/base/stepper * Docs: https://ui.shadcn.com/docs/components/base/sheet
*/ */
type Platform = 'linux' | 'mikrotik' type Platform = 'linux' | 'mikrotik'
@@ -57,7 +46,6 @@ interface AddAgentSheetProps {
export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) { export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
const qc = useQueryClient() const qc = useQueryClient()
const { copyToClipboard } = useCopyToClipboard() const { copyToClipboard } = useCopyToClipboard()
const [step, setStep] = useState(1)
const [name, setName] = useState('web-01') const [name, setName] = useState('web-01')
const [platform, setPlatform] = useState<Platform>('linux') const [platform, setPlatform] = useState<Platform>('linux')
const [created, setCreated] = useState<InstallLink | null>(null) const [created, setCreated] = useState<InstallLink | null>(null)
@@ -67,7 +55,6 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
setCreated(null) setCreated(null)
setName('web-01') setName('web-01')
setPlatform('linux') setPlatform('linux')
setStep(1)
} }
}, [open]) }, [open])
@@ -79,7 +66,6 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
}), }),
onSuccess: (link) => { onSuccess: (link) => {
setCreated(link) setCreated(link)
setStep(4)
toast.success('Агент создан') toast.success('Агент создан')
void qc.invalidateQueries({ queryKey: ['agents'] }) void qc.invalidateQueries({ queryKey: ['agents'] })
}, },
@@ -94,177 +80,102 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
toast.success('Скопировано') toast.success('Скопировано')
} }
function resetWizard() {
setCreated(null)
setName('web-01')
setPlatform('linux')
setStep(1)
}
return ( return (
<Sheet open={open} onOpenChange={onOpenChange}> <Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="flex flex-col gap-0 overflow-hidden sm:max-w-lg"> <SheetContent className="flex flex-col gap-0 overflow-hidden sm:max-w-md">
<SheetHeader className="shrink-0"> <SheetHeader className="shrink-0">
<SheetTitle> <SheetTitle>
{created ? 'Команда установки' : 'Добавить агента'} {created ? 'Команда установки' : 'Добавить агента'}
</SheetTitle> </SheetTitle>
<SheetDescription> <SheetDescription>
{created {created
? 'Агент в списке (Invited). Скопируйте one-liner на хост.' ? 'Агент уже в списке (Invited). Скопируйте one-liner и выполните на хосте.'
: 'Платформа → имя → подтверждение → install.'} : 'Создайте агента и короткую install-ссылку.'}
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
<ScrollArea className="flex-1 px-4"> <ScrollArea className="flex-1 px-4">
<div className="flex flex-col gap-4 py-2 pb-4"> <div className="grid auto-rows-min gap-4 py-2 pb-4">
<Stepper {!created ? (
value={created ? 4 : step} <>
onValueChange={setStep} <Field>
className="gap-4" <FieldLabel htmlFor="agent-name">Имя клиента</FieldLabel>
> <Input
<StepperNav className="gap-1"> id="agent-name"
{( value={name}
[ onChange={(e) => setName(e.target.value)}
[1, 'Платформа'], placeholder="web-01"
[2, 'Имя'], />
[3, 'Обзор'], </Field>
[4, 'Install'], <Field>
] as const <FieldLabel>Платформа</FieldLabel>
).map(([n, label], idx, arr) => ( <Select
<StepperItem items={[...PLATFORM_ITEMS]}
key={n} value={platform}
step={n} onValueChange={(v) => {
className="flex-1" if (v === 'linux' || v === 'mikrotik') setPlatform(v)
completed={Boolean(created) || step > n} }}
disabled={n === 4 && !created}
> >
<StepperTrigger className="w-full flex-col gap-1 rounded-md p-2"> <SelectTrigger className="w-full">
<StepperIndicator /> <SelectValue />
<StepperTitle className="text-xs">{label}</StepperTitle> </SelectTrigger>
</StepperTrigger> <SelectContent>
{idx < arr.length - 1 ? <StepperSeparator /> : null} {PLATFORM_ITEMS.map((item) => (
</StepperItem> <SelectItem key={item.value} value={item.value}>
))} {item.label}
</StepperNav> </SelectItem>
))}
<StepperPanel> </SelectContent>
<StepperContent value={1} className="grid gap-4"> </Select>
<p className="text-muted-foreground text-sm"> </Field>
Выберите ОС агента — от неё зависит install one-liner. </>
</p> ) : (
<Field> <>
<FieldLabel>Платформа</FieldLabel> <Field>
<Select <FieldLabel>По id</FieldLabel>
items={[...PLATFORM_ITEMS]} <div className="flex flex-col gap-2">
value={platform} <pre className="bg-muted overflow-x-auto rounded-lg p-3 text-xs break-all whitespace-pre-wrap">
onValueChange={(v) => { {created.curl?.by_id}
if (v === 'linux' || v === 'mikrotik') setPlatform(v) </pre>
}} <Button
type="button"
variant="outline"
size="sm"
className="self-start"
onClick={() => handleCopy(created.curl?.by_id ?? '')}
> >
<SelectTrigger className="w-full"> <Copy data-icon="inline-start" />
<SelectValue /> Копировать
</SelectTrigger> </Button>
<SelectContent>
{PLATFORM_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
</StepperContent>
<StepperContent value={2} className="grid gap-4">
<p className="text-muted-foreground text-sm">
Имя клиента в UI и в install-ссылке.
</p>
<Field>
<FieldLabel htmlFor="agent-name">Имя клиента</FieldLabel>
<Input
id="agent-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="web-01"
/>
</Field>
</StepperContent>
<StepperContent value={3} className="grid gap-3">
<p className="text-muted-foreground text-sm">
Проверьте параметры перед созданием.
</p>
<div className="bg-muted flex flex-col gap-1 rounded-lg p-3 text-sm">
<div>
<span className="text-muted-foreground">Платформа: </span>
{platform === 'mikrotik' ? 'MikroTik' : 'Linux'}
</div>
<div>
<span className="text-muted-foreground">Имя: </span>
{name.trim() || '—'}
</div>
</div> </div>
</StepperContent> </Field>
<Field>
<StepperContent value={4} className="grid gap-4"> <FieldLabel>Короткий slug</FieldLabel>
{created ? ( <div className="flex flex-col gap-2">
<> <pre className="bg-muted overflow-x-auto rounded-lg p-3 text-xs break-all whitespace-pre-wrap">
<Field> {created.curl?.by_slug}
<FieldLabel>По id</FieldLabel> </pre>
<div className="flex flex-col gap-2"> <Button
<pre className="bg-muted overflow-x-auto rounded-lg p-3 text-xs break-all whitespace-pre-wrap"> type="button"
{created.curl?.by_id} variant="outline"
</pre> size="sm"
<Button className="self-start"
type="button" onClick={() => handleCopy(created.curl?.by_slug ?? '')}
variant="outline" >
size="sm" <Copy data-icon="inline-start" />
className="self-start" Копировать
onClick={() => </Button>
handleCopy(created.curl?.by_id ?? '') </div>
} </Field>
> </>
<Copy data-icon="inline-start" /> )}
Копировать
</Button>
</div>
</Field>
<Field>
<FieldLabel>Короткий slug</FieldLabel>
<div className="flex flex-col gap-2">
<pre className="bg-muted overflow-x-auto rounded-lg p-3 text-xs break-all whitespace-pre-wrap">
{created.curl?.by_slug}
</pre>
<Button
type="button"
variant="outline"
size="sm"
className="self-start"
onClick={() =>
handleCopy(created.curl?.by_slug ?? '')
}
>
<Copy data-icon="inline-start" />
Копировать
</Button>
</div>
</Field>
</>
) : (
<p className="text-muted-foreground text-sm">
Сначала создайте агента на шаге «Обзор».
</p>
)}
</StepperContent>
</StepperPanel>
</Stepper>
</div> </div>
</ScrollArea> </ScrollArea>
<SheetFooter className="mt-0 shrink-0 flex-row flex-wrap gap-2 border-t"> <SheetFooter className="mt-0 shrink-0 flex-row flex-wrap gap-2 border-t">
{created ? ( {created ? (
<> <>
<Button variant="outline" onClick={resetWizard}> <Button variant="outline" onClick={() => setCreated(null)}>
Ещё агент Ещё агент
</Button> </Button>
<Button onClick={() => onOpenChange(false)}>Готово</Button> <Button onClick={() => onOpenChange(false)}>Готово</Button>
@@ -274,29 +185,9 @@ export function AddAgentSheet({ open, onOpenChange }: AddAgentSheetProps) {
<Button variant="outline" onClick={() => onOpenChange(false)}> <Button variant="outline" onClick={() => onOpenChange(false)}>
Отмена Отмена
</Button> </Button>
{step > 1 ? ( <Button disabled={!canCreate} onClick={() => create.mutate()}>
<Button Создать
variant="outline" </Button>
onClick={() => setStep((s) => Math.max(1, s - 1))}
>
Назад
</Button>
) : null}
{step < 3 ? (
<Button
onClick={() => setStep((s) => s + 1)}
disabled={step === 2 && !name.trim()}
>
Далее
</Button>
) : (
<Button
disabled={!canCreate}
onClick={() => create.mutate()}
>
Создать
</Button>
)}
</> </>
)} )}
</SheetFooter> </SheetFooter>
-18
View File
@@ -235,24 +235,6 @@ function AgentDetailPage() {
<AlertDescription>{a.last_apply_error}</AlertDescription> <AlertDescription>{a.last_apply_error}</AlertDescription>
</Alert> </Alert>
) : null} ) : null}
{a.status === 'pending' ? (
<Alert variant="warning">
<CircleAlertIcon />
<AlertTitle>Ожидает approve</AlertTitle>
<AlertDescription>
Агент записался, но политика не выдаётся до одобрения.
</AlertDescription>
</Alert>
) : null}
{a.status === 'invited' ? (
<Alert variant="info">
<CircleAlertIcon />
<AlertTitle>Invited</AlertTitle>
<AlertDescription>
Скопируйте install-команду и выполните на хосте.
</AlertDescription>
</Alert>
) : null}
<DetailPanel.Metrics <DetailPanel.Metrics
cards={[ cards={[