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