Files
dns-autoresolver/web/src/pages/AccountsPage.tsx
T

202 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useId } from "react"
import { Controller, useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { ExternalLink, Inbox, KeyRound, Loader2, Plus, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldSet,
} from "@/components/ui/field"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { useAccounts, useCreateAccount, useDeleteAccount } from "@/hooks/useApi"
const createAccountSchema = z.object({
provider: z.literal("selectel"),
secret: z.string().min(1, "Укажите API-ключ"),
comment: z.string().min(1, "Укажите комментарий"),
})
type CreateAccountForm = z.infer<typeof createAccountSchema>
export function AccountsPage() {
const accounts = useAccounts()
const createAccount = useCreateAccount()
const deleteAccount = useDeleteAccount()
const secretFieldId = useId()
const commentFieldId = useId()
const accountList = accounts.data ?? []
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm<CreateAccountForm>({
resolver: zodResolver(createAccountSchema),
defaultValues: { provider: "selectel", secret: "", comment: "" },
})
function onSubmit(values: CreateAccountForm) {
createAccount.mutate(values, {
onSuccess: () => reset({ provider: "selectel", secret: "", comment: "" }),
})
}
function onDelete(id: string, comment: string) {
if (window.confirm(`Удалить учётную запись «${comment}»? Действие необратимо.`)) {
deleteAccount.mutate(id)
}
}
return (
<div className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-8">
<header className="flex flex-col gap-1">
<span className="font-dns text-[11px] tracking-wider text-muted-foreground uppercase">
providers
</span>
<h1 className="text-xl font-semibold tracking-tight text-foreground">Учётные записи</h1>
</header>
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4 rounded-xl border border-border bg-card/60 p-4"
>
<FieldSet className="gap-3">
<FieldGroup className="gap-3 sm:flex-row sm:items-start">
<Field className="sm:max-w-56">
<FieldLabel htmlFor={secretFieldId}>API-ключ Selectel</FieldLabel>
<FieldContent>
<Controller
control={control}
name="secret"
render={({ field }) => (
<Input
{...field}
id={secretFieldId}
type="password"
autoComplete="off"
placeholder="••••••••••••"
aria-invalid={!!errors.secret}
/>
)}
/>
<FieldError errors={[errors.secret]} />
</FieldContent>
</Field>
<Field>
<FieldLabel htmlFor={commentFieldId}>Комментарий</FieldLabel>
<FieldContent>
<Controller
control={control}
name="comment"
render={({ field }) => (
<Input
{...field}
id={commentFieldId}
placeholder="Например, основной аккаунт"
aria-invalid={!!errors.comment}
/>
)}
/>
<FieldError errors={[errors.comment]} />
</FieldContent>
</Field>
</FieldGroup>
<FieldDescription className="flex items-start gap-2 rounded-lg border border-border/60 bg-background/40 px-3 py-2.5">
<KeyRound className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" strokeWidth={1.75} />
<span>
Провайдер: <span className="font-dns text-foreground">selectel</span>. Ключ создаётся
в панели управления Selectel раздел «API-ключи» и используется только для
запроса, храниться в открытом виде он не будет.{" "}
<a
href="https://my.selectel.ru/profile/apikeys"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1"
>
Получить ключ на my.selectel.ru
<ExternalLink className="size-3" strokeWidth={1.75} />
</a>
</span>
</FieldDescription>
</FieldSet>
<div className="flex items-center justify-between gap-3 border-t border-border pt-3">
{createAccount.isError && (
<span role="alert" className="font-dns text-xs text-destructive">
{createAccount.error.message}
</span>
)}
<Button type="submit" disabled={createAccount.isPending} className="ml-auto">
{createAccount.isPending ? (
<Loader2 className="size-4 animate-spin" strokeWidth={1.75} />
) : (
<Plus className="size-4" strokeWidth={1.75} />
)}
Добавить учётку
</Button>
</div>
</form>
{deleteAccount.isError && (
<span role="alert" className="font-dns text-xs text-destructive">
{deleteAccount.error.message}
</span>
)}
{accountList.length === 0 ? (
<div className="flex flex-col items-center gap-2 rounded-xl border border-dashed border-border px-4 py-12 text-center text-sm text-muted-foreground">
<Inbox className="size-6" strokeWidth={1.5} />
Учётных записей пока нет добавьте первую выше.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Провайдер</TableHead>
<TableHead>Комментарий</TableHead>
<TableHead className="text-right">Действия</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{accountList.map((a) => (
<TableRow key={a.id}>
<TableCell className="font-dns">{a.provider}</TableCell>
<TableCell>{a.comment}</TableCell>
<TableCell className="text-right">
<Button
variant="destructive"
size="icon-sm"
aria-label={`Удалить учётку ${a.comment}`}
onClick={() => onDelete(a.id, a.comment)}
disabled={deleteAccount.isPending}
>
<Trash2 className="size-3.5" strokeWidth={1.75} />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
)
}