Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d434eb0d94 |
@@ -46,7 +46,11 @@ export function DataGridShell<TData extends object>({
|
||||
<DataGridContainer>
|
||||
<DataGridTable />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function DataGridToolbar({
|
||||
className,
|
||||
}: DataGridToolbarProps) {
|
||||
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">
|
||||
<InputGroup>
|
||||
<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),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success('Правило добавлено')
|
||||
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 { 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 { toast } from 'sonner'
|
||||
|
||||
@@ -10,10 +10,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evob
|
||||
import { Input } from '@evobgp/ui/components/input'
|
||||
import { Label } from '@evobgp/ui/components/label'
|
||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
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 { PageHeader } from '@/components/page-header'
|
||||
import { CommunitySelect } from '@/components/modules/community-select'
|
||||
import { QueryState } from '@/components/query-state'
|
||||
import { TableSkeleton } from '@/components/skeletons'
|
||||
import { directoriesCommunitiesQueryOptions } from '@/queries/directories'
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
firewallInstallContextQueryOptions,
|
||||
firewallRulesQueryOptions,
|
||||
useApproveFirewallClient,
|
||||
useCreateFirewallRule,
|
||||
useDeleteFirewallClient,
|
||||
useDeleteFirewallRule,
|
||||
} from '@/queries/firewall'
|
||||
@@ -48,7 +48,6 @@ function FirewallPage() {
|
||||
const rulesQ = useQuery(firewallRulesQueryOptions('tenant'))
|
||||
const approve = useApproveFirewallClient()
|
||||
const deleteClient = useDeleteFirewallClient()
|
||||
const createRule = useCreateFirewallRule()
|
||||
const deleteRule = useDeleteFirewallRule()
|
||||
|
||||
const installCtx = installCtxQ.data
|
||||
@@ -58,6 +57,7 @@ function FirewallPage() {
|
||||
typeof window !== 'undefined' ? httpsOrigin(window.location.origin) : 'https://api.example.com',
|
||||
)
|
||||
const [seed, setSeed] = useState('')
|
||||
const [createRuleOpen, setCreateRuleOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (installCtx?.suggested_cp_url) {
|
||||
@@ -67,9 +67,6 @@ function FirewallPage() {
|
||||
setSeed(installCtx.bundle_seed)
|
||||
}
|
||||
}, [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 ?? []
|
||||
|
||||
@@ -198,115 +195,91 @@ function FirewallPage() {
|
||||
]}
|
||||
>
|
||||
<TabsContent value="clients" className="mt-0">
|
||||
<QueryState
|
||||
data={clientsQ.data}
|
||||
isLoading={clientsQ.isLoading}
|
||||
isError={clientsQ.isError}
|
||||
error={clientsQ.error}
|
||||
onRetry={() => void clientsQ.refetch()}
|
||||
skeleton={<TableSkeleton rows={5} cols={6} />}
|
||||
>
|
||||
{() => (
|
||||
<FirewallClientsGrid
|
||||
clients={activeClients}
|
||||
isLoading={clientsQ.isFetching && !clientsQ.isLoading}
|
||||
onApprove={(id) => approve.mutate(id)}
|
||||
onReject={(id) => deleteClient.mutate(id)}
|
||||
approvePending={approve.isPending}
|
||||
rejectPending={deleteClient.isPending}
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
<DataGridCard title="Клиенты" description="Активные Linux-серверы с синхронизацией blocklist">
|
||||
<QueryState
|
||||
data={clientsQ.data}
|
||||
isLoading={clientsQ.isLoading}
|
||||
isError={clientsQ.isError}
|
||||
error={clientsQ.error}
|
||||
onRetry={() => void clientsQ.refetch()}
|
||||
skeleton={<TableSkeleton rows={5} cols={6} />}
|
||||
>
|
||||
{() => (
|
||||
<FirewallClientsGrid
|
||||
clients={activeClients}
|
||||
isLoading={clientsQ.isFetching && !clientsQ.isLoading}
|
||||
onApprove={(id) => approve.mutate(id)}
|
||||
onReject={(id) => deleteClient.mutate(id)}
|
||||
approvePending={approve.isPending}
|
||||
rejectPending={deleteClient.isPending}
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="rules" className="mt-0 space-y-4">
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>Действие</Label>
|
||||
<select
|
||||
className="border-input bg-background h-9 rounded-md border px-2 text-sm"
|
||||
value={ruleAction}
|
||||
onChange={(e) => setRuleAction(e.target.value as 'block' | 'accept')}
|
||||
>
|
||||
<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} />}
|
||||
<TabsContent value="rules" className="mt-0">
|
||||
<DataGridCard
|
||||
title="Правила"
|
||||
actions={
|
||||
<Button size="sm" type="button" onClick={() => setCreateRuleOpen(true)}>
|
||||
<Plus />
|
||||
Добавить правило
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{() => (
|
||||
<FirewallRulesGrid
|
||||
rules={rules}
|
||||
communities={communities}
|
||||
isLoading={rulesQ.isFetching && !rulesQ.isLoading}
|
||||
onDelete={(id) => deleteRule.mutate(id)}
|
||||
deletePending={deleteRule.isPending}
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
<QueryState
|
||||
data={rulesQ.data}
|
||||
isLoading={rulesQ.isLoading}
|
||||
isError={rulesQ.isError}
|
||||
error={rulesQ.error}
|
||||
onRetry={() => void rulesQ.refetch()}
|
||||
skeleton={<TableSkeleton rows={5} cols={5} />}
|
||||
>
|
||||
{() => (
|
||||
<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 value="requests" className="mt-0">
|
||||
<QueryState
|
||||
data={clientsQ.data}
|
||||
isLoading={clientsQ.isLoading}
|
||||
isError={clientsQ.isError}
|
||||
error={clientsQ.error}
|
||||
onRetry={() => void clientsQ.refetch()}
|
||||
skeleton={<TableSkeleton rows={3} cols={6} />}
|
||||
<DataGridCard
|
||||
title="Запросы"
|
||||
description="Pending enroll — одобрите или отклоните новые клиенты"
|
||||
>
|
||||
{() => (
|
||||
<FirewallClientsGrid
|
||||
clients={pending}
|
||||
isLoading={clientsQ.isFetching && !clientsQ.isLoading}
|
||||
onApprove={(id) => approve.mutate(id)}
|
||||
onReject={(id) => deleteClient.mutate(id)}
|
||||
approvePending={approve.isPending}
|
||||
rejectPending={deleteClient.isPending}
|
||||
emptyTitle="Нет pending-запросов"
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
<QueryState
|
||||
data={clientsQ.data}
|
||||
isLoading={clientsQ.isLoading}
|
||||
isError={clientsQ.isError}
|
||||
error={clientsQ.error}
|
||||
onRetry={() => void clientsQ.refetch()}
|
||||
skeleton={<TableSkeleton rows={3} cols={6} />}
|
||||
>
|
||||
{() => (
|
||||
<FirewallClientsGrid
|
||||
clients={pending}
|
||||
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>
|
||||
</BadgeTabs>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from '@evobgp/ui/components/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||
import { Separator } from '@evobgp/ui/components/separator'
|
||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import {
|
||||
DashboardOperationsFlowCard,
|
||||
@@ -129,24 +130,21 @@ function MonitoringComponent() {
|
||||
</Alert>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Доступность и готовность</CardTitle>
|
||||
<CardDescription>GET /v1/health · GET /v1/ready</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<QueryState
|
||||
data={readyQ.data}
|
||||
isLoading={readyQ.isLoading}
|
||||
isError={readyQ.isError}
|
||||
error={readyQ.error}
|
||||
skeleton={<div className="h-40" />}
|
||||
onRetry={() => readyQ.refetch()}
|
||||
>
|
||||
{(ready) => <MonitoringReadyGrid health={healthQ.data} ready={ready} />}
|
||||
</QueryState>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DataGridCard
|
||||
title="Доступность и готовность"
|
||||
description="GET /v1/health · GET /v1/ready"
|
||||
>
|
||||
<QueryState
|
||||
data={readyQ.data}
|
||||
isLoading={readyQ.isLoading}
|
||||
isError={readyQ.isError}
|
||||
error={readyQ.error}
|
||||
skeleton={<div className="h-40" />}
|
||||
onRetry={() => readyQ.refetch()}
|
||||
>
|
||||
{(ready) => <MonitoringReadyGrid health={healthQ.data} ready={ready} />}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evob
|
||||
import { Input } from '@evobgp/ui/components/input'
|
||||
import { Label } from '@evobgp/ui/components/label'
|
||||
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
|
||||
import { DataGridCard } from '@/components/data-grid-shell'
|
||||
import { SelectField } from '@/components/select-field'
|
||||
import { SettingsKvGrid } from '@/components/settings/settings-kv-grid'
|
||||
import { PageHeader } from '@/components/page-header'
|
||||
@@ -318,33 +319,28 @@ function TenantSettingsComponent() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="additional" className="mt-0">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Дополнительные параметры</CardTitle>
|
||||
<CardDescription>
|
||||
Параметры вне стандартных групп (readonly — изменяются только через API)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<QueryState
|
||||
data={partitioned?.additional ?? []}
|
||||
isLoading={settingsQ.isLoading}
|
||||
isError={settingsQ.isError}
|
||||
error={settingsQ.error}
|
||||
empty={(partitioned?.additional ?? []).length === 0}
|
||||
emptyTitle="Нет дополнительных параметров"
|
||||
skeleton={<div className="h-32" />}
|
||||
onRetry={() => settingsQ.refetch()}
|
||||
>
|
||||
{(items) => (
|
||||
<SettingsKvGrid
|
||||
items={items}
|
||||
isLoading={settingsQ.isFetching && !settingsQ.isLoading}
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DataGridCard
|
||||
title="Дополнительные параметры"
|
||||
description="Параметры вне стандартных групп (readonly — изменяются только через API)"
|
||||
>
|
||||
<QueryState
|
||||
data={partitioned?.additional ?? []}
|
||||
isLoading={settingsQ.isLoading}
|
||||
isError={settingsQ.isError}
|
||||
error={settingsQ.error}
|
||||
empty={(partitioned?.additional ?? []).length === 0}
|
||||
emptyTitle="Нет дополнительных параметров"
|
||||
skeleton={<div className="h-32" />}
|
||||
onRetry={() => settingsQ.refetch()}
|
||||
>
|
||||
{(items) => (
|
||||
<SettingsKvGrid
|
||||
items={items}
|
||||
isLoading={settingsQ.isFetching && !settingsQ.isLoading}
|
||||
/>
|
||||
)}
|
||||
</QueryState>
|
||||
</DataGridCard>
|
||||
</TabsContent>
|
||||
</BadgeTabs>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user