feat(web): enhance UI components and improve documentation
Build, Test, and Push CFDM Docker Image / test (push) Failing after 37s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
Build, Test, and Push CFDM Docker Image / test (push) Failing after 37s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
- Updated frontend UI patterns documentation to reflect new components and their usage. - Enhanced CertKpiCards for better KPI visualization and streamlined data handling. - Refactored OpsDashboard and ResourcePage for improved layout consistency and loading states. - Introduced CatalogBoardToggle for better navigation between views in the Groups and Services pages. - Improved Tabs component styles for better responsiveness and accessibility. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { MoreHorizontalIcon, SearchIcon } from 'lucide-react'
|
||||
|
||||
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
||||
import { createFilter, type FilterFieldConfig } from '@/components/reui/filters'
|
||||
import { Button } from '@cfdm/ui/components/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@cfdm/ui/components/dropdown-menu'
|
||||
|
||||
export interface GroupCatalogRow {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
domainCount: number
|
||||
}
|
||||
|
||||
export const GROUP_TABS = [
|
||||
{ id: 'all', label: 'Все' },
|
||||
{ id: 'with_domains', label: 'С доменами' },
|
||||
{ id: 'empty', label: 'Пустые' },
|
||||
] as const
|
||||
|
||||
export function groupTabFilter(item: GroupCatalogRow, tabId: string) {
|
||||
if (tabId === 'with_domains') return item.domainCount > 0
|
||||
if (tabId === 'empty') return item.domainCount === 0
|
||||
return true
|
||||
}
|
||||
|
||||
export function createDefaultGroupFilters() {
|
||||
return [createFilter('name', 'contains', [''])]
|
||||
}
|
||||
|
||||
export function useGroupFilterFields() {
|
||||
return useMemo<FilterFieldConfig[]>(
|
||||
() => [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Название',
|
||||
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
||||
type: 'text',
|
||||
className: 'w-52',
|
||||
placeholder: 'Поиск…',
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
export function groupFilterFieldValue(item: GroupCatalogRow, field: string) {
|
||||
switch (field) {
|
||||
case 'name':
|
||||
return `${item.name} ${item.slug}`.toLowerCase()
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function useGroupColumns({
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
onEdit: (group: GroupCatalogRow) => void
|
||||
onDelete: (group: GroupCatalogRow) => void
|
||||
}) {
|
||||
const columns = useMemo<ColumnDef<GroupCatalogRow>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'name',
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Название" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto max-w-64 truncate p-0 font-medium"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link
|
||||
to="/groups/$groupId"
|
||||
params={{ groupId: String(row.original.id) }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{row.original.name}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'slug',
|
||||
accessorKey: 'slug',
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Slug" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground font-mono text-xs">
|
||||
{row.original.slug}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'domainCount',
|
||||
accessorKey: 'domainCount',
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Домены" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="tabular-nums">{row.original.domainCount}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
header: () => <span className="sr-only">Действия</span>,
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
aria-label="Действия"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
to="/groups/$groupId"
|
||||
params={{ groupId: String(row.original.id) }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Открыть
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
||||
Изменить
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => onDelete(row.original)}
|
||||
>
|
||||
Удалить
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
},
|
||||
],
|
||||
[onEdit, onDelete],
|
||||
)
|
||||
|
||||
return { columns }
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { MoreHorizontalIcon, SearchIcon } from 'lucide-react'
|
||||
|
||||
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
||||
import { createFilter, type FilterFieldConfig } from '@/components/reui/filters'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import type { ServiceView } from '@/lib/schemas'
|
||||
import { Button } from '@cfdm/ui/components/button'
|
||||
import { Switch } from '@cfdm/ui/components/switch'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@cfdm/ui/components/dropdown-menu'
|
||||
|
||||
export interface ServiceCatalogRow {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
enabled: boolean
|
||||
groupId: number | null
|
||||
groupName: string | null
|
||||
domainIds: number[]
|
||||
service: ServiceView
|
||||
}
|
||||
|
||||
export const SERVICE_TABS = [
|
||||
{ id: 'all', label: 'Все' },
|
||||
{ id: 'enabled', label: 'Включены' },
|
||||
{ id: 'disabled', label: 'Выключены' },
|
||||
{ id: 'ungrouped', label: 'Без группы' },
|
||||
] as const
|
||||
|
||||
export function serviceTabFilter(item: ServiceCatalogRow, tabId: string) {
|
||||
if (tabId === 'enabled') return item.enabled
|
||||
if (tabId === 'disabled') return !item.enabled
|
||||
if (tabId === 'ungrouped') return item.groupId == null
|
||||
return true
|
||||
}
|
||||
|
||||
export function createDefaultServiceFilters() {
|
||||
return [createFilter('name', 'contains', [''])]
|
||||
}
|
||||
|
||||
export function useServiceFilterFields() {
|
||||
return useMemo<FilterFieldConfig[]>(
|
||||
() => [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Название',
|
||||
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
||||
type: 'text',
|
||||
className: 'w-52',
|
||||
placeholder: 'Поиск…',
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
export function serviceFilterFieldValue(item: ServiceCatalogRow, field: string) {
|
||||
switch (field) {
|
||||
case 'name':
|
||||
return `${item.name} ${item.slug} ${item.groupName ?? ''}`.toLowerCase()
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function useServiceColumns({
|
||||
onEdit,
|
||||
onDelete,
|
||||
onToggle,
|
||||
togglingId,
|
||||
}: {
|
||||
onEdit: (service: ServiceView) => void
|
||||
onDelete: (service: ServiceView) => void
|
||||
onToggle: (serviceId: number, enabled: boolean) => void
|
||||
togglingId: number | null
|
||||
}) {
|
||||
const columns = useMemo<ColumnDef<ServiceCatalogRow>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'name',
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Название" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<button
|
||||
type="button"
|
||||
className="text-foreground max-w-64 truncate text-left font-medium hover:underline"
|
||||
onClick={() => onEdit(row.original.service)}
|
||||
>
|
||||
{row.original.name}
|
||||
</button>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'group',
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Группа" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{row.original.groupName ?? 'Без группы'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'enabled',
|
||||
accessorKey: 'enabled',
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Статус" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={row.original.enabled}
|
||||
disabled={togglingId === row.original.id}
|
||||
onCheckedChange={(checked) =>
|
||||
onToggle(row.original.id, Boolean(checked))
|
||||
}
|
||||
aria-label={
|
||||
row.original.enabled ? 'Выключить сервис' : 'Включить сервис'
|
||||
}
|
||||
/>
|
||||
<StatusBadge
|
||||
status={row.original.enabled ? 'active' : 'disabled'}
|
||||
label={row.original.enabled ? 'Вкл' : 'Выкл'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
header: () => <span className="sr-only">Действия</span>,
|
||||
cell: ({ row }) => (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
aria-label="Действия"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onEdit(row.original.service)}>
|
||||
Изменить
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => onDelete(row.original.service)}
|
||||
>
|
||||
Удалить
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
),
|
||||
},
|
||||
],
|
||||
[onEdit, onDelete, onToggle, togglingId],
|
||||
)
|
||||
|
||||
return { columns }
|
||||
}
|
||||
Reference in New Issue
Block a user