refactor(web): simplify AddAgentSheet by removing Stepper and updating layout
- 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:
@@ -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>
|
||||||
|
|||||||
@@ -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={[
|
||||||
|
|||||||
Reference in New Issue
Block a user