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:
@@ -24,6 +24,7 @@ import { useDomainPage } from '@/hooks/use-domain-page'
|
||||
import type { SubdomainTableRow } from '@/hooks/use-domain-page'
|
||||
import { PageShell } from '@/components/page-shell'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { CountedLineTabs } from '@/components/counted-line-tabs'
|
||||
import { DetailPanel, ResourcePage } from '@/components/reui-kit'
|
||||
import {
|
||||
SUBDOMAIN_TABS,
|
||||
@@ -53,12 +54,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@cfdm/ui/components/select'
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@cfdm/ui/components/tabs'
|
||||
import { TabsContent } from '@cfdm/ui/components/tabs'
|
||||
|
||||
export const Route = createFileRoute('/_auth/domains/$domainId/')({
|
||||
loader: async ({ context: { queryClient }, params }) => {
|
||||
@@ -327,50 +323,26 @@ function DomainOverviewPage() {
|
||||
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}
|
||||
onValueChange={setActiveTab}
|
||||
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">
|
||||
<DetailPanel.Metrics cards={metricCards} />
|
||||
<DetailPanel.Section
|
||||
@@ -438,50 +410,43 @@ function DomainOverviewPage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="subdomains" className="flex flex-col gap-4">
|
||||
<DetailPanel.Section title="Поддомены">
|
||||
<ResourcePage
|
||||
title="Поддомены"
|
||||
description={`Записи в зоне ${domain.zone_name}`}
|
||||
hideHeader
|
||||
tabs={SUBDOMAIN_TABS.map((tab) => ({ ...tab }))}
|
||||
tabFilter={subdomainTabFilter}
|
||||
filterFields={filterFields}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
onClearFilters={() =>
|
||||
setFilters(createDefaultSubdomainFilters())
|
||||
}
|
||||
getFilterFieldValue={subdomainFilterFieldValue}
|
||||
columns={columns}
|
||||
data={subdomainRows}
|
||||
getRowId={(row) => String(row.subdomain.id)}
|
||||
toolbarExtra={
|
||||
<ResourcePage
|
||||
title="Поддомены"
|
||||
description={`Записи в зоне ${domain.zone_name}`}
|
||||
hideHeader
|
||||
tabs={SUBDOMAIN_TABS.map((tab) => ({ ...tab }))}
|
||||
tabFilter={subdomainTabFilter}
|
||||
filterFields={filterFields}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
onClearFilters={() =>
|
||||
setFilters(createDefaultSubdomainFilters())
|
||||
}
|
||||
getFilterFieldValue={subdomainFilterFieldValue}
|
||||
columns={columns}
|
||||
data={subdomainRows}
|
||||
getRowId={(row) => String(row.subdomain.id)}
|
||||
toolbarExtra={
|
||||
<Button type="button" onClick={openCreateSheet}>
|
||||
Добавить поддомен
|
||||
</Button>
|
||||
}
|
||||
emptyState={{
|
||||
title: 'Поддомены не созданы',
|
||||
description: 'Добавьте поддомен для привязки сервисов',
|
||||
action: (
|
||||
<Button type="button" onClick={openCreateSheet}>
|
||||
Добавить поддомен
|
||||
</Button>
|
||||
}
|
||||
emptyState={{
|
||||
title: 'Поддомены не созданы',
|
||||
description: 'Добавьте поддомен для привязки сервисов',
|
||||
action: (
|
||||
<Button type="button" onClick={openCreateSheet}>
|
||||
Добавить поддомен
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</DetailPanel.Section>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="bindings" className="flex flex-col gap-4">
|
||||
<DetailPanel.Section
|
||||
title="Привязки сервисов"
|
||||
description="Сервисы, назначенные hostname в этой зоне"
|
||||
>
|
||||
<DomainBindingsCard bindings={bindings} />
|
||||
</DetailPanel.Section>
|
||||
<DomainBindingsCard bindings={bindings} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CountedLineTabs>
|
||||
|
||||
<SubdomainEditSheet
|
||||
mode={sheetMode}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { FolderTreeIcon, GlobeIcon, HashIcon } from 'lucide-react'
|
||||
import type { Filter } from '@/components/reui/filters'
|
||||
import {
|
||||
domainsListQueryOptions,
|
||||
groupDetailQueryOptions,
|
||||
} from '@/queries'
|
||||
import { PageShell } from '@/components/page-shell'
|
||||
import { ResourcePage } from '@/components/reui-kit'
|
||||
import { DetailPanel, ResourcePage } from '@/components/reui-kit'
|
||||
import {
|
||||
createDefaultGroupDomainFilters,
|
||||
groupDomainFilterFieldValue,
|
||||
@@ -52,48 +53,108 @@ function GroupDetailPage() {
|
||||
const filterFields = useGroupDomainFilterFields()
|
||||
const columns = useGroupDomainColumns()
|
||||
|
||||
const primaryAction = (
|
||||
const headerActions = (
|
||||
<Button variant="outline" nativeButton={false} render={<Link to="/groups" />}>
|
||||
К группам
|
||||
</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 (
|
||||
<PageShell>
|
||||
<ResourcePage
|
||||
title={group?.name ?? 'Группа'}
|
||||
description={
|
||||
group
|
||||
? `${group.domain_count} домен(ов) · slug: ${group.slug}`
|
||||
: 'Домены в группе'
|
||||
}
|
||||
filterFields={filterFields}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
onClearFilters={() => setFilters(createDefaultGroupDomainFilters())}
|
||||
getFilterFieldValue={groupDomainFilterFieldValue}
|
||||
columns={columns}
|
||||
data={domains ?? []}
|
||||
getRowId={(row) => String(row.id)}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
error={error}
|
||||
onRetry={() => {
|
||||
void refetchGroup()
|
||||
void refetchDomains()
|
||||
}}
|
||||
primaryAction={primaryAction}
|
||||
emptyState={{
|
||||
title: 'В группе нет доменов',
|
||||
description:
|
||||
'Перетащите домены на канбане групп или назначьте группу при импорте',
|
||||
action: (
|
||||
<Button variant="outline" nativeButton={false} render={<Link to="/groups" />}>
|
||||
Открыть канбан групп
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<DetailPanel>
|
||||
<DetailPanel.Header
|
||||
title={group?.name ?? 'Группа'}
|
||||
description={
|
||||
group
|
||||
? `${group.domain_count} домен(ов) · slug: ${group.slug}`
|
||||
: 'Домены в группе'
|
||||
}
|
||||
actions={headerActions}
|
||||
/>
|
||||
|
||||
{!isLoading && group ? (
|
||||
<DetailPanel.Metrics cards={metricCards} />
|
||||
) : null}
|
||||
|
||||
<DetailPanel.Section>
|
||||
<ResourcePage
|
||||
title="Домены группы"
|
||||
description="Зоны, назначенные в эту группу"
|
||||
hideHeader
|
||||
filterFields={filterFields}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
onClearFilters={() => setFilters(createDefaultGroupDomainFilters())}
|
||||
getFilterFieldValue={groupDomainFilterFieldValue}
|
||||
columns={columns}
|
||||
data={domains ?? []}
|
||||
getRowId={(row) => String(row.id)}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user