feat(ui): refactor ServiceIpList and ServiceUnitCard components for improved layout and functionality
quality / commitlint (push) Skipped
quality / changes (push) Successful in 10s
quality / api (push) Skipped
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 4s
quality / web (push) Successful in 53s
CD / quality (push) Successful in 1m9s
CD / publish (push) Successful in 1m37s

- Replaced div elements with Item components in ServiceIpList for better alignment and structure.
- Introduced alignWithMenu prop to align IP Switch with the card menu.
- Updated ServiceUnitCard to utilize Item components, enhancing the visual consistency and responsiveness of the service card layout.
- Improved overall user experience by streamlining component structure and ensuring proper alignment of UI elements.
This commit is contained in:
Denozordec
2026-08-19 22:37:54 +07:00
parent 44d0eb0114
commit 6bced71037
2 changed files with 143 additions and 96 deletions
@@ -8,6 +8,13 @@ import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { serviceDisplayFqdns } from '@/lib/service-utils' import { serviceDisplayFqdns } from '@/lib/service-utils'
import type { ServiceView } from '@/lib/schemas' import type { ServiceView } from '@/lib/schemas'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import {
Item,
ItemActions,
ItemContent,
ItemGroup,
ItemMedia,
} from '@cfdm/ui/components/item'
import { Switch } from '@cfdm/ui/components/switch' import { Switch } from '@cfdm/ui/components/switch'
import { import {
Tooltip, Tooltip,
@@ -17,6 +24,9 @@ import {
} from '@cfdm/ui/components/tooltip' } from '@cfdm/ui/components/tooltip'
import { cn } from '@cfdm/ui/lib/utils' import { cn } from '@cfdm/ui/lib/utils'
/** Matches `Button size="icon-sm"` so Switch columns align with the card menu. */
const MENU_SLOT_CLASS = 'size-7 shrink-0'
export function CopyFqdnButton({ export function CopyFqdnButton({
value, value,
className, className,
@@ -126,6 +136,8 @@ interface ServiceIpListProps {
togglingIp?: string | null togglingIp?: string | null
ipToggleDisabled?: boolean ipToggleDisabled?: boolean
onToggleIp?: (ip: string, enabled: boolean) => void onToggleIp?: (ip: string, enabled: boolean) => void
/** Invisible icon-sm slot so IP Switch lines up with the card overflow menu. */
alignWithMenu?: boolean
className?: string className?: string
emptyLabel?: string emptyLabel?: string
copyable?: boolean copyable?: boolean
@@ -139,6 +151,7 @@ export function ServiceIpList({
togglingIp = null, togglingIp = null,
ipToggleDisabled = false, ipToggleDisabled = false,
onToggleIp, onToggleIp,
alignWithMenu = false,
className, className,
emptyLabel = 'Нет IP', emptyLabel = 'Нет IP',
copyable = false, copyable = false,
@@ -155,14 +168,20 @@ export function ServiceIpList({
const healthByIp = new Map(ipHealth.map((row) => [row.ip, row])) const healthByIp = new Map(ipHealth.map((row) => [row.ip, row]))
const visible = onToggleIp ? ips : ips.slice(0, VISIBLE_IP_LIMIT) const visible = onToggleIp ? ips : ips.slice(0, VISIBLE_IP_LIMIT)
const extraCount = ips.length - visible.length const extraCount = ips.length - visible.length
const showMenuSlot = Boolean(onToggleIp && alignWithMenu)
return ( return (
<div className={cn('flex min-w-0 flex-col gap-1', className)}> <ItemGroup className={cn('gap-1', className)}>
{visible.map((ip) => { {visible.map((ip) => {
const health = healthByIp.get(ip) const health = healthByIp.get(ip)
const enabled = ipEnabled[ip] !== false const enabled = ipEnabled[ip] !== false
return ( return (
<div key={ip} className="flex min-w-0 items-center gap-1.5"> <Item
key={ip}
size="sm"
className="w-full min-w-0 flex-nowrap border-0 p-0"
>
<ItemMedia>
<HealthCheckBadge <HealthCheckBadge
status={health?.status ?? 'unknown'} status={health?.status ?? 'unknown'}
latencyMs={health?.latency_ms} latencyMs={health?.latency_ms}
@@ -172,32 +191,46 @@ export function ServiceIpList({
provider={health?.provider} provider={health?.provider}
size="xs" size="xs"
/> />
</ItemMedia>
<ItemContent className="min-w-0 gap-0">
<div className="flex min-w-0 items-center gap-1.5">
<TruncatedText <TruncatedText
className={cn( className={cn(
'min-w-0 font-mono text-xs', 'min-w-0 font-mono text-xs',
enabled ? 'text-muted-foreground' : 'text-muted-foreground/60', enabled
? 'text-muted-foreground'
: 'text-muted-foreground/60',
textClassName, textClassName,
)} )}
> >
{ip} {ip}
</TruncatedText> </TruncatedText>
{copyable ? <CopyFqdnButton value={ip} /> : null} {copyable ? <CopyFqdnButton value={ip} /> : null}
</div>
</ItemContent>
{onToggleIp ? ( {onToggleIp ? (
<ItemActions className="ml-auto shrink-0 gap-1">
<Switch <Switch
size="sm" size="sm"
className="ml-auto shrink-0" className="shrink-0"
checked={enabled} checked={enabled}
disabled={ipToggleDisabled || togglingIp === ip} disabled={ipToggleDisabled || togglingIp === ip}
onClick={(event) => { onClick={(event) => {
event.stopPropagation() event.stopPropagation()
}} }}
onCheckedChange={(checked) => onToggleIp(ip, Boolean(checked))} onCheckedChange={(checked) =>
onToggleIp(ip, Boolean(checked))
}
aria-label={ aria-label={
enabled ? `Выключить IP ${ip}` : `Включить IP ${ip}` enabled ? `Выключить IP ${ip}` : `Включить IP ${ip}`
} }
/> />
{showMenuSlot ? (
<span className={MENU_SLOT_CLASS} aria-hidden="true" />
) : null} ) : null}
</div> </ItemActions>
) : null}
</Item>
) )
})} })}
{extraCount > 0 ? ( {extraCount > 0 ? (
@@ -224,6 +257,6 @@ export function ServiceIpList({
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
) : null} ) : null}
</div> </ItemGroup>
) )
} }
@@ -5,7 +5,6 @@ import { Badge } from '@/components/reui/badge'
import { import {
Frame, Frame,
FrameDescription, FrameDescription,
FrameHeader,
FramePanel, FramePanel,
FrameTitle, FrameTitle,
} from '@/components/reui/frame' } from '@/components/reui/frame'
@@ -23,6 +22,12 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@cfdm/ui/components/dropdown-menu' } from '@cfdm/ui/components/dropdown-menu'
import {
Item,
ItemActions,
ItemContent,
ItemMedia,
} from '@cfdm/ui/components/item'
import { Switch } from '@cfdm/ui/components/switch' import { Switch } from '@cfdm/ui/components/switch'
import { import {
Tooltip, Tooltip,
@@ -31,6 +36,12 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@cfdm/ui/components/tooltip' } from '@cfdm/ui/components/tooltip'
/**
* Compact service card — settings-8 DNA (Badge + copy + Switch + menu).
* Preview: https://reui.io/preview/base/settings-8
* Frame: https://reui.io/docs/components/base/frame
* IconTile: https://reui.io/docs/components/base/icon-tile
*/
interface ServiceUnitCardProps { interface ServiceUnitCardProps {
service: ServiceView service: ServiceView
togglingId: number | null togglingId: number | null
@@ -55,9 +66,10 @@ export function ServiceUnitCard({
const extraCount = Math.max(0, fqdns.length - 1) const extraCount = Math.max(0, fqdns.length - 1)
return ( return (
<Frame dense spacing="sm" className="h-full min-w-0 overflow-hidden"> <Frame stacked spacing="sm" className="h-full min-w-0">
<FrameHeader className="flex-row items-center justify-between gap-2"> <FramePanel fit>
<div className="flex min-w-0 items-center gap-2"> <Item size="sm" className="w-full min-w-0 flex-nowrap border-0 p-0">
<ItemMedia>
<IconTile <IconTile
variant="elevated" variant="elevated"
size="sm" size="sm"
@@ -66,7 +78,8 @@ export function ServiceUnitCard({
> >
<ServerIcon /> <ServerIcon />
</IconTile> </IconTile>
<div className="flex min-w-0 flex-col gap-px"> </ItemMedia>
<ItemContent className="min-w-0 gap-px">
<FrameTitle className="min-w-0 truncate text-sm"> <FrameTitle className="min-w-0 truncate text-sm">
<Link <Link
to="/services/$serviceId" to="/services/$serviceId"
@@ -108,9 +121,8 @@ export function ServiceUnitCard({
<CopyFqdnButton value={fqdns.join('\n')} /> <CopyFqdnButton value={fqdns.join('\n')} />
) : null} ) : null}
</div> </div>
</div> </ItemContent>
</div> <ItemActions className="ml-auto shrink-0 gap-1">
<div className="flex shrink-0 items-center gap-1">
<Switch <Switch
size="sm" size="sm"
checked={service.enabled} checked={service.enabled}
@@ -157,12 +169,14 @@ export function ServiceUnitCard({
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </ItemActions>
</FrameHeader> </Item>
</FramePanel>
<FramePanel className="flex min-w-0 flex-col gap-1 pt-0 shadow-none!"> <FramePanel className="flex min-w-0 flex-col">
<ServiceIpList <ServiceIpList
copyable copyable
alignWithMenu
ips={service.ips ?? []} ips={service.ips ?? []}
ipHealth={service.ip_health ?? []} ipHealth={service.ip_health ?? []}
ipEnabled={service.ip_enabled ?? {}} ipEnabled={service.ip_enabled ?? {}}