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