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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user