refactor(ui): выровнять lists и domain detail под ResourcePage
Bulk toolbar на Frame/muted; availability на Field+Item; domain detail на CountedLineTabs; group detail с DetailPanel. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -18,20 +18,6 @@ import { LoadingButton } from '@/components/loading-button'
|
|||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { QueryState } from '@/components/query-state'
|
import { QueryState } from '@/components/query-state'
|
||||||
import { TableSkeleton } from '@/components/skeletons'
|
import { TableSkeleton } from '@/components/skeletons'
|
||||||
import {
|
|
||||||
AppField,
|
|
||||||
AppFieldGroup,
|
|
||||||
AppFieldLabel,
|
|
||||||
} from '@/components/app-field'
|
|
||||||
import { AppInput } from '@/components/app-input'
|
|
||||||
import {
|
|
||||||
AppItem,
|
|
||||||
AppItemActions,
|
|
||||||
AppItemContent,
|
|
||||||
AppItemGroup,
|
|
||||||
AppItemSeparator,
|
|
||||||
AppItemTitle,
|
|
||||||
} from '@/components/app-item'
|
|
||||||
import {
|
import {
|
||||||
Frame,
|
Frame,
|
||||||
FrameDescription,
|
FrameDescription,
|
||||||
@@ -40,6 +26,16 @@ import {
|
|||||||
FrameTitle,
|
FrameTitle,
|
||||||
} from '@/components/reui/frame'
|
} from '@/components/reui/frame'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
|
import { Field, FieldGroup, FieldLabel } from '@cfdm/ui/components/field'
|
||||||
|
import { Input } from '@cfdm/ui/components/input'
|
||||||
|
import {
|
||||||
|
Item,
|
||||||
|
ItemActions,
|
||||||
|
ItemContent,
|
||||||
|
ItemGroup,
|
||||||
|
ItemSeparator,
|
||||||
|
ItemTitle,
|
||||||
|
} from '@cfdm/ui/components/item'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -178,21 +174,19 @@ export function DomainAvailabilityPanel({
|
|||||||
createMutation.mutate()
|
createMutation.mutate()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppFieldGroup className="grid gap-4 sm:grid-cols-[1fr_10rem_auto] sm:items-end">
|
<FieldGroup className="grid gap-4 sm:grid-cols-[1fr_10rem_auto] sm:items-end">
|
||||||
<AppField>
|
<Field>
|
||||||
<AppFieldLabel htmlFor="monitor-hostname">
|
<FieldLabel htmlFor="monitor-hostname">Hostname</FieldLabel>
|
||||||
Hostname
|
<Input
|
||||||
</AppFieldLabel>
|
|
||||||
<AppInput
|
|
||||||
id="monitor-hostname"
|
id="monitor-hostname"
|
||||||
value={hostname}
|
value={hostname}
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
onChange={(e) => setHostname(e.target.value)}
|
onChange={(e) => setHostname(e.target.value)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</AppField>
|
</Field>
|
||||||
<AppField>
|
<Field>
|
||||||
<AppFieldLabel htmlFor="monitor-type">Тип</AppFieldLabel>
|
<FieldLabel htmlFor="monitor-type">Тип</FieldLabel>
|
||||||
<Select
|
<Select
|
||||||
items={MONITOR_TYPE_ITEMS}
|
items={MONITOR_TYPE_ITEMS}
|
||||||
value={monitorType}
|
value={monitorType}
|
||||||
@@ -213,7 +207,7 @@ export function DomainAvailabilityPanel({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</AppField>
|
</Field>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={createMutation.isPending}
|
isLoading={createMutation.isPending}
|
||||||
@@ -223,7 +217,7 @@ export function DomainAvailabilityPanel({
|
|||||||
<PlusIcon data-icon="inline-start" />
|
<PlusIcon data-icon="inline-start" />
|
||||||
Добавить
|
Добавить
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</AppFieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
</Frame>
|
</Frame>
|
||||||
@@ -251,14 +245,14 @@ export function DomainAvailabilityPanel({
|
|||||||
description="Добавьте hostname для проверки доступности"
|
description="Добавьте hostname для проверки доступности"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AppItemGroup className="gap-0">
|
<ItemGroup className="gap-0">
|
||||||
{monitors.map((monitor, index) => (
|
{monitors.map((monitor, index) => (
|
||||||
<div key={monitor.id}>
|
<div key={monitor.id}>
|
||||||
<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">
|
<ItemTitle className="font-mono">
|
||||||
{monitor.hostname}
|
{monitor.hostname}
|
||||||
</AppItemTitle>
|
</ItemTitle>
|
||||||
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-sm">
|
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-sm">
|
||||||
<span className="uppercase">
|
<span className="uppercase">
|
||||||
{monitorTypeLabel(monitor.type)}
|
{monitorTypeLabel(monitor.type)}
|
||||||
@@ -271,8 +265,8 @@ export function DomainAvailabilityPanel({
|
|||||||
<span>Ещё не проверялся</span>
|
<span>Ещё не проверялся</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AppItemContent>
|
</ItemContent>
|
||||||
<AppItemActions className="gap-2">
|
<ItemActions className="gap-2">
|
||||||
<HealthCheckBadge
|
<HealthCheckBadge
|
||||||
status={monitor.last_status}
|
status={monitor.last_status}
|
||||||
latencyMs={monitor.last_latency_ms}
|
latencyMs={monitor.last_latency_ms}
|
||||||
@@ -299,14 +293,12 @@ export function DomainAvailabilityPanel({
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</AppItemActions>
|
</ItemActions>
|
||||||
</AppItem>
|
</Item>
|
||||||
{index < monitors.length - 1 ? (
|
{index < monitors.length - 1 ? <ItemSeparator /> : null}
|
||||||
<AppItemSeparator />
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</AppItemGroup>
|
</ItemGroup>
|
||||||
)}
|
)}
|
||||||
</QueryState>
|
</QueryState>
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { Badge } from '@/components/reui/badge'
|
||||||
|
import { Frame, FramePanel } from '@/components/reui/frame'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -42,89 +44,97 @@ export function DomainsBulkToolbar({
|
|||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 flex flex-wrap items-center gap-2 rounded-lg border bg-muted/40 px-3 py-2">
|
<Frame dense spacing="sm" className="mb-3 w-full">
|
||||||
<span className="text-muted-foreground text-sm tabular-nums">
|
<FramePanel className="flex flex-wrap items-center gap-2 bg-muted/30">
|
||||||
Выбрано: {count}
|
<Badge size="sm" variant="secondary" className="tabular-nums">
|
||||||
</span>
|
{count} выбрано
|
||||||
<Select
|
</Badge>
|
||||||
items={groupItems}
|
<Select
|
||||||
value={groupValue}
|
items={groupItems}
|
||||||
onValueChange={(v) => setGroupValue(v ?? 'none')}
|
value={groupValue}
|
||||||
>
|
onValueChange={(v) => setGroupValue(v ?? 'none')}
|
||||||
<SelectTrigger className="w-44" size="sm">
|
>
|
||||||
<SelectValue placeholder="Группа" />
|
<SelectTrigger className="w-44" size="sm">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Группа" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{groupItems.map((item) => (
|
<SelectContent>
|
||||||
<SelectItem key={item.value} value={item.value}>
|
{groupItems.map((item) => (
|
||||||
{item.label}
|
<SelectItem key={item.value} value={item.value}>
|
||||||
</SelectItem>
|
{item.label}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
<Button
|
</Select>
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
size="sm"
|
variant="outline"
|
||||||
disabled={isPending}
|
size="sm"
|
||||||
onClick={() =>
|
disabled={isPending}
|
||||||
onAssignGroup(groupValue === 'none' ? null : Number(groupValue))
|
onClick={() =>
|
||||||
}
|
onAssignGroup(groupValue === 'none' ? null : Number(groupValue))
|
||||||
>
|
}
|
||||||
В группу
|
>
|
||||||
</Button>
|
В группу
|
||||||
<Select
|
</Button>
|
||||||
items={envItems}
|
<Select
|
||||||
value={envValue}
|
items={envItems}
|
||||||
onValueChange={(v) => setEnvValue(v ?? 'none')}
|
value={envValue}
|
||||||
>
|
onValueChange={(v) => setEnvValue(v ?? 'none')}
|
||||||
<SelectTrigger className="w-32" size="sm">
|
>
|
||||||
<SelectValue placeholder="Env" />
|
<SelectTrigger className="w-32" size="sm">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Env" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{envItems.map((item) => (
|
<SelectContent>
|
||||||
<SelectItem key={item.value} value={item.value}>
|
{envItems.map((item) => (
|
||||||
{item.label}
|
<SelectItem key={item.value} value={item.value}>
|
||||||
</SelectItem>
|
{item.label}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
<Button
|
</Select>
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
size="sm"
|
variant="outline"
|
||||||
disabled={isPending}
|
size="sm"
|
||||||
onClick={() =>
|
disabled={isPending}
|
||||||
onSetEnvironment(
|
onClick={() =>
|
||||||
envValue === 'none'
|
onSetEnvironment(
|
||||||
? null
|
envValue === 'none'
|
||||||
: (envValue as 'prod' | 'staging' | 'dev'),
|
? null
|
||||||
)
|
: (envValue as 'prod' | 'staging' | 'dev'),
|
||||||
}
|
)
|
||||||
>
|
}
|
||||||
Env
|
>
|
||||||
</Button>
|
Env
|
||||||
<Input
|
</Button>
|
||||||
className="h-8 w-32"
|
<Input
|
||||||
placeholder="тег"
|
className="h-8 w-32"
|
||||||
value={tag}
|
placeholder="тег"
|
||||||
onChange={(e) => setTag(e.target.value)}
|
value={tag}
|
||||||
/>
|
onChange={(e) => setTag(e.target.value)}
|
||||||
<Button
|
/>
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
size="sm"
|
variant="outline"
|
||||||
disabled={isPending || !tag.trim()}
|
size="sm"
|
||||||
onClick={() => {
|
disabled={isPending || !tag.trim()}
|
||||||
onAddTag(tag.trim())
|
onClick={() => {
|
||||||
setTag('')
|
onAddTag(tag.trim())
|
||||||
}}
|
setTag('')
|
||||||
>
|
}}
|
||||||
+ тег
|
>
|
||||||
</Button>
|
+ тег
|
||||||
<Button type="button" variant="ghost" size="sm" disabled={isPending} onClick={onClear}>
|
</Button>
|
||||||
Снять выделение
|
<Button
|
||||||
</Button>
|
type="button"
|
||||||
</div>
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={onClear}
|
||||||
|
>
|
||||||
|
Снять выделение
|
||||||
|
</Button>
|
||||||
|
</FramePanel>
|
||||||
|
</Frame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useDomainPage } from '@/hooks/use-domain-page'
|
|||||||
import type { SubdomainTableRow } from '@/hooks/use-domain-page'
|
import type { SubdomainTableRow } from '@/hooks/use-domain-page'
|
||||||
import { PageShell } from '@/components/page-shell'
|
import { PageShell } from '@/components/page-shell'
|
||||||
import { QueryState } from '@/components/query-state'
|
import { QueryState } from '@/components/query-state'
|
||||||
|
import { CountedLineTabs } from '@/components/counted-line-tabs'
|
||||||
import { DetailPanel, ResourcePage } from '@/components/reui-kit'
|
import { DetailPanel, ResourcePage } from '@/components/reui-kit'
|
||||||
import {
|
import {
|
||||||
SUBDOMAIN_TABS,
|
SUBDOMAIN_TABS,
|
||||||
@@ -53,12 +54,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@cfdm/ui/components/select'
|
} from '@cfdm/ui/components/select'
|
||||||
import {
|
import { TabsContent } from '@cfdm/ui/components/tabs'
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from '@cfdm/ui/components/tabs'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_auth/domains/$domainId/')({
|
export const Route = createFileRoute('/_auth/domains/$domainId/')({
|
||||||
loader: async ({ context: { queryClient }, params }) => {
|
loader: async ({ context: { queryClient }, params }) => {
|
||||||
@@ -327,50 +323,26 @@ function DomainOverviewPage() {
|
|||||||
actions={headerActions}
|
actions={headerActions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs
|
<CountedLineTabs
|
||||||
|
tabs={[
|
||||||
|
{ id: 'overview', label: 'Обзор' },
|
||||||
|
{ id: 'dns', label: 'DNS' },
|
||||||
|
{ id: 'availability', label: 'Доступность' },
|
||||||
|
{
|
||||||
|
id: 'subdomains',
|
||||||
|
label: 'Поддомены',
|
||||||
|
count: subdomainRows.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bindings',
|
||||||
|
label: 'Привязки',
|
||||||
|
count: bindings.length,
|
||||||
|
},
|
||||||
|
]}
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onValueChange={setActiveTab}
|
onValueChange={setActiveTab}
|
||||||
className="flex w-full flex-col gap-4"
|
className="flex w-full flex-col gap-4"
|
||||||
>
|
>
|
||||||
<TabsList variant="line" className="gap-5">
|
|
||||||
<TabsTrigger
|
|
||||||
value="overview"
|
|
||||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
|
||||||
>
|
|
||||||
<span>Обзор</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="dns"
|
|
||||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
|
||||||
>
|
|
||||||
<span>DNS</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="availability"
|
|
||||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
|
||||||
>
|
|
||||||
<span>Доступность</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="subdomains"
|
|
||||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
|
||||||
>
|
|
||||||
<span>Поддомены</span>
|
|
||||||
<span className="bg-muted text-muted-foreground inline-flex min-w-5 items-center justify-center rounded-md px-1.5 py-0.5 text-xs tabular-nums">
|
|
||||||
{subdomainRows.length}
|
|
||||||
</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="bindings"
|
|
||||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
|
||||||
>
|
|
||||||
<span>Привязки</span>
|
|
||||||
<span className="bg-muted text-muted-foreground inline-flex min-w-5 items-center justify-center rounded-md px-1.5 py-0.5 text-xs tabular-nums">
|
|
||||||
{bindings.length}
|
|
||||||
</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="overview" className="flex flex-col gap-4">
|
<TabsContent value="overview" className="flex flex-col gap-4">
|
||||||
<DetailPanel.Metrics cards={metricCards} />
|
<DetailPanel.Metrics cards={metricCards} />
|
||||||
<DetailPanel.Section
|
<DetailPanel.Section
|
||||||
@@ -438,50 +410,43 @@ function DomainOverviewPage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="subdomains" className="flex flex-col gap-4">
|
<TabsContent value="subdomains" className="flex flex-col gap-4">
|
||||||
<DetailPanel.Section title="Поддомены">
|
<ResourcePage
|
||||||
<ResourcePage
|
title="Поддомены"
|
||||||
title="Поддомены"
|
description={`Записи в зоне ${domain.zone_name}`}
|
||||||
description={`Записи в зоне ${domain.zone_name}`}
|
hideHeader
|
||||||
hideHeader
|
tabs={SUBDOMAIN_TABS.map((tab) => ({ ...tab }))}
|
||||||
tabs={SUBDOMAIN_TABS.map((tab) => ({ ...tab }))}
|
tabFilter={subdomainTabFilter}
|
||||||
tabFilter={subdomainTabFilter}
|
filterFields={filterFields}
|
||||||
filterFields={filterFields}
|
filters={filters}
|
||||||
filters={filters}
|
onFiltersChange={setFilters}
|
||||||
onFiltersChange={setFilters}
|
onClearFilters={() =>
|
||||||
onClearFilters={() =>
|
setFilters(createDefaultSubdomainFilters())
|
||||||
setFilters(createDefaultSubdomainFilters())
|
}
|
||||||
}
|
getFilterFieldValue={subdomainFilterFieldValue}
|
||||||
getFilterFieldValue={subdomainFilterFieldValue}
|
columns={columns}
|
||||||
columns={columns}
|
data={subdomainRows}
|
||||||
data={subdomainRows}
|
getRowId={(row) => String(row.subdomain.id)}
|
||||||
getRowId={(row) => String(row.subdomain.id)}
|
toolbarExtra={
|
||||||
toolbarExtra={
|
<Button type="button" onClick={openCreateSheet}>
|
||||||
|
Добавить поддомен
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
emptyState={{
|
||||||
|
title: 'Поддомены не созданы',
|
||||||
|
description: 'Добавьте поддомен для привязки сервисов',
|
||||||
|
action: (
|
||||||
<Button type="button" onClick={openCreateSheet}>
|
<Button type="button" onClick={openCreateSheet}>
|
||||||
Добавить поддомен
|
Добавить поддомен
|
||||||
</Button>
|
</Button>
|
||||||
}
|
),
|
||||||
emptyState={{
|
}}
|
||||||
title: 'Поддомены не созданы',
|
/>
|
||||||
description: 'Добавьте поддомен для привязки сервисов',
|
|
||||||
action: (
|
|
||||||
<Button type="button" onClick={openCreateSheet}>
|
|
||||||
Добавить поддомен
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DetailPanel.Section>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="bindings" className="flex flex-col gap-4">
|
<TabsContent value="bindings" className="flex flex-col gap-4">
|
||||||
<DetailPanel.Section
|
<DomainBindingsCard bindings={bindings} />
|
||||||
title="Привязки сервисов"
|
|
||||||
description="Сервисы, назначенные hostname в этой зоне"
|
|
||||||
>
|
|
||||||
<DomainBindingsCard bindings={bindings} />
|
|
||||||
</DetailPanel.Section>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</CountedLineTabs>
|
||||||
|
|
||||||
<SubdomainEditSheet
|
<SubdomainEditSheet
|
||||||
mode={sheetMode}
|
mode={sheetMode}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { FolderTreeIcon, GlobeIcon, HashIcon } from 'lucide-react'
|
||||||
import type { Filter } from '@/components/reui/filters'
|
import type { Filter } from '@/components/reui/filters'
|
||||||
import {
|
import {
|
||||||
domainsListQueryOptions,
|
domainsListQueryOptions,
|
||||||
groupDetailQueryOptions,
|
groupDetailQueryOptions,
|
||||||
} from '@/queries'
|
} from '@/queries'
|
||||||
import { PageShell } from '@/components/page-shell'
|
import { PageShell } from '@/components/page-shell'
|
||||||
import { ResourcePage } from '@/components/reui-kit'
|
import { DetailPanel, ResourcePage } from '@/components/reui-kit'
|
||||||
import {
|
import {
|
||||||
createDefaultGroupDomainFilters,
|
createDefaultGroupDomainFilters,
|
||||||
groupDomainFilterFieldValue,
|
groupDomainFilterFieldValue,
|
||||||
@@ -52,48 +53,108 @@ function GroupDetailPage() {
|
|||||||
const filterFields = useGroupDomainFilterFields()
|
const filterFields = useGroupDomainFilterFields()
|
||||||
const columns = useGroupDomainColumns()
|
const columns = useGroupDomainColumns()
|
||||||
|
|
||||||
const primaryAction = (
|
const headerActions = (
|
||||||
<Button variant="outline" nativeButton={false} render={<Link to="/groups" />}>
|
<Button variant="outline" nativeButton={false} render={<Link to="/groups" />}>
|
||||||
К группам
|
К группам
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const metricCards = useMemo(() => {
|
||||||
|
if (!group) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'domains',
|
||||||
|
icon: <GlobeIcon aria-hidden="true" />,
|
||||||
|
label: 'Домены',
|
||||||
|
description: 'Зоны в этой группе',
|
||||||
|
footer: (
|
||||||
|
<span className="text-foreground text-lg font-semibold tabular-nums">
|
||||||
|
{group.domain_count}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'slug',
|
||||||
|
icon: <HashIcon aria-hidden="true" />,
|
||||||
|
label: 'Slug',
|
||||||
|
description: 'Идентификатор группы',
|
||||||
|
footer: (
|
||||||
|
<span className="text-foreground font-mono text-sm">{group.slug}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group',
|
||||||
|
icon: <FolderTreeIcon aria-hidden="true" />,
|
||||||
|
label: 'Группа',
|
||||||
|
description: group.name,
|
||||||
|
footer: (
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="h-auto p-0"
|
||||||
|
nativeButton={false}
|
||||||
|
render={<Link to="/groups" search={{ view: 'board' }} />}
|
||||||
|
>
|
||||||
|
Открыть канбан
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [group])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell>
|
<PageShell>
|
||||||
<ResourcePage
|
<DetailPanel>
|
||||||
title={group?.name ?? 'Группа'}
|
<DetailPanel.Header
|
||||||
description={
|
title={group?.name ?? 'Группа'}
|
||||||
group
|
description={
|
||||||
? `${group.domain_count} домен(ов) · slug: ${group.slug}`
|
group
|
||||||
: 'Домены в группе'
|
? `${group.domain_count} домен(ов) · slug: ${group.slug}`
|
||||||
}
|
: 'Домены в группе'
|
||||||
filterFields={filterFields}
|
}
|
||||||
filters={filters}
|
actions={headerActions}
|
||||||
onFiltersChange={setFilters}
|
/>
|
||||||
onClearFilters={() => setFilters(createDefaultGroupDomainFilters())}
|
|
||||||
getFilterFieldValue={groupDomainFilterFieldValue}
|
{!isLoading && group ? (
|
||||||
columns={columns}
|
<DetailPanel.Metrics cards={metricCards} />
|
||||||
data={domains ?? []}
|
) : null}
|
||||||
getRowId={(row) => String(row.id)}
|
|
||||||
isLoading={isLoading}
|
<DetailPanel.Section>
|
||||||
isError={isError}
|
<ResourcePage
|
||||||
error={error}
|
title="Домены группы"
|
||||||
onRetry={() => {
|
description="Зоны, назначенные в эту группу"
|
||||||
void refetchGroup()
|
hideHeader
|
||||||
void refetchDomains()
|
filterFields={filterFields}
|
||||||
}}
|
filters={filters}
|
||||||
primaryAction={primaryAction}
|
onFiltersChange={setFilters}
|
||||||
emptyState={{
|
onClearFilters={() => setFilters(createDefaultGroupDomainFilters())}
|
||||||
title: 'В группе нет доменов',
|
getFilterFieldValue={groupDomainFilterFieldValue}
|
||||||
description:
|
columns={columns}
|
||||||
'Перетащите домены на канбане групп или назначьте группу при импорте',
|
data={domains ?? []}
|
||||||
action: (
|
getRowId={(row) => String(row.id)}
|
||||||
<Button variant="outline" nativeButton={false} render={<Link to="/groups" />}>
|
isLoading={isLoading}
|
||||||
Открыть канбан групп
|
isError={isError}
|
||||||
</Button>
|
error={error}
|
||||||
),
|
onRetry={() => {
|
||||||
}}
|
void refetchGroup()
|
||||||
/>
|
void refetchDomains()
|
||||||
|
}}
|
||||||
|
emptyState={{
|
||||||
|
title: 'В группе нет доменов',
|
||||||
|
description:
|
||||||
|
'Перетащите домены на канбане групп или назначьте группу при импорте',
|
||||||
|
action: (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
nativeButton={false}
|
||||||
|
render={<Link to="/groups" />}
|
||||||
|
>
|
||||||
|
Открыть канбан групп
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DetailPanel.Section>
|
||||||
|
</DetailPanel>
|
||||||
</PageShell>
|
</PageShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user