Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d434eb0d94 |
@@ -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