refactor(ui): перевести bindings и login на Frame / auth-13

Заменить AppCard на Frame в domain/service bindings и login-form; TableSkeleton — Frame + Skeleton.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-17 13:47:31 +07:00
co-authored by Cursor
parent a15b52df95
commit c3ad676e75
4 changed files with 123 additions and 119 deletions
@@ -5,24 +5,24 @@ import { HealthCheckBadge } from '@/components/health-check-badge'
import { groupBindingsByHostname } from '@/lib/domain-ips' import { groupBindingsByHostname } from '@/lib/domain-ips'
import { useHealthRows } from '@/lib/use-aggregated-health' import { useHealthRows } from '@/lib/use-aggregated-health'
import type { ServiceBinding } from '@/lib/schemas' import type { ServiceBinding } from '@/lib/schemas'
import { AppBadge } from '@/components/app-badge' import { Badge } from '@/components/reui/badge'
import { AppButton } from '@/components/app-button'
import { import {
AppCard, Frame,
AppCardContent, FrameDescription,
AppCardDescription, FrameFooter,
AppCardFooter, FrameHeader,
AppCardHeader, FramePanel,
AppCardTitle, FrameTitle,
} from '@/components/app-card' } from '@/components/reui/frame'
import { Button } from '@cfdm/ui/components/button'
import { import {
AppItem, Item,
AppItemActions, ItemActions,
AppItemContent, ItemContent,
AppItemGroup, ItemGroup,
AppItemSeparator, ItemSeparator,
AppItemTitle, ItemTitle,
} from '@/components/app-item' } from '@cfdm/ui/components/item'
interface DomainBindingsCardProps { interface DomainBindingsCardProps {
bindings: ServiceBinding[] bindings: ServiceBinding[]
@@ -76,12 +76,12 @@ export function DomainBindingsCard({ bindings }: DomainBindingsCardProps) {
const entries = [...byHostname.entries()] const entries = [...byHostname.entries()]
return ( return (
<AppCard> <Frame dense spacing="sm" className="w-full">
<AppCardHeader> <FrameHeader>
<AppCardTitle>Привязки сервисов</AppCardTitle> <FrameTitle>Привязки сервисов</FrameTitle>
<AppCardDescription>Сервисы, назначенные hostname в этой зоне</AppCardDescription> <FrameDescription>Сервисы, назначенные hostname в этой зоне</FrameDescription>
</AppCardHeader> </FrameHeader>
<AppCardContent> <FramePanel>
{entries.length === 0 ? ( {entries.length === 0 ? (
<EmptyState <EmptyState
icon={Link2Icon} icon={Link2Icon}
@@ -89,7 +89,7 @@ export function DomainBindingsCard({ bindings }: DomainBindingsCardProps) {
description="Привяжите сервис к поддомену" description="Привяжите сервис к поддомену"
/> />
) : ( ) : (
<AppItemGroup className="gap-0"> <ItemGroup className="gap-0">
{entries.map(([hostname, groupBindings], index) => { {entries.map(([hostname, groupBindings], index) => {
const ips = [ const ips = [
...new Set( ...new Set(
@@ -104,9 +104,9 @@ export function DomainBindingsCard({ bindings }: DomainBindingsCardProps) {
] ]
return ( return (
<div key={hostname}> <div key={hostname}>
<AppItem size="sm" variant="muted" className="border-0 px-0"> <Item size="sm" variant="muted" className="border-0 px-0">
<AppItemContent className="gap-1"> <ItemContent className="gap-1">
<AppItemTitle className="font-mono">{hostname}</AppItemTitle> <ItemTitle className="font-mono">{hostname}</ItemTitle>
<div className="text-muted-foreground text-sm"> <div className="text-muted-foreground text-sm">
{uniqueServices(groupBindings).join(', ')} {uniqueServices(groupBindings).join(', ')}
</div> </div>
@@ -114,30 +114,30 @@ export function DomainBindingsCard({ bindings }: DomainBindingsCardProps) {
bindings={groupBindings} bindings={groupBindings}
ips={ips} ips={ips}
/> />
</AppItemContent> </ItemContent>
<AppItemActions> <ItemActions>
<AppBadge variant="outline" className="tabular-nums"> <Badge variant="outline" className="tabular-nums">
{ips.length} IP {ips.length} IP
</AppBadge> </Badge>
</AppItemActions> </ItemActions>
</AppItem> </Item>
{index < entries.length - 1 ? <AppItemSeparator /> : null} {index < entries.length - 1 ? <ItemSeparator /> : null}
</div> </div>
) )
})} })}
</AppItemGroup> </ItemGroup>
)} )}
</AppCardContent> </FramePanel>
<AppCardFooter> <FrameFooter>
<AppButton <Button
variant="link" variant="link"
className="h-auto p-0" className="h-auto p-0"
nativeButton={false} nativeButton={false}
render={<Link to="/services" search={{ domainId: bindings[0]?.domain_id }} />} render={<Link to="/services" search={{ domainId: bindings[0]?.domain_id }} />}
> >
К сервисам К сервисам
</AppButton> </Button>
</AppCardFooter> </FrameFooter>
</AppCard> </Frame>
) )
} }
+34 -28
View File
@@ -9,15 +9,19 @@ import { loginSchema, type LoginInput } from '@/lib/schemas'
import { FormFieldSimple } from '@/components/form-field' import { FormFieldSimple } from '@/components/form-field'
import { LoadingButton } from '@/components/loading-button' import { LoadingButton } from '@/components/loading-button'
import { import {
AppCard, Frame,
AppCardContent, FrameDescription,
AppCardDescription, FrameHeader,
AppCardHeader, FramePanel,
AppCardTitle, FrameTitle,
} from '@/components/app-card' } from '@/components/reui/frame'
import { AppFieldGroup } from '@/components/app-field' import {
import { AppInput } from '@/components/app-input' Alert,
import { AppAlert, AppAlertDescription, AppAlertTitle } from '@/components/app-alert' AlertDescription,
AlertTitle,
} from '@/components/reui/alert'
import { FieldGroup } from '@cfdm/ui/components/field'
import { Input } from '@cfdm/ui/components/input'
type LoginFormProps = React.ComponentProps<'div'> type LoginFormProps = React.ComponentProps<'div'>
@@ -45,25 +49,26 @@ export function LoginForm({ className, ...props }: LoginFormProps) {
return ( return (
<div className={cn('flex flex-col gap-6', className)} {...props}> <div className={cn('flex flex-col gap-6', className)} {...props}>
<AppCard> <Frame dense spacing="default" className="w-full">
<AppCardHeader className="text-center"> <FrameHeader className="text-center">
<AppCardTitle className="text-xl">Вход в систему</AppCardTitle> <FrameTitle className="text-xl">Вход в систему</FrameTitle>
<AppCardDescription> <FrameDescription>
Введите учётные данные для доступа к панели управления Введите учётные данные для доступа к панели управления
</AppCardDescription> </FrameDescription>
</AppCardHeader> </FrameHeader>
<AppCardContent> <FramePanel>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<AppFieldGroup> <FieldGroup>
<FormFieldSimple <FormFieldSimple
label="Имя пользователя" label="Имя пользователя"
htmlFor="username" htmlFor="username"
error={form.formState.errors.username} error={form.formState.errors.username}
> >
<AppInput <Input
id="username" id="username"
placeholder="admin" placeholder="admin"
autoComplete="username" autoComplete="username"
className="bg-background"
{...form.register('username')} {...form.register('username')}
aria-invalid={!!form.formState.errors.username} aria-invalid={!!form.formState.errors.username}
/> />
@@ -73,21 +78,22 @@ export function LoginForm({ className, ...props }: LoginFormProps) {
htmlFor="password" htmlFor="password"
error={form.formState.errors.password} error={form.formState.errors.password}
> >
<AppInput <Input
id="password" id="password"
type="password" type="password"
autoComplete="current-password" autoComplete="current-password"
className="bg-background"
{...form.register('password')} {...form.register('password')}
aria-invalid={!!form.formState.errors.password} aria-invalid={!!form.formState.errors.password}
/> />
</FormFieldSimple> </FormFieldSimple>
{rootError && ( {rootError ? (
<AppAlert variant="destructive"> <Alert variant="destructive">
<CircleAlertIcon /> <CircleAlertIcon />
<AppAlertTitle>Ошибка входа</AppAlertTitle> <AlertTitle>Ошибка входа</AlertTitle>
<AppAlertDescription>{rootError}</AppAlertDescription> <AlertDescription>{rootError}</AlertDescription>
</AppAlert> </Alert>
)} ) : null}
<LoadingButton <LoadingButton
type="submit" type="submit"
className="w-full" className="w-full"
@@ -96,10 +102,10 @@ export function LoginForm({ className, ...props }: LoginFormProps) {
> >
Войти Войти
</LoadingButton> </LoadingButton>
</AppFieldGroup> </FieldGroup>
</form> </form>
</AppCardContent> </FramePanel>
</AppCard> </Frame>
</div> </div>
) )
} }
@@ -3,22 +3,21 @@ import { CSS } from '@dnd-kit/utilities'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { AppBadge } from '@/components/app-badge' import { Badge } from '@/components/reui/badge'
import { AppButton } from '@/components/app-button'
import { import {
AppCard, Frame,
AppCardAction, FrameFooter,
AppCardContent, FrameHeader,
AppCardFooter, FramePanel,
AppCardHeader, FrameTitle,
AppCardTitle, } from '@/components/reui/frame'
} from '@/components/app-card' import { Button } from '@cfdm/ui/components/button'
import { import {
AppField, Field,
AppFieldGroup, FieldGroup,
AppFieldLabel, FieldLabel,
} from '@/components/app-field' } from '@cfdm/ui/components/field'
import { AppInput } from '@/components/app-input' import { Input } from '@cfdm/ui/components/input'
import { cn } from '@cfdm/ui/lib/utils' import { cn } from '@cfdm/ui/lib/utils'
import type { ServiceBinding } from '@/lib/schemas' import type { ServiceBinding } from '@/lib/schemas'
@@ -49,32 +48,31 @@ export function ServiceBindingCard({
: undefined : undefined
return ( return (
<AppCard <Frame
ref={setNodeRef} ref={setNodeRef}
size="sm" dense
spacing="sm"
style={style} style={style}
className={cn( className={cn(
'cursor-grab bg-card active:cursor-grabbing', 'cursor-grab active:cursor-grabbing',
isDragging && 'opacity-60 shadow-lg', isDragging && 'opacity-60 shadow-lg',
)} )}
{...listeners} {...listeners}
{...attributes} {...attributes}
> >
<AppCardHeader> <FrameHeader className="flex-row items-start justify-between gap-2">
<AppCardTitle>{binding.zone_name}</AppCardTitle> <FrameTitle>{binding.zone_name}</FrameTitle>
<AppCardAction> {binding.group_name ? (
{binding.group_name ? ( <Badge variant="secondary">{binding.group_name}</Badge>
<AppBadge variant="secondary">{binding.group_name}</AppBadge> ) : (
) : ( <Badge variant="outline">Без группы</Badge>
<AppBadge variant="outline">Без группы</AppBadge> )}
)} </FrameHeader>
</AppCardAction> <FramePanel className="flex flex-col gap-3">
</AppCardHeader> <FieldGroup className="flex flex-col gap-3">
<AppCardContent> <Field>
<AppFieldGroup className="flex flex-col gap-3"> <FieldLabel htmlFor={`hostname-${binding.id}`}>Hostname</FieldLabel>
<AppField> <Input
<AppFieldLabel htmlFor={`hostname-${binding.id}`}>Hostname</AppFieldLabel>
<AppInput
id={`hostname-${binding.id}`} id={`hostname-${binding.id}`}
value={hostname} value={hostname}
placeholder="@" placeholder="@"
@@ -87,10 +85,10 @@ export function ServiceBindingCard({
} }
}} }}
/> />
</AppField> </Field>
<AppField> <Field>
<AppFieldLabel htmlFor={`ip-${binding.id}`}>IPv4</AppFieldLabel> <FieldLabel htmlFor={`ip-${binding.id}`}>IPv4</FieldLabel>
<AppInput <Input
id={`ip-${binding.id}`} id={`ip-${binding.id}`}
value={ip} value={ip}
placeholder="192.168.1.1" placeholder="192.168.1.1"
@@ -103,15 +101,15 @@ export function ServiceBindingCard({
} }
}} }}
/> />
</AppField> </Field>
</AppFieldGroup> </FieldGroup>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<AppBadge>{binding.service_name}</AppBadge> <Badge>{binding.service_name}</Badge>
{binding.sync_status && <StatusBadge status={binding.sync_status} />} {binding.sync_status && <StatusBadge status={binding.sync_status} />}
</div> </div>
</AppCardContent> </FramePanel>
<AppCardFooter> <FrameFooter>
<AppButton <Button
variant="outline" variant="outline"
size="sm" size="sm"
render={ render={
@@ -122,8 +120,8 @@ export function ServiceBindingCard({
} }
> >
Домен Домен
</AppButton> </Button>
</AppCardFooter> </FrameFooter>
</AppCard> </Frame>
) )
} }
+6 -6
View File
@@ -1,10 +1,10 @@
import { Skeleton } from '@cfdm/ui/components/skeleton' import { Skeleton } from '@cfdm/ui/components/skeleton'
import { Card, CardContent } from '@cfdm/ui/components/card' import { Frame, FramePanel } from '@/components/reui/frame'
export function TableSkeleton({ rows = 6, cols = 4 }: { rows?: number; cols?: number }) { export function TableSkeleton({ rows = 6, cols = 4 }: { rows?: number; cols?: number }) {
return ( return (
<Card className="gap-0"> <Frame dense spacing="sm" className="w-full">
<CardContent className="p-0"> <FramePanel className="p-0">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex gap-2 border-b p-3"> <div className="flex gap-2 border-b p-3">
{Array.from({ length: cols }).map((_, i) => ( {Array.from({ length: cols }).map((_, i) => (
@@ -12,14 +12,14 @@ export function TableSkeleton({ rows = 6, cols = 4 }: { rows?: number; cols?: nu
))} ))}
</div> </div>
{Array.from({ length: rows }).map((_, r) => ( {Array.from({ length: rows }).map((_, r) => (
<div className="flex gap-2 border-b p-3" key={`r-${r}`}> <div className="flex gap-2 border-b p-3 last:border-b-0" key={`r-${r}`}>
{Array.from({ length: cols }).map((_, c) => ( {Array.from({ length: cols }).map((_, c) => (
<Skeleton className="h-4 flex-1" key={`c-${r}-${c}`} /> <Skeleton className="h-4 flex-1" key={`c-${r}-${c}`} />
))} ))}
</div> </div>
))} ))}
</div> </div>
</CardContent> </FramePanel>
</Card> </Frame>
) )
} }