feat: enhance UI components with DataGridCard integration and improved error handling
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 50s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m21s
CI / changes (push) Successful in 9s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 50s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m21s
Refactored multiple components to utilize the new DataGridCard for better organization and presentation of data. Updated the FirewallPage and Monitoring components to enhance loading states and error handling using QueryState. Added success and error notifications for firewall rule creation, improving user feedback. This update streamlines the user experience and ensures a more consistent interface across the application.
This commit is contained in:
@@ -46,7 +46,11 @@ export function DataGridShell<TData extends object>({
|
|||||||
<DataGridContainer>
|
<DataGridContainer>
|
||||||
<DataGridTable />
|
<DataGridTable />
|
||||||
</DataGridContainer>
|
</DataGridContainer>
|
||||||
{showPagination ? <DataGridPagination {...DATA_GRID_PAGINATION_RU} /> : null}
|
{showPagination ? (
|
||||||
|
<div className="border-t px-4 py-3">
|
||||||
|
<DataGridPagination {...DATA_GRID_PAGINATION_RU} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function DataGridToolbar({
|
|||||||
className,
|
className,
|
||||||
}: DataGridToolbarProps) {
|
}: DataGridToolbarProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-wrap items-center gap-2 border-b px-3 py-3 ${className ?? ''}`}>
|
<div className={`flex flex-wrap items-center gap-2 border-b px-4 py-3 ${className ?? ''}`}>
|
||||||
<Field className="min-w-[200px] flex-1">
|
<Field className="min-w-[200px] flex-1">
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupAddon align="inline-start">
|
<InputGroupAddon align="inline-start">
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@evobgp/ui/components/button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@evobgp/ui/components/dialog'
|
||||||
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
|
|
||||||
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
|
import { CommunitySelect } from '@/components/modules/community-select'
|
||||||
|
import { SelectField } from '@/components/select-field'
|
||||||
|
import { useCreateFirewallRule } from '@/queries/firewall'
|
||||||
|
import type { BgpCommunity } from '@/types/api'
|
||||||
|
|
||||||
|
const FIREWALL_ACTION_ITEMS = [
|
||||||
|
{ value: 'block', label: 'block' },
|
||||||
|
{ value: 'accept', label: 'accept' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
interface FirewallRuleCreateDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
communities: BgpCommunity[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FirewallRuleCreateDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
communities,
|
||||||
|
}: FirewallRuleCreateDialogProps) {
|
||||||
|
const createMutation = useCreateFirewallRule()
|
||||||
|
const [action, setAction] = useState<'block' | 'accept'>('block')
|
||||||
|
const [communityId, setCommunityId] = useState<string | null>(null)
|
||||||
|
const [comment, setComment] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
setAction('block')
|
||||||
|
setCommunityId(null)
|
||||||
|
setComment('')
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
try {
|
||||||
|
await createMutation.mutateAsync({
|
||||||
|
scope: 'tenant',
|
||||||
|
action,
|
||||||
|
community_id: communityId,
|
||||||
|
comment: comment.trim(),
|
||||||
|
})
|
||||||
|
onOpenChange(false)
|
||||||
|
} catch {
|
||||||
|
// toast handled in mutation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Новое правило</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 py-2">
|
||||||
|
<SelectField
|
||||||
|
id="fw-rule-action"
|
||||||
|
label="Действие"
|
||||||
|
items={[...FIREWALL_ACTION_ITEMS]}
|
||||||
|
value={action}
|
||||||
|
placeholder="Выберите действие"
|
||||||
|
onValueChange={(v) => v && setAction(v as 'block' | 'accept')}
|
||||||
|
/>
|
||||||
|
<CommunitySelect
|
||||||
|
id="fw-rule-community"
|
||||||
|
label="Community"
|
||||||
|
value={communityId}
|
||||||
|
onValueChange={setCommunityId}
|
||||||
|
communities={communities}
|
||||||
|
nullable
|
||||||
|
placeholder="Все communities"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="fw-rule-comment">Комментарий</Label>
|
||||||
|
<Input
|
||||||
|
id="fw-rule-comment"
|
||||||
|
placeholder="Комментарий"
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<LoadingButton type="button" onClick={save} loading={createMutation.isPending}>
|
||||||
|
Добавить
|
||||||
|
</LoadingButton>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -83,8 +83,10 @@ export function useCreateFirewallRule() {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
toast.success('Правило добавлено')
|
||||||
void qc.invalidateQueries({ queryKey: firewallKeys.all })
|
void qc.invalidateQueries({ queryKey: firewallKeys.all })
|
||||||
},
|
},
|
||||||
|
onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось добавить правило'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Copy, Info, RefreshCw, Shield } from 'lucide-react'
|
import { Copy, Info, Plus, RefreshCw, Shield } from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
@@ -10,10 +10,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evob
|
|||||||
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 { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||||
|
import { DataGridCard } from '@/components/data-grid-shell'
|
||||||
import { FirewallClientsGrid } from '@/components/firewall/firewall-clients-grid'
|
import { FirewallClientsGrid } from '@/components/firewall/firewall-clients-grid'
|
||||||
|
import { FirewallRuleCreateDialog } from '@/components/firewall/firewall-rule-create-dialog'
|
||||||
import { FirewallRulesGrid } from '@/components/firewall/firewall-rules-grid'
|
import { FirewallRulesGrid } from '@/components/firewall/firewall-rules-grid'
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
import { CommunitySelect } from '@/components/modules/community-select'
|
|
||||||
import { QueryState } from '@/components/query-state'
|
import { QueryState } from '@/components/query-state'
|
||||||
import { TableSkeleton } from '@/components/skeletons'
|
import { TableSkeleton } from '@/components/skeletons'
|
||||||
import { directoriesCommunitiesQueryOptions } from '@/queries/directories'
|
import { directoriesCommunitiesQueryOptions } from '@/queries/directories'
|
||||||
@@ -22,7 +23,6 @@ import {
|
|||||||
firewallInstallContextQueryOptions,
|
firewallInstallContextQueryOptions,
|
||||||
firewallRulesQueryOptions,
|
firewallRulesQueryOptions,
|
||||||
useApproveFirewallClient,
|
useApproveFirewallClient,
|
||||||
useCreateFirewallRule,
|
|
||||||
useDeleteFirewallClient,
|
useDeleteFirewallClient,
|
||||||
useDeleteFirewallRule,
|
useDeleteFirewallRule,
|
||||||
} from '@/queries/firewall'
|
} from '@/queries/firewall'
|
||||||
@@ -48,7 +48,6 @@ function FirewallPage() {
|
|||||||
const rulesQ = useQuery(firewallRulesQueryOptions('tenant'))
|
const rulesQ = useQuery(firewallRulesQueryOptions('tenant'))
|
||||||
const approve = useApproveFirewallClient()
|
const approve = useApproveFirewallClient()
|
||||||
const deleteClient = useDeleteFirewallClient()
|
const deleteClient = useDeleteFirewallClient()
|
||||||
const createRule = useCreateFirewallRule()
|
|
||||||
const deleteRule = useDeleteFirewallRule()
|
const deleteRule = useDeleteFirewallRule()
|
||||||
|
|
||||||
const installCtx = installCtxQ.data
|
const installCtx = installCtxQ.data
|
||||||
@@ -58,6 +57,7 @@ function FirewallPage() {
|
|||||||
typeof window !== 'undefined' ? httpsOrigin(window.location.origin) : 'https://api.example.com',
|
typeof window !== 'undefined' ? httpsOrigin(window.location.origin) : 'https://api.example.com',
|
||||||
)
|
)
|
||||||
const [seed, setSeed] = useState('')
|
const [seed, setSeed] = useState('')
|
||||||
|
const [createRuleOpen, setCreateRuleOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (installCtx?.suggested_cp_url) {
|
if (installCtx?.suggested_cp_url) {
|
||||||
@@ -67,9 +67,6 @@ function FirewallPage() {
|
|||||||
setSeed(installCtx.bundle_seed)
|
setSeed(installCtx.bundle_seed)
|
||||||
}
|
}
|
||||||
}, [installCtx?.bundle_seed, installCtx?.suggested_cp_url])
|
}, [installCtx?.bundle_seed, installCtx?.suggested_cp_url])
|
||||||
const [ruleAction, setRuleAction] = useState<'block' | 'accept'>('block')
|
|
||||||
const [ruleCommunityId, setRuleCommunityId] = useState<string | null>(null)
|
|
||||||
const [ruleComment, setRuleComment] = useState('')
|
|
||||||
|
|
||||||
const communities = communitiesQ.data?.items ?? []
|
const communities = communitiesQ.data?.items ?? []
|
||||||
|
|
||||||
@@ -198,115 +195,91 @@ function FirewallPage() {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TabsContent value="clients" className="mt-0">
|
<TabsContent value="clients" className="mt-0">
|
||||||
<QueryState
|
<DataGridCard title="Клиенты" description="Активные Linux-серверы с синхронизацией blocklist">
|
||||||
data={clientsQ.data}
|
<QueryState
|
||||||
isLoading={clientsQ.isLoading}
|
data={clientsQ.data}
|
||||||
isError={clientsQ.isError}
|
isLoading={clientsQ.isLoading}
|
||||||
error={clientsQ.error}
|
isError={clientsQ.isError}
|
||||||
onRetry={() => void clientsQ.refetch()}
|
error={clientsQ.error}
|
||||||
skeleton={<TableSkeleton rows={5} cols={6} />}
|
onRetry={() => void clientsQ.refetch()}
|
||||||
>
|
skeleton={<TableSkeleton rows={5} cols={6} />}
|
||||||
{() => (
|
>
|
||||||
<FirewallClientsGrid
|
{() => (
|
||||||
clients={activeClients}
|
<FirewallClientsGrid
|
||||||
isLoading={clientsQ.isFetching && !clientsQ.isLoading}
|
clients={activeClients}
|
||||||
onApprove={(id) => approve.mutate(id)}
|
isLoading={clientsQ.isFetching && !clientsQ.isLoading}
|
||||||
onReject={(id) => deleteClient.mutate(id)}
|
onApprove={(id) => approve.mutate(id)}
|
||||||
approvePending={approve.isPending}
|
onReject={(id) => deleteClient.mutate(id)}
|
||||||
rejectPending={deleteClient.isPending}
|
approvePending={approve.isPending}
|
||||||
/>
|
rejectPending={deleteClient.isPending}
|
||||||
)}
|
/>
|
||||||
</QueryState>
|
)}
|
||||||
|
</QueryState>
|
||||||
|
</DataGridCard>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="rules" className="mt-0 space-y-4">
|
<TabsContent value="rules" className="mt-0">
|
||||||
<div className="flex flex-wrap items-end gap-3">
|
<DataGridCard
|
||||||
<div className="space-y-1">
|
title="Правила"
|
||||||
<Label>Действие</Label>
|
actions={
|
||||||
<select
|
<Button size="sm" type="button" onClick={() => setCreateRuleOpen(true)}>
|
||||||
className="border-input bg-background h-9 rounded-md border px-2 text-sm"
|
<Plus />
|
||||||
value={ruleAction}
|
Добавить правило
|
||||||
onChange={(e) => setRuleAction(e.target.value as 'block' | 'accept')}
|
</Button>
|
||||||
>
|
}
|
||||||
<option value="block">block</option>
|
|
||||||
<option value="accept">accept</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<CommunitySelect
|
|
||||||
id="fw-rule-community"
|
|
||||||
label="Community"
|
|
||||||
value={ruleCommunityId}
|
|
||||||
onValueChange={setRuleCommunityId}
|
|
||||||
communities={communities}
|
|
||||||
nullable
|
|
||||||
placeholder="Все communities"
|
|
||||||
/>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor="fw-rule-comment">Комментарий</Label>
|
|
||||||
<Input
|
|
||||||
id="fw-rule-comment"
|
|
||||||
className="max-w-xs"
|
|
||||||
placeholder="Комментарий"
|
|
||||||
value={ruleComment}
|
|
||||||
onChange={(e) => setRuleComment(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="mb-0.5"
|
|
||||||
onClick={() =>
|
|
||||||
createRule.mutate({
|
|
||||||
scope: 'tenant',
|
|
||||||
action: ruleAction,
|
|
||||||
community_id: ruleCommunityId,
|
|
||||||
comment: ruleComment,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Добавить правило
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<QueryState
|
|
||||||
data={rulesQ.data}
|
|
||||||
isLoading={rulesQ.isLoading}
|
|
||||||
isError={rulesQ.isError}
|
|
||||||
error={rulesQ.error}
|
|
||||||
onRetry={() => void rulesQ.refetch()}
|
|
||||||
skeleton={<TableSkeleton rows={5} cols={5} />}
|
|
||||||
>
|
>
|
||||||
{() => (
|
<QueryState
|
||||||
<FirewallRulesGrid
|
data={rulesQ.data}
|
||||||
rules={rules}
|
isLoading={rulesQ.isLoading}
|
||||||
communities={communities}
|
isError={rulesQ.isError}
|
||||||
isLoading={rulesQ.isFetching && !rulesQ.isLoading}
|
error={rulesQ.error}
|
||||||
onDelete={(id) => deleteRule.mutate(id)}
|
onRetry={() => void rulesQ.refetch()}
|
||||||
deletePending={deleteRule.isPending}
|
skeleton={<TableSkeleton rows={5} cols={5} />}
|
||||||
/>
|
>
|
||||||
)}
|
{() => (
|
||||||
</QueryState>
|
<FirewallRulesGrid
|
||||||
|
rules={rules}
|
||||||
|
communities={communities}
|
||||||
|
isLoading={rulesQ.isFetching && !rulesQ.isLoading}
|
||||||
|
onDelete={(id) => deleteRule.mutate(id)}
|
||||||
|
deletePending={deleteRule.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</QueryState>
|
||||||
|
</DataGridCard>
|
||||||
|
<FirewallRuleCreateDialog
|
||||||
|
open={createRuleOpen}
|
||||||
|
onOpenChange={setCreateRuleOpen}
|
||||||
|
communities={communities}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="requests" className="mt-0">
|
<TabsContent value="requests" className="mt-0">
|
||||||
<QueryState
|
<DataGridCard
|
||||||
data={clientsQ.data}
|
title="Запросы"
|
||||||
isLoading={clientsQ.isLoading}
|
description="Pending enroll — одобрите или отклоните новые клиенты"
|
||||||
isError={clientsQ.isError}
|
|
||||||
error={clientsQ.error}
|
|
||||||
onRetry={() => void clientsQ.refetch()}
|
|
||||||
skeleton={<TableSkeleton rows={3} cols={6} />}
|
|
||||||
>
|
>
|
||||||
{() => (
|
<QueryState
|
||||||
<FirewallClientsGrid
|
data={clientsQ.data}
|
||||||
clients={pending}
|
isLoading={clientsQ.isLoading}
|
||||||
isLoading={clientsQ.isFetching && !clientsQ.isLoading}
|
isError={clientsQ.isError}
|
||||||
onApprove={(id) => approve.mutate(id)}
|
error={clientsQ.error}
|
||||||
onReject={(id) => deleteClient.mutate(id)}
|
onRetry={() => void clientsQ.refetch()}
|
||||||
approvePending={approve.isPending}
|
skeleton={<TableSkeleton rows={3} cols={6} />}
|
||||||
rejectPending={deleteClient.isPending}
|
>
|
||||||
emptyTitle="Нет pending-запросов"
|
{() => (
|
||||||
/>
|
<FirewallClientsGrid
|
||||||
)}
|
clients={pending}
|
||||||
</QueryState>
|
isLoading={clientsQ.isFetching && !clientsQ.isLoading}
|
||||||
|
onApprove={(id) => approve.mutate(id)}
|
||||||
|
onReject={(id) => deleteClient.mutate(id)}
|
||||||
|
approvePending={approve.isPending}
|
||||||
|
rejectPending={deleteClient.isPending}
|
||||||
|
emptyTitle="Нет pending-запросов"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</QueryState>
|
||||||
|
</DataGridCard>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</BadgeTabs>
|
</BadgeTabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 { Separator } from '@evobgp/ui/components/separator'
|
import { Separator } from '@evobgp/ui/components/separator'
|
||||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||||
|
import { DataGridCard } from '@/components/data-grid-shell'
|
||||||
import { StatusBadge } from '@/components/status-badge'
|
import { StatusBadge } from '@/components/status-badge'
|
||||||
import {
|
import {
|
||||||
DashboardOperationsFlowCard,
|
DashboardOperationsFlowCard,
|
||||||
@@ -129,24 +130,21 @@ function MonitoringComponent() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
<Card>
|
<DataGridCard
|
||||||
<CardHeader>
|
title="Доступность и готовность"
|
||||||
<CardTitle className="text-base">Доступность и готовность</CardTitle>
|
description="GET /v1/health · GET /v1/ready"
|
||||||
<CardDescription>GET /v1/health · GET /v1/ready</CardDescription>
|
>
|
||||||
</CardHeader>
|
<QueryState
|
||||||
<CardContent className="space-y-4">
|
data={readyQ.data}
|
||||||
<QueryState
|
isLoading={readyQ.isLoading}
|
||||||
data={readyQ.data}
|
isError={readyQ.isError}
|
||||||
isLoading={readyQ.isLoading}
|
error={readyQ.error}
|
||||||
isError={readyQ.isError}
|
skeleton={<div className="h-40" />}
|
||||||
error={readyQ.error}
|
onRetry={() => readyQ.refetch()}
|
||||||
skeleton={<div className="h-40" />}
|
>
|
||||||
onRetry={() => readyQ.refetch()}
|
{(ready) => <MonitoringReadyGrid health={healthQ.data} ready={ready} />}
|
||||||
>
|
</QueryState>
|
||||||
{(ready) => <MonitoringReadyGrid health={healthQ.data} ready={ready} />}
|
</DataGridCard>
|
||||||
</QueryState>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evob
|
|||||||
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 { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||||
|
import { DataGridCard } from '@/components/data-grid-shell'
|
||||||
import { SelectField } from '@/components/select-field'
|
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'
|
||||||
@@ -318,33 +319,28 @@ function TenantSettingsComponent() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="additional" className="mt-0">
|
<TabsContent value="additional" className="mt-0">
|
||||||
<Card>
|
<DataGridCard
|
||||||
<CardHeader>
|
title="Дополнительные параметры"
|
||||||
<CardTitle>Дополнительные параметры</CardTitle>
|
description="Параметры вне стандартных групп (readonly — изменяются только через API)"
|
||||||
<CardDescription>
|
>
|
||||||
Параметры вне стандартных групп (readonly — изменяются только через API)
|
<QueryState
|
||||||
</CardDescription>
|
data={partitioned?.additional ?? []}
|
||||||
</CardHeader>
|
isLoading={settingsQ.isLoading}
|
||||||
<CardContent className="p-0">
|
isError={settingsQ.isError}
|
||||||
<QueryState
|
error={settingsQ.error}
|
||||||
data={partitioned?.additional ?? []}
|
empty={(partitioned?.additional ?? []).length === 0}
|
||||||
isLoading={settingsQ.isLoading}
|
emptyTitle="Нет дополнительных параметров"
|
||||||
isError={settingsQ.isError}
|
skeleton={<div className="h-32" />}
|
||||||
error={settingsQ.error}
|
onRetry={() => settingsQ.refetch()}
|
||||||
empty={(partitioned?.additional ?? []).length === 0}
|
>
|
||||||
emptyTitle="Нет дополнительных параметров"
|
{(items) => (
|
||||||
skeleton={<div className="h-32" />}
|
<SettingsKvGrid
|
||||||
onRetry={() => settingsQ.refetch()}
|
items={items}
|
||||||
>
|
isLoading={settingsQ.isFetching && !settingsQ.isLoading}
|
||||||
{(items) => (
|
/>
|
||||||
<SettingsKvGrid
|
)}
|
||||||
items={items}
|
</QueryState>
|
||||||
isLoading={settingsQ.isFetching && !settingsQ.isLoading}
|
</DataGridCard>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</QueryState>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</BadgeTabs>
|
</BadgeTabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user