Files
vps-tracker/apps/web/src/components/integrations/app-switcher-editor.tsx
T
DenozordecandCursor 0e9a6a6a21
Docker / build (push) Failing after 22s
feat: интеграция с CF Domain Manager
Синхронизация bindings в vps_domains с привязкой по IP, API приёма с Bearer-токеном, настройки и App Switcher из SQLite, UI доменов на VPS и уведомление CFDM при vps_down.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:45:16 +07:00

117 lines
4.6 KiB
TypeScript

import { useFieldArray, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { PlusIcon, Trash2Icon } from 'lucide-react'
import { z } from 'zod'
import { appSwitcherConfigSchema } from '@cfdm/shared/contracts/app-switcher'
import { Button } from '@cfdm/ui/components/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cfdm/ui/components/card'
import { FieldGroup } from '@cfdm/ui/components/field'
import { Input } from '@cfdm/ui/components/input'
import { FormField } from '@/components/form-field'
import { SelectField } from '@/components/select-field'
import { LoadingButton } from '@/components/loading-button'
import { APP_SWITCHER_ICONS, type AppSwitcherIconName } from '@/lib/app-switcher-config'
const ICON_OPTIONS = (Object.keys(APP_SWITCHER_ICONS) as AppSwitcherIconName[]).map((icon) => ({
value: icon,
label: icon,
}))
const formSchema = z.object({
menuLabel: z.string().min(1),
apps: appSwitcherConfigSchema.shape.apps,
})
export type AppSwitcherFormValues = z.infer<typeof formSchema>
interface AppSwitcherEditorProps {
defaultValues: AppSwitcherFormValues
onSave: (values: AppSwitcherFormValues) => void
isSaving?: boolean
}
export function AppSwitcherEditor({ defaultValues, onSave, isSaving }: AppSwitcherEditorProps) {
const form = useForm<AppSwitcherFormValues>({
resolver: zodResolver(formSchema),
defaultValues,
})
const { fields, append, remove } = useFieldArray({ control: form.control, name: 'apps' })
return (
<Card>
<CardHeader>
<CardTitle>Связанные приложения</CardTitle>
<CardDescription>URL для переключателя в sidebar и deep links</CardDescription>
</CardHeader>
<CardContent>
<form
className="flex flex-col gap-4"
onSubmit={(e) => void form.handleSubmit(onSave)(e)}
>
<FieldGroup>
<FormField label="Заголовок меню" htmlFor="menu-label">
<Input id="menu-label" {...form.register('menuLabel')} />
</FormField>
{fields.map((field, index) => (
<div key={field.id} className="grid gap-3 rounded-lg border p-3 md:grid-cols-2">
<FormField label="ID" htmlFor={`app-id-${index}`}>
<Input id={`app-id-${index}`} {...form.register(`apps.${index}.id`)} />
</FormField>
<FormField label="Название" htmlFor={`app-name-${index}`}>
<Input id={`app-name-${index}`} {...form.register(`apps.${index}.name`)} />
</FormField>
<FormField label="URL" htmlFor={`app-url-${index}`}>
<Input id={`app-url-${index}`} className="md:col-span-2" {...form.register(`apps.${index}.url`)} />
</FormField>
<FormField label="Иконка" htmlFor={`app-icon-${index}`}>
<SelectField
triggerId={`app-icon-${index}`}
value={form.watch(`apps.${index}.icon`)}
onValueChange={(v) =>
form.setValue(`apps.${index}.icon`, (v ?? 'server') as AppSwitcherIconName, {
shouldDirty: true,
})
}
options={ICON_OPTIONS}
/>
</FormField>
<div className="flex items-end justify-end">
<Button
type="button"
variant="outline"
size="icon"
disabled={fields.length <= 1}
onClick={() => remove(index)}
>
<Trash2Icon className="size-4" />
</Button>
</div>
</div>
))}
<Button
type="button"
variant="outline"
className="w-fit"
onClick={() =>
append({
id: `app-${fields.length + 1}`,
name: 'Приложение',
url: 'http://localhost:3000',
icon: 'server',
})
}
>
<PlusIcon data-icon="inline-start" />
Добавить приложение
</Button>
</FieldGroup>
<LoadingButton type="submit" className="w-fit" loading={isSaving} disabled={!form.formState.isDirty}>
Сохранить приложения
</LoadingButton>
</form>
</CardContent>
</Card>
)
}