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:
@@ -11,15 +11,9 @@ import {
|
||||
} from '@evobgp/ui/components/dialog'
|
||||
import { Input } from '@evobgp/ui/components/input'
|
||||
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 { SelectField } from '@/components/select-field'
|
||||
import { API_KEY_ROLE_ITEMS } from '@/lib/access/api-key-labels'
|
||||
import { useCreateApiKeyMutation } from '@/queries/api-keys'
|
||||
import type { ApiKeyCreate, ApiKeyCreated, ApiKeyRole } from '@/types/api'
|
||||
@@ -89,25 +83,14 @@ export function ApiKeyCreateDialog({ open, onOpenChange, onCreated }: ApiKeyCrea
|
||||
placeholder="CI / оператор UI"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="key-role">Роль</Label>
|
||||
<Select
|
||||
items={[...API_KEY_ROLE_ITEMS]}
|
||||
value={role}
|
||||
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>
|
||||
<SelectField
|
||||
id="key-role"
|
||||
label="Роль"
|
||||
items={[...API_KEY_ROLE_ITEMS]}
|
||||
value={role}
|
||||
placeholder="Выберите роль"
|
||||
onValueChange={(v) => v && setRole(v as ApiKeyRole)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="key-expires">Истекает (опционально)</Label>
|
||||
<Input
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Link } from '@tanstack/react-router'
|
||||
import { Gauge, Network, Play, Plus, Share2, Tags } from 'lucide-react'
|
||||
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
import { FrameFooter } from '@/components/reui/frame'
|
||||
import { CardFooter } from '@evobgp/ui/components/card'
|
||||
|
||||
export function DashboardQuickActions() {
|
||||
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" />}>
|
||||
<Plus className="size-4" />
|
||||
Создать модуль
|
||||
@@ -36,6 +36,6 @@ export function DashboardQuickActions() {
|
||||
<Gauge className="size-4" />
|
||||
Мониторинг
|
||||
</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 { Label } from '@evobgp/ui/components/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@evobgp/ui/components/select'
|
||||
import { Field, FieldLabel } from '@evobgp/ui/components/field'
|
||||
|
||||
import { SelectMenu } from '@/components/select-field'
|
||||
import {
|
||||
NONE_OPTION,
|
||||
communityOptionLabel,
|
||||
@@ -49,28 +43,27 @@ export function CommunitySelect({
|
||||
|
||||
const selectValue = nullable ? nullableSelectValue(value) : (value ?? '')
|
||||
|
||||
const select = (
|
||||
<SelectMenu
|
||||
id={id}
|
||||
items={items}
|
||||
value={selectValue}
|
||||
placeholder={placeholder}
|
||||
onValueChange={(v) => {
|
||||
if (!v) return
|
||||
onValueChange(nullable ? fromNullableSelect(v) : v)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!label) {
|
||||
return select
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{label ? <Label htmlFor={id}>{label}</Label> : null}
|
||||
<Select
|
||||
items={items}
|
||||
value={selectValue}
|
||||
onValueChange={(v) => {
|
||||
if (!v) return
|
||||
onValueChange(nullable ? fromNullableSelect(v) : v)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id={id} className="w-full">
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={id}>{label}</FieldLabel>
|
||||
{select}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,13 +10,8 @@ import {
|
||||
} from '@evobgp/ui/components/dialog'
|
||||
import { Input } from '@evobgp/ui/components/input'
|
||||
import { Label } from '@evobgp/ui/components/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@evobgp/ui/components/select'
|
||||
|
||||
import { SelectField } from '@/components/select-field'
|
||||
import { Button } from '@evobgp/ui/components/button'
|
||||
|
||||
import { LoadingButton } from '@/components/loading-button'
|
||||
@@ -174,25 +169,13 @@ export function ModuleCdnSourceDialog({
|
||||
onChange={(e) => setForm((s) => ({ ...s, url: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="cdn-kind">Тип источника</Label>
|
||||
<Select
|
||||
items={kindItems}
|
||||
value={form.source_kind}
|
||||
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>
|
||||
<SelectField
|
||||
id="cdn-kind"
|
||||
label="Тип источника"
|
||||
items={kindItems}
|
||||
value={form.source_kind}
|
||||
onValueChange={(v) => v && setForm((s) => ({ ...s, source_kind: v }))}
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="cdn-prefix-path">JSON path (prefix_path)</Label>
|
||||
<Input
|
||||
|
||||
@@ -4,12 +4,8 @@ import { useDataGrid } from "@/components/reui/data-grid/data-grid"
|
||||
import { cn } from "@evobgp/ui/lib/utils"
|
||||
import { Button } from "@evobgp/ui/components/button"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@evobgp/ui/components/select"
|
||||
SelectMenu,
|
||||
} from "@/components/select-field"
|
||||
import { Skeleton } from "@evobgp/ui/components/skeleton"
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
@@ -152,28 +148,23 @@ function DataGridPagination(props: DataGridPaginationProps): React.JSX.Element {
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{mergedProps.rowsPerPageLabel}
|
||||
</div>
|
||||
<Select
|
||||
items={mergedProps?.sizes?.map((size: number) => ({
|
||||
value: `${size}`,
|
||||
label: `${size}`,
|
||||
}))}
|
||||
<SelectMenu
|
||||
items={
|
||||
mergedProps?.sizes?.map((size: number) => ({
|
||||
value: `${size}`,
|
||||
label: `${size}`,
|
||||
})) ?? []
|
||||
}
|
||||
value={`${pageSize}`}
|
||||
triggerClassName="w-14"
|
||||
size="sm"
|
||||
side="top"
|
||||
contentClassName="min-w-18"
|
||||
onValueChange={(value) => {
|
||||
const newPageSize = Number(value)
|
||||
table.setPageSize(newPageSize)
|
||||
if (!value) return
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user