feat: refactor components to utilize SelectField for improved UI consistency

Updated various components to replace traditional select implementations with the new SelectField component. This change enhances the user interface by providing a more consistent layout and improved accessibility. Additionally, refactored the dashboard and operations pages to utilize DataGridCard for better organization of content, streamlining the overall user experience.
This commit is contained in:
Denozordec
2026-07-09 11:53:43 +07:00
parent b321aa5321
commit 452f6b2db0
15 changed files with 279 additions and 276 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"pid": 39884, "pid": 43636,
"version": "0.9.9", "version": "0.9.9",
"socketPath": "\\\\.\\pipe\\codegraph-97b92efdcc5351da", "socketPath": "\\\\.\\pipe\\codegraph-97b92efdcc5351da",
"startedAt": 1783489774882 "startedAt": 1783570299043
} }
@@ -11,15 +11,9 @@ import {
} from '@evobgp/ui/components/dialog' } from '@evobgp/ui/components/dialog'
import { Input } from '@evobgp/ui/components/input' import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label' import { Label } from '@evobgp/ui/components/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@evobgp/ui/components/select'
import { LoadingButton } from '@/components/loading-button' import { LoadingButton } from '@/components/loading-button'
import { SelectField } from '@/components/select-field'
import { API_KEY_ROLE_ITEMS } from '@/lib/access/api-key-labels' import { API_KEY_ROLE_ITEMS } from '@/lib/access/api-key-labels'
import { useCreateApiKeyMutation } from '@/queries/api-keys' import { useCreateApiKeyMutation } from '@/queries/api-keys'
import type { ApiKeyCreate, ApiKeyCreated, ApiKeyRole } from '@/types/api' import type { ApiKeyCreate, ApiKeyCreated, ApiKeyRole } from '@/types/api'
@@ -89,25 +83,14 @@ export function ApiKeyCreateDialog({ open, onOpenChange, onCreated }: ApiKeyCrea
placeholder="CI / оператор UI" placeholder="CI / оператор UI"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <SelectField
<Label htmlFor="key-role">Роль</Label> id="key-role"
<Select label="Роль"
items={[...API_KEY_ROLE_ITEMS]} items={[...API_KEY_ROLE_ITEMS]}
value={role} value={role}
placeholder="Выберите роль"
onValueChange={(v) => v && setRole(v as ApiKeyRole)} onValueChange={(v) => v && setRole(v as ApiKeyRole)}
> />
<SelectTrigger id="key-role" className="w-full">
<SelectValue placeholder="Выберите роль" />
</SelectTrigger>
<SelectContent>
{API_KEY_ROLE_ITEMS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label htmlFor="key-expires">Истекает (опционально)</Label> <Label htmlFor="key-expires">Истекает (опционально)</Label>
<Input <Input
@@ -2,11 +2,11 @@ import { Link } from '@tanstack/react-router'
import { Gauge, Network, Play, Plus, Share2, Tags } from 'lucide-react' import { Gauge, Network, Play, Plus, Share2, Tags } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button' import { Button } from '@evobgp/ui/components/button'
import { FrameFooter } from '@/components/reui/frame' import { CardFooter } from '@evobgp/ui/components/card'
export function DashboardQuickActions() { export function DashboardQuickActions() {
return ( return (
<FrameFooter className="flex flex-wrap gap-2"> <CardFooter className="flex flex-wrap gap-2 border-t-0 bg-transparent">
<Button variant="outline" size="sm" type="button" render={<Link to="/modules" />}> <Button variant="outline" size="sm" type="button" render={<Link to="/modules" />}>
<Plus className="size-4" /> <Plus className="size-4" />
Создать модуль Создать модуль
@@ -36,6 +36,6 @@ export function DashboardQuickActions() {
<Gauge className="size-4" /> <Gauge className="size-4" />
Мониторинг Мониторинг
</Button> </Button>
</FrameFooter> </CardFooter>
) )
} }
@@ -0,0 +1,19 @@
import { Field } from '@evobgp/ui/components/field'
import { SelectMenu } from '@/components/select-field'
const items = [
{ label: 'Select an item', value: 'placeholder' as const },
...Array.from({ length: 100 }).map((_, i) => ({
label: `Item ${i}`,
value: `item-${i}` as const,
})),
]
export function Pattern() {
return (
<Field className="max-w-xs">
<SelectMenu items={items} placeholder="Select an item" />
</Field>
)
}
@@ -1,14 +1,8 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { Label } from '@evobgp/ui/components/label' import { Field, FieldLabel } from '@evobgp/ui/components/field'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@evobgp/ui/components/select'
import { SelectMenu } from '@/components/select-field'
import { import {
NONE_OPTION, NONE_OPTION,
communityOptionLabel, communityOptionLabel,
@@ -49,28 +43,27 @@ export function CommunitySelect({
const selectValue = nullable ? nullableSelectValue(value) : (value ?? '') const selectValue = nullable ? nullableSelectValue(value) : (value ?? '')
return ( const select = (
<div className="flex flex-col gap-1.5"> <SelectMenu
{label ? <Label htmlFor={id}>{label}</Label> : null} id={id}
<Select
items={items} items={items}
value={selectValue} value={selectValue}
placeholder={placeholder}
onValueChange={(v) => { onValueChange={(v) => {
if (!v) return if (!v) return
onValueChange(nullable ? fromNullableSelect(v) : v) onValueChange(nullable ? fromNullableSelect(v) : v)
}} }}
> />
<SelectTrigger id={id} className="w-full"> )
<SelectValue placeholder={placeholder} />
</SelectTrigger> if (!label) {
<SelectContent> return select
{items.map((item) => ( }
<SelectItem key={item.value} value={item.value}>
{item.label} return (
</SelectItem> <Field>
))} <FieldLabel htmlFor={id}>{label}</FieldLabel>
</SelectContent> {select}
</Select> </Field>
</div>
) )
} }
@@ -10,13 +10,8 @@ import {
} from '@evobgp/ui/components/dialog' } from '@evobgp/ui/components/dialog'
import { Input } from '@evobgp/ui/components/input' import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label' import { Label } from '@evobgp/ui/components/label'
import {
Select, import { SelectField } from '@/components/select-field'
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@evobgp/ui/components/select'
import { Button } from '@evobgp/ui/components/button' import { Button } from '@evobgp/ui/components/button'
import { LoadingButton } from '@/components/loading-button' import { LoadingButton } from '@/components/loading-button'
@@ -174,25 +169,13 @@ export function ModuleCdnSourceDialog({
onChange={(e) => setForm((s) => ({ ...s, url: e.target.value }))} onChange={(e) => setForm((s) => ({ ...s, url: e.target.value }))}
/> />
</div> </div>
<div className="flex flex-col gap-1.5"> <SelectField
<Label htmlFor="cdn-kind">Тип источника</Label> id="cdn-kind"
<Select label="Тип источника"
items={kindItems} items={kindItems}
value={form.source_kind} value={form.source_kind}
onValueChange={(v) => v && setForm((s) => ({ ...s, source_kind: v }))} onValueChange={(v) => v && setForm((s) => ({ ...s, source_kind: v }))}
> />
<SelectTrigger id="cdn-kind" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{kindItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label htmlFor="cdn-prefix-path">JSON path (prefix_path)</Label> <Label htmlFor="cdn-prefix-path">JSON path (prefix_path)</Label>
<Input <Input
@@ -4,12 +4,8 @@ import { useDataGrid } from "@/components/reui/data-grid/data-grid"
import { cn } from "@evobgp/ui/lib/utils" import { cn } from "@evobgp/ui/lib/utils"
import { Button } from "@evobgp/ui/components/button" import { Button } from "@evobgp/ui/components/button"
import { import {
Select, SelectMenu,
SelectContent, } from "@/components/select-field"
SelectItem,
SelectTrigger,
SelectValue,
} from "@evobgp/ui/components/select"
import { Skeleton } from "@evobgp/ui/components/skeleton" import { Skeleton } from "@evobgp/ui/components/skeleton"
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
@@ -152,28 +148,23 @@ function DataGridPagination(props: DataGridPaginationProps): React.JSX.Element {
<div className="text-muted-foreground text-sm"> <div className="text-muted-foreground text-sm">
{mergedProps.rowsPerPageLabel} {mergedProps.rowsPerPageLabel}
</div> </div>
<Select <SelectMenu
items={mergedProps?.sizes?.map((size: number) => ({ items={
mergedProps?.sizes?.map((size: number) => ({
value: `${size}`, value: `${size}`,
label: `${size}`, label: `${size}`,
}))} })) ?? []
}
value={`${pageSize}`} value={`${pageSize}`}
triggerClassName="w-14"
size="sm"
side="top"
contentClassName="min-w-18"
onValueChange={(value) => { onValueChange={(value) => {
const newPageSize = Number(value) if (!value) return
table.setPageSize(newPageSize) table.setPageSize(Number(value))
}} }}
> />
<SelectTrigger className="w-14" size="sm">
<SelectValue />
</SelectTrigger>
<SelectContent side="top" className="min-w-18">
{mergedProps?.sizes?.map((size: number) => (
<SelectItem key={size} value={`${size}`}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
</> </>
)} )}
</div> </div>
+100
View File
@@ -0,0 +1,100 @@
import type { ComponentProps, ReactNode } from 'react'
import { Field, FieldDescription, FieldLabel } from '@evobgp/ui/components/field'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@evobgp/ui/components/select'
import { cn } from '@evobgp/ui/lib/utils'
export type SelectMenuItem<T extends string = string> = {
value: T
label: ReactNode
disabled?: boolean
}
type SelectRootProps = ComponentProps<typeof Select>
export interface SelectMenuProps<T extends string = string>
extends Omit<SelectRootProps, 'children' | 'onValueChange' | 'value' | 'defaultValue'> {
items: ReadonlyArray<SelectMenuItem<T>>
value?: T | null
defaultValue?: T | null
onValueChange?: (value: T | null) => void
placeholder?: string
id?: string
triggerClassName?: string
contentClassName?: string
size?: 'sm' | 'default'
side?: ComponentProps<typeof SelectContent>['side']
}
/** ReUI c-select-4: `items` на Root, опции в `SelectGroup`. */
export function SelectMenu<T extends string = string>({
items,
placeholder,
id,
triggerClassName,
contentClassName,
size = 'default',
side,
value,
defaultValue,
onValueChange,
...selectProps
}: SelectMenuProps<T>) {
return (
<Select
items={items}
value={value}
defaultValue={defaultValue}
onValueChange={(next) => onValueChange?.(next as T | null)}
{...selectProps}
>
<SelectTrigger id={id} className={cn('w-full', triggerClassName)} size={size}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent side={side} className={contentClassName}>
<SelectGroup>
{items.map((item) => (
<SelectItem key={item.value} value={item.value} disabled={item.disabled}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)
}
export interface SelectFieldProps<T extends string = string> extends SelectMenuProps<T> {
label?: ReactNode
description?: ReactNode
fieldClassName?: string
}
export function SelectField<T extends string = string>({
label,
description,
fieldClassName,
id,
...menuProps
}: SelectFieldProps<T>) {
return (
<Field className={fieldClassName}>
{label ? <FieldLabel htmlFor={id}>{label}</FieldLabel> : null}
<SelectMenu id={id} {...menuProps} />
{description ? (
typeof description === 'string' ? (
<FieldDescription className="font-mono text-xs">{description}</FieldDescription>
) : (
description
)
) : null}
</Field>
)
}
+34 -39
View File
@@ -15,20 +15,21 @@ import { useState } from 'react'
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert' import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
import { Button } from '@evobgp/ui/components/button' import { Button } from '@evobgp/ui/components/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@evobgp/ui/components/card'
import { Skeleton } from '@evobgp/ui/components/skeleton' import { Skeleton } from '@evobgp/ui/components/skeleton'
import { DataGridCard } from '@/components/data-grid-shell'
import { DashboardNetworkPanel } from '@/components/dashboard/dashboard-network-panel' import { DashboardNetworkPanel } from '@/components/dashboard/dashboard-network-panel'
import { DashboardQuickActions } from '@/components/dashboard/dashboard-quick-actions' import { DashboardQuickActions } from '@/components/dashboard/dashboard-quick-actions'
import { DashboardRecentJobsGrid } from '@/components/dashboard/dashboard-recent-jobs-grid' import { DashboardRecentJobsGrid } from '@/components/dashboard/dashboard-recent-jobs-grid'
import { DashboardRecentRevisionsGrid } from '@/components/dashboard/dashboard-recent-revisions-grid' import { DashboardRecentRevisionsGrid } from '@/components/dashboard/dashboard-recent-revisions-grid'
import { PageHeader } from '@/components/page-header' import { PageHeader } from '@/components/page-header'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { SectionCards, type SectionCardItem } from '@/components/section-cards' import { SectionCards, type SectionCardItem } from '@/components/section-cards'
import { SectionCardsSkeleton } from '@/components/skeletons' import { SectionCardsSkeleton } from '@/components/skeletons'
@@ -172,58 +173,52 @@ function DashboardComponent() {
{initialLoading ? <SectionCardsSkeleton count={5} /> : <SectionCards items={items} className="gap-3" />} {initialLoading ? <SectionCardsSkeleton count={5} /> : <SectionCards items={items} className="gap-3" />}
<div className="grid gap-4 lg:grid-cols-3"> <div className="grid gap-4 lg:grid-cols-3">
<Frame spacing="sm" className="h-full"> <DataGridCard
<FramePanel className="flex h-full flex-col p-0"> title="Недавние задачи"
<FrameHeader className="border-b"> description="Последние фоновые операции"
<FrameTitle>Недавние задачи</FrameTitle> className="h-full"
<FrameDescription>Последние фоновые операции</FrameDescription> >
</FrameHeader>
{activityLoading ? ( {activityLoading ? (
<Skeleton className="m-3 h-24 w-auto" /> <Skeleton className="m-3 h-24 w-auto" />
) : ( ) : (
<DashboardRecentJobsGrid jobs={jobs} nameById={nameById} isLoading={refreshing} /> <DashboardRecentJobsGrid jobs={jobs} nameById={nameById} isLoading={refreshing} />
)} )}
</FramePanel> </DataGridCard>
</Frame>
<Frame spacing="sm" className="h-full"> <DataGridCard
<FramePanel className="flex h-full flex-col p-0"> title="Последние ревизии"
<FrameHeader className="border-b"> description="История конфигураций"
<FrameTitle>Последние ревизии</FrameTitle> className="h-full"
<FrameDescription>История конфигураций</FrameDescription> >
</FrameHeader>
{activityLoading ? ( {activityLoading ? (
<Skeleton className="m-3 h-24 w-auto" /> <Skeleton className="m-3 h-24 w-auto" />
) : ( ) : (
<DashboardRecentRevisionsGrid revisions={revisions} isLoading={refreshing} /> <DashboardRecentRevisionsGrid revisions={revisions} isLoading={refreshing} />
)} )}
</FramePanel> </DataGridCard>
</Frame>
<Frame spacing="sm" className="h-full"> <Card className="h-full gap-0">
<FramePanel className="flex h-full flex-col p-0"> <CardHeader className="border-b py-3">
<FrameHeader className="border-b"> <CardTitle className="text-base">Состояние сети</CardTitle>
<FrameTitle>Состояние сети</FrameTitle> <CardDescription>BGP-сессии и спикеры</CardDescription>
<FrameDescription>BGP-сессии и спикеры</FrameDescription> </CardHeader>
</FrameHeader> <CardContent className="p-0">
{activityLoading ? ( {activityLoading ? (
<Skeleton className="m-3 h-24 w-auto" /> <Skeleton className="m-3 h-24 w-auto" />
) : ( ) : (
<DashboardNetworkPanel peers={peers} speakers={speakers} /> <DashboardNetworkPanel peers={peers} speakers={speakers} />
)} )}
</FramePanel> </CardContent>
</Frame> </Card>
</div> </div>
<Frame spacing="sm"> <Card className="gap-0">
<FramePanel className="p-0"> <CardHeader className="border-b py-3">
<FrameHeader className="border-b"> <CardTitle className="text-base">Быстрые действия</CardTitle>
<FrameTitle>Быстрые действия</FrameTitle> <CardDescription>Частые переходы к настройке и деплою</CardDescription>
<FrameDescription>Частые переходы к настройке и деплою</FrameDescription> </CardHeader>
</FrameHeader>
<DashboardQuickActions /> <DashboardQuickActions />
</FramePanel> </Card>
</Frame>
</div> </div>
) )
} }
+13 -31
View File
@@ -7,16 +7,10 @@ import { useState, useMemo } from 'react'
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert' import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
import { Button } from '@evobgp/ui/components/button' import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card' import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@evobgp/ui/components/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
import { DataGridCard } from '@/components/data-grid-shell' import { DataGridCard } from '@/components/data-grid-shell'
import { SelectMenu } from '@/components/select-field'
import { OperationsJobsGrid } from '@/components/operations/operations-jobs-grid' import { OperationsJobsGrid } from '@/components/operations/operations-jobs-grid'
import { OperationsRevisionsGrid } from '@/components/operations/operations-revisions-grid' import { OperationsRevisionsGrid } from '@/components/operations/operations-revisions-grid'
import { PageHeader } from '@/components/page-header' import { PageHeader } from '@/components/page-header'
@@ -245,33 +239,21 @@ function DiffTab({ revisions }: { revisions: import('@/types/api').RevisionRow[]
<div className="flex flex-wrap items-end gap-3"> <div className="flex flex-wrap items-end gap-3">
<div className="flex w-full max-w-xs flex-col gap-1"> <div className="flex w-full max-w-xs flex-col gap-1">
<span className="text-xs text-muted-foreground">Ревизия A</span> <span className="text-xs text-muted-foreground">Ревизия A</span>
<Select items={revisionItems} value={a} onValueChange={(v) => v && setA(v)}> <SelectMenu
<SelectTrigger> items={revisionItems}
<SelectValue placeholder="Выберите" /> value={a}
</SelectTrigger> placeholder="Выберите"
<SelectContent> onValueChange={(v) => v && setA(v)}
{revisions.map((r) => ( />
<SelectItem key={r.id} value={r.id}>
{r.id.slice(0, 12)}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<div className="flex w-full max-w-xs flex-col gap-1"> <div className="flex w-full max-w-xs flex-col gap-1">
<span className="text-xs text-muted-foreground">Ревизия B</span> <span className="text-xs text-muted-foreground">Ревизия B</span>
<Select items={revisionItems} value={b} onValueChange={(v) => v && setB(v)}> <SelectMenu
<SelectTrigger> items={revisionItems}
<SelectValue placeholder="Выберите" /> value={b}
</SelectTrigger> placeholder="Выберите"
<SelectContent> onValueChange={(v) => v && setB(v)}
{revisions.map((r) => ( />
<SelectItem key={r.id} value={r.id}>
{r.id.slice(0, 12)}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<Button onClick={() => diffQ.refetch()} disabled={!a || !b || diffQ.isFetching}> <Button onClick={() => diffQ.refetch()} disabled={!a || !b || diffQ.isFetching}>
Сравнить Сравнить
+7 -19
View File
@@ -6,16 +6,10 @@ import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { Input } from '@evobgp/ui/components/input' import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label' import { Label } from '@evobgp/ui/components/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@evobgp/ui/components/select'
import { PageHeader } from '@/components/page-header' import { PageHeader } from '@/components/page-header'
import { LoadingButton } from '@/components/loading-button' import { LoadingButton } from '@/components/loading-button'
import { SelectField } from '@/components/select-field'
import { DEV_API_TOKEN, normalizeApiToken, setToken, TOKEN_STORAGE_KEY } from '@/lib/api-client' import { DEV_API_TOKEN, normalizeApiToken, setToken, TOKEN_STORAGE_KEY } from '@/lib/api-client'
import { authKeys, authSessionQueryOptions } from '@/queries/auth' import { authKeys, authSessionQueryOptions } from '@/queries/auth'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -141,21 +135,15 @@ function SettingsComponent() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-2"> <CardContent className="flex flex-col gap-2">
<Label htmlFor="theme-select">Тема</Label> <SelectField
<Select id="theme-select"
label="Тема"
items={[...THEME_SELECT_ITEMS]} items={[...THEME_SELECT_ITEMS]}
value={theme ?? 'system'} value={theme ?? 'system'}
placeholder="Выберите тему"
triggerClassName="max-w-xs"
onValueChange={(v) => v && setTheme(v)} onValueChange={(v) => v && setTheme(v)}
> />
<SelectTrigger id="theme-select" className="w-full max-w-xs">
<SelectValue placeholder="Выберите тему" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Светлая</SelectItem>
<SelectItem value="dark">Тёмная</SelectItem>
<SelectItem value="system">Как в системе</SelectItem>
</SelectContent>
</Select>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
+12 -39
View File
@@ -8,15 +8,10 @@ import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { Input } from '@evobgp/ui/components/input' import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label' import { Label } from '@evobgp/ui/components/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@evobgp/ui/components/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
import { SelectField } from '@/components/select-field'
import { SettingsKvGrid } from '@/components/settings/settings-kv-grid' import { SettingsKvGrid } from '@/components/settings/settings-kv-grid'
import { PageHeader } from '@/components/page-header' import { PageHeader } from '@/components/page-header'
@@ -244,11 +239,12 @@ function TenantSettingsComponent() {
> >
{() => ( {() => (
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="flex flex-col gap-1.5"> <SelectField
<Label>Авто-очистка включена</Label> label="Авто-очистка включена"
<Select
items={[...RUNTIME_LOGS_ENABLED_ITEMS]} items={[...RUNTIME_LOGS_ENABLED_ITEMS]}
value={runtimeLogsForm.runtime_logs_auto_enabled ?? 'false'} value={runtimeLogsForm.runtime_logs_auto_enabled ?? 'false'}
placeholder="Выберите"
description="runtime_logs_auto_enabled"
onValueChange={(v) => onValueChange={(v) =>
v && v &&
setRuntimeLogsForm((s) => ({ setRuntimeLogsForm((s) => ({
@@ -256,19 +252,7 @@ function TenantSettingsComponent() {
runtime_logs_auto_enabled: v, runtime_logs_auto_enabled: v,
})) }))
} }
> />
<SelectTrigger>
<SelectValue placeholder="Выберите" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">Вкл</SelectItem>
<SelectItem value="false">Выкл</SelectItem>
</SelectContent>
</Select>
<p className="font-mono text-xs text-muted-foreground">
runtime_logs_auto_enabled
</p>
</div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label htmlFor="runtime_logs_max_file_mb">Макс. размер файла (MB)</Label> <Label htmlFor="runtime_logs_max_file_mb">Макс. размер файла (MB)</Label>
<Input <Input
@@ -302,11 +286,12 @@ function TenantSettingsComponent() {
runtime_logs_auto_schedule runtime_logs_auto_schedule
</p> </p>
</div> </div>
<div className="flex flex-col gap-1.5"> <SelectField
<Label>Режим очистки</Label> label="Режим очистки"
<Select
items={[...RUNTIME_LOGS_MODE_ITEMS]} items={[...RUNTIME_LOGS_MODE_ITEMS]}
value={runtimeLogsForm.runtime_logs_auto_mode ?? ''} value={runtimeLogsForm.runtime_logs_auto_mode ?? ''}
placeholder="Выберите"
description="runtime_logs_auto_mode"
onValueChange={(v) => onValueChange={(v) =>
v && v &&
setRuntimeLogsForm((s) => ({ setRuntimeLogsForm((s) => ({
@@ -314,19 +299,7 @@ function TenantSettingsComponent() {
runtime_logs_auto_mode: v, runtime_logs_auto_mode: v,
})) }))
} }
> />
<SelectTrigger>
<SelectValue placeholder="Выберите" />
</SelectTrigger>
<SelectContent>
<SelectItem value="truncate">truncate обнулить</SelectItem>
<SelectItem value="delete">delete удалить файл</SelectItem>
</SelectContent>
</Select>
<p className="font-mono text-xs text-muted-foreground">
runtime_logs_auto_mode
</p>
</div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<LoadingButton onClick={saveRuntimeLogs} loading={patchMutation.isPending}> <LoadingButton onClick={saveRuntimeLogs} loading={patchMutation.isPending}>
<Save /> <Save />
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/confirm-dialog.tsx","./src/components/data-grid-shell.tsx","./src/components/empty-state.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"} {"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/confirm-dialog.tsx","./src/components/data-grid-shell.tsx","./src/components/empty-state.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-select-4.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"}
-6
View File
@@ -1,5 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select" import { Select as SelectPrimitive } from "@base-ui/react/select"
@@ -111,15 +109,11 @@ function SelectLabel({
function SelectItem({ function SelectItem({
className, className,
children, children,
label,
...props ...props
}: SelectPrimitive.Item.Props) { }: SelectPrimitive.Item.Props) {
const resolvedLabel = label ?? (typeof children === "string" ? children : undefined)
return ( return (
<SelectPrimitive.Item <SelectPrimitive.Item
data-slot="select-item" data-slot="select-item"
label={resolvedLabel}
className={cn( className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className className
+2
View File
@@ -1,3 +1,5 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator" import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@evobgp/ui/lib/utils" import { cn } from "@evobgp/ui/lib/utils"