feat(web): pages and tables as on shared mockup boards
quality / changes (push) Successful in 9s
quality / go (push) Skipped
quality / bird2 (push) Skipped
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 23s
quality / web (push) Successful in 1m22s
CD / quality (push) Successful in 1m57s
CD / publish (push) Successful in 3m31s
quality / changes (push) Successful in 9s
quality / go (push) Skipped
quality / bird2 (push) Skipped
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 23s
quality / web (push) Successful in 1m22s
CD / quality (push) Successful in 1m57s
CD / publish (push) Successful in 3m31s
ResourcePage и FrameDataGrid получили раскладку досок: заголовок Frame с поиском и фильтрами, линейные виды со счётчиками, подвал «Показано N из M». Страница модулей — KPI-полоса, кнопка создания в шапке, вкладка. Операции: действия применения и перезагрузки BIRD в шапке, баннер при ошибках задач. Сеть: баннер пиров на одобрение. Кнопки шапок — стандартной высоты. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
53420b49a1
commit
aba18e15b7
13 files changed
+185
-160
No files matched your search
@@ -17,8 +17,8 @@ import type { ModuleRow } from '@/types/api'
|
||||
|
||||
const MODULE_TABS = [
|
||||
{ id: 'all', label: 'Все' },
|
||||
{ id: 'enabled', label: 'Вкл' },
|
||||
{ id: 'disabled', label: 'Выкл' },
|
||||
{ id: 'enabled', label: 'Включённые' },
|
||||
{ id: 'disabled', label: 'Выключенные' },
|
||||
]
|
||||
|
||||
const TYPE_OPTIONS = [
|
||||
@@ -66,7 +66,7 @@ function tabFilter(item: ModuleRow, tabId: string): boolean {
|
||||
export function ModulesListGrid({
|
||||
items,
|
||||
isLoading = false,
|
||||
title = 'Все модули',
|
||||
title = 'Маршрутные модули',
|
||||
actions,
|
||||
}: {
|
||||
items: ModuleRow[]
|
||||
@@ -130,7 +130,7 @@ export function ModulesListGrid({
|
||||
return (
|
||||
<ResourcePage
|
||||
title={title}
|
||||
description="Маршрутные списки"
|
||||
description="Поиск по имени · фильтр по типу · клик открывает модуль"
|
||||
tabs={MODULE_TABS}
|
||||
tabFilter={tabFilter}
|
||||
filterFields={filterFields}
|
||||
|
||||
@@ -382,7 +382,7 @@ export function CodeBlock({ label, children }: { label: string; children: string
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-muted-foreground text-xs">{label}</span>
|
||||
<pre className="bg-muted/40 overflow-x-auto rounded-md border p-3 font-mono text-xs leading-relaxed">{children}</pre>
|
||||
<pre className="bg-muted/40 overflow-x-auto [scrollbar-width:none] rounded-md border p-3 font-mono text-xs leading-relaxed">{children}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -401,7 +401,7 @@ export function Segmented({
|
||||
}) {
|
||||
return (
|
||||
<Tabs value={String(value)} onValueChange={(v) => onChange(Number(v))} aria-label={label} className="max-w-full">
|
||||
<TabsList className="max-w-full justify-start overflow-x-auto">
|
||||
<TabsList className="max-w-full justify-start overflow-x-auto [scrollbar-width:none]">
|
||||
{options.map((o, i) => (
|
||||
<TabsTrigger key={o} value={String(i)}>
|
||||
{o}
|
||||
@@ -424,7 +424,7 @@ export function PendingChip({ mark }: { mark: 'new' | 'dirty' | 'deleted' }) {
|
||||
/** Вкладки страницы как на досках: линия-подчёркивание, число — мелким индексом. */
|
||||
export function PageTabsList({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<TabsList variant="line" className="h-10! w-full justify-start gap-0.5 overflow-x-auto">
|
||||
<TabsList variant="line" className="h-10! w-full justify-start gap-0.5 overflow-x-auto [scrollbar-width:none]">
|
||||
{children}
|
||||
</TabsList>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { Columns3Icon } from 'lucide-react'
|
||||
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { Separator } from '@evobgp/ui/components/separator'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
import {
|
||||
DataGrid,
|
||||
@@ -40,6 +41,7 @@ import { DataGridTableVirtual } from '@/components/reui/data-grid/data-grid-tabl
|
||||
import {
|
||||
Frame,
|
||||
FrameDescription,
|
||||
FrameFooter,
|
||||
FrameHeader,
|
||||
FramePanel,
|
||||
FrameTitle,
|
||||
@@ -315,7 +317,13 @@ function FrameDataGridBody<TData extends object>({
|
||||
)}
|
||||
{showPagination ? (
|
||||
<>
|
||||
<DataGridPagination className="px-5 py-3" sizes={[10, 25, 50]} />
|
||||
<Separator />
|
||||
<FrameFooter className="flex-wrap justify-between gap-2">
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
Показано {table.getRowModel().rows.length} из {data.length}
|
||||
</span>
|
||||
<DataGridPagination sizes={[10, 25, 50]} />
|
||||
</FrameFooter>
|
||||
</>
|
||||
) : null}
|
||||
</DataGrid>
|
||||
|
||||
@@ -23,12 +23,12 @@ import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll
|
||||
import { DataGridTable } from '@/components/reui/data-grid/data-grid-table'
|
||||
import { DataGridTableRowExpand } from '@/components/reui/data-grid/data-grid-table'
|
||||
import { Filters } from '@/components/reui/filters/filters'
|
||||
import { SegmentedControl } from '@/components/portal/data-page'
|
||||
import { flattenFilterConditions } from '@/components/reui/filters/filters-query'
|
||||
import type { FilterField, FilterQuery } from '@/components/reui/filters/filters-types'
|
||||
import {
|
||||
Frame,
|
||||
FrameDescription,
|
||||
FrameFooter,
|
||||
FrameHeader,
|
||||
FramePanel,
|
||||
FrameTitle,
|
||||
@@ -40,7 +40,9 @@ import {
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from '@evobgp/ui/components/input-group'
|
||||
import { Separator } from '@evobgp/ui/components/separator'
|
||||
import { Skeleton } from '@evobgp/ui/components/skeleton'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
||||
import { DATA_GRID_I18N_RU } from '@/lib/reui-i18n-ru'
|
||||
import { FILTERS_LABELS_RU, FILTERS_OPERATOR_LABELS_RU } from '@/lib/filters-i18n'
|
||||
import {
|
||||
@@ -538,92 +540,75 @@ function ResourcePageFiltered<T extends object>({
|
||||
tableClassNames={kitDataGridTableClassNames}
|
||||
>
|
||||
<Frame dense variant="default" spacing="sm" className="w-full">
|
||||
{!hideHeader ? (
|
||||
<FrameHeader className="flex-row items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-col gap-px">
|
||||
<FrameHeader className="flex-row flex-wrap items-center gap-x-3 gap-y-2">
|
||||
{!hideHeader ? (
|
||||
<div className="flex min-w-0 flex-1 basis-52 flex-col gap-0.5">
|
||||
<FrameTitle className="text-balance">{title}</FrameTitle>
|
||||
{description ? (
|
||||
<FrameDescription className="flex flex-wrap items-center gap-1.5 text-xs text-pretty">
|
||||
<span>{description}</span>
|
||||
<span className="bg-input size-1 shrink-0 rounded-full" aria-hidden="true" />
|
||||
<span className="tabular-nums">{filteredData.length} записей</span>
|
||||
{selectedCount > 0 ? (
|
||||
<>
|
||||
<span className="bg-input size-1 shrink-0 rounded-full" aria-hidden="true" />
|
||||
<span>{selectedCount} выбрано</span>
|
||||
</>
|
||||
) : null}
|
||||
</FrameDescription>
|
||||
<FrameDescription className="text-xs text-pretty">{description}</FrameDescription>
|
||||
) : null}
|
||||
</div>
|
||||
{headerActions ? (
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
||||
{headerActions}
|
||||
</div>
|
||||
) : null}
|
||||
</FrameHeader>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="flex-1" />
|
||||
)}
|
||||
{headerActions ? <div className="flex shrink-0 flex-wrap items-center justify-end gap-2">{headerActions}</div> : null}
|
||||
{toolbarExtra}
|
||||
{showFiltersPopover ? (
|
||||
<Filters
|
||||
query={extraQuery}
|
||||
fields={extraFilterFields}
|
||||
onQueryChange={handleFiltersChange}
|
||||
variant="advanced"
|
||||
advancedMode="popover"
|
||||
size="default"
|
||||
labels={FILTERS_LABELS_RU}
|
||||
operatorLabels={FILTERS_OPERATOR_LABELS_RU}
|
||||
trigger={
|
||||
<Button type="button" variant="outline" aria-label="Фильтры">
|
||||
<FilterIcon className="size-4" aria-hidden="true" />
|
||||
Фильтры
|
||||
{extraActiveCount > 0 ? (
|
||||
<Badge size="xs" variant="secondary" radius="full">
|
||||
{extraActiveCount}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{showSearch && primaryTextField ? (
|
||||
<InputGroup className="w-[220px] max-w-full">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon aria-hidden="true" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
value={searchText}
|
||||
onChange={(event) => handleSearchChange(event.target.value)}
|
||||
placeholder="Поиск"
|
||||
aria-label={primaryTextField.label || 'Поиск'}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : null}
|
||||
{hasDirtyFilters ? (
|
||||
<Button type="button" variant="outline" onClick={handleClear}>
|
||||
<FilterXIcon className="size-4" aria-hidden="true" />
|
||||
Сбросить
|
||||
</Button>
|
||||
) : null}
|
||||
</FrameHeader>
|
||||
|
||||
<FramePanel className="p-0 shadow-none!">
|
||||
<div className="flex flex-wrap items-center gap-3 border-b px-5 py-3">
|
||||
{countedTabs.length > 0 ? (
|
||||
<SegmentedControl
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
label="Фильтр по статусу"
|
||||
options={countedTabs.map((t) => ({ value: t.id, label: t.label, count: t.count }))}
|
||||
/>
|
||||
) : null}
|
||||
{showFiltersPopover ? (
|
||||
<Filters
|
||||
query={extraQuery}
|
||||
fields={extraFilterFields}
|
||||
onQueryChange={handleFiltersChange}
|
||||
variant="advanced"
|
||||
advancedMode="popover"
|
||||
size="default"
|
||||
labels={FILTERS_LABELS_RU}
|
||||
operatorLabels={FILTERS_OPERATOR_LABELS_RU}
|
||||
trigger={
|
||||
<Button type="button" variant="outline" aria-label="Фильтры">
|
||||
<FilterIcon className="size-4" aria-hidden="true" />
|
||||
Фильтры
|
||||
{extraActiveCount > 0 ? (
|
||||
<Badge size="xs" variant="secondary" radius="full">
|
||||
{extraActiveCount}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{showSearch && primaryTextField ? (
|
||||
<InputGroup className="max-w-sm min-w-[220px]">
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="size-3.5" aria-hidden="true" />
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
value={searchText}
|
||||
onChange={(event) => handleSearchChange(event.target.value)}
|
||||
placeholder={primaryTextField.placeholder ?? 'Поиск…'}
|
||||
aria-label={primaryTextField.label || 'Поиск'}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : null}
|
||||
<span className="text-muted-foreground ml-auto text-sm tabular-nums">{filteredData.length} записей</span>
|
||||
{toolbarExtra}
|
||||
{selectedCount > 0 ? (
|
||||
<Badge size="sm" variant="secondary">
|
||||
{selectedCount} выбрано
|
||||
</Badge>
|
||||
) : null}
|
||||
{hasDirtyFilters ? (
|
||||
<Button type="button" variant="outline" onClick={handleClear}>
|
||||
<FilterXIcon className="size-4" aria-hidden="true" />
|
||||
Сбросить
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{countedTabs.length > 0 ? (
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||||
<TabsList variant="line" aria-label="Сохранённые виды" className="h-10! w-full justify-start gap-4 overflow-x-auto [scrollbar-width:none] px-4">
|
||||
{countedTabs.map((t) => (
|
||||
<TabsTrigger key={t.id} value={t.id} className="flex-none px-1 text-[13px]">
|
||||
{t.label} · {t.count}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
) : null}
|
||||
|
||||
{virtualization ? (
|
||||
<DataGridScrollArea orientation="vertical" style={{ height }}>
|
||||
@@ -633,7 +618,14 @@ function ResourcePageFiltered<T extends object>({
|
||||
<DataGridScrollArea>{tableNode}</DataGridScrollArea>
|
||||
)}
|
||||
|
||||
<DataGridPagination className="px-5 py-3" sizes={[5, 10, 20, 50]} />
|
||||
<Separator />
|
||||
|
||||
<FrameFooter className="flex-wrap justify-between gap-2">
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
{selectedCount > 0 ? `Выбрано ${selectedCount} · ` : ''}Показано {table.getRowModel().rows.length} из {filteredData.length}
|
||||
</span>
|
||||
<DataGridPagination sizes={[5, 10, 20, 50]} />
|
||||
</FrameFooter>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
</DataGrid>
|
||||
|
||||
@@ -46,7 +46,7 @@ export function SegmentedTabs({
|
||||
orientation="horizontal"
|
||||
className={cn('w-full min-w-0 gap-4', className)}
|
||||
>
|
||||
<TabsList variant="line" className={cn('h-10! w-full min-w-0 justify-start gap-0.5 overflow-x-auto', listClassName)}>
|
||||
<TabsList variant="line" className={cn('h-10! w-full min-w-0 justify-start gap-0.5 overflow-x-auto [scrollbar-width:none]', listClassName)}>
|
||||
{items.map((item) => (
|
||||
<TabsTrigger key={item.value} value={item.value} className="flex-none shrink-0 gap-1.5 px-4 text-sm">
|
||||
{item.icon}
|
||||
|
||||
Reference in new issue
Block a user