diff --git a/apps/api/src/routes/agents.ts b/apps/api/src/routes/agents.ts index da30343..d7448ef 100644 --- a/apps/api/src/routes/agents.ts +++ b/apps/api/src/routes/agents.ts @@ -5,6 +5,7 @@ import { putAgentPolicySetsBodySchema, patchAgentBodySchema, cloneFromBodySchema, + agentIdsBodySchema, } from '@evofw/shared' import { AppError } from '../plugins/error-handler.js' import { evaluateAgentPolicy, truncateCidrs } from '../services/policy/evaluate.js' @@ -147,6 +148,32 @@ export const agentsRoutes: FastifyPluginAsync<{ config: AppConfig }> = async ( return mapAgent(updated!) }) + app.post('/agents/approve-bulk', async (req) => { + const body = agentIdsBodySchema.parse(req.body) + const now = new Date().toISOString() + const approved: string[] = [] + app.sqlite.transaction(() => { + for (const id of body.agent_ids) { + const a = repos.getAgent(app.db, id) + if (!a || (a.status !== 'pending' && a.status !== 'invited')) continue + repos.updateAgent(app.db, a.id, { + status: 'approved', + approvedAt: now, + }) + repos.ensureSharedSetAssigned(app.db, a.id) + approved.push(a.id) + } + })() + auditMutation(app, config, req, { + action: 'agent.approve', + targetType: 'app_resource', + targetId: approved[0] ?? '', + summary: `Массовое одобрение агентов: ${approved.length}`, + details: { agent_ids: approved }, + }) + return { items: approved.map((id) => mapAgent(repos.getAgent(app.db, id)!)) } + }) + app.post<{ Params: { id: string } }>('/agents/:id/revoke', async (req) => { const a = repos.getAgent(app.db, req.params.id) if (!a) throw new AppError('NOT_FOUND', 'Agent not found', 404) diff --git a/apps/api/src/routes/policy-sets.ts b/apps/api/src/routes/policy-sets.ts index 838e5de..d4f533e 100644 --- a/apps/api/src/routes/policy-sets.ts +++ b/apps/api/src/routes/policy-sets.ts @@ -3,6 +3,7 @@ import { repos } from '@evofw/db' import { createPolicySetBodySchema, patchPolicySetBodySchema, + agentIdsBodySchema, } from '@evofw/shared' import { AppError } from '../plugins/error-handler.js' import type { AppConfig } from '../config.js' @@ -100,4 +101,59 @@ export const policySetsRoutes: FastifyPluginAsync<{ config: AppConfig }> = async } return { ok: true } }) + + /** + * Replace which agents have this set assigned: listed agents gain the set + * (other assignments preserved), unlisted agents lose it. + */ + app.put<{ Params: { id: string } }>( + '/policy-sets/:id/agents', + async (req) => { + const set = repos.getPolicySet(app.db, req.params.id) + if (!set) throw new AppError('NOT_FOUND', 'Policy set not found', 404) + const body = agentIdsBodySchema.parse(req.body) + + const target = new Set(body.agent_ids) + for (const agentId of body.agent_ids) { + if (!repos.getAgent(app.db, agentId)) { + throw new AppError('NOT_FOUND', `Agent not found: ${agentId}`, 404) + } + } + + const current = repos.listAgentIdsForSet(app.db, set.id) + const toAdd = body.agent_ids.filter((id) => !current.includes(id)) + const toRemove = current.filter((id) => !target.has(id)) + + const applyAssignment = (agentId: string, withSet: boolean) => { + const others = repos + .listSetsForAgent(app.db, agentId) + .map((s) => s.setId) + .filter((id) => id !== set.id) + const next = withSet ? [...others, set.id] : others + repos.setAgentPolicySets(app.db, agentId, next) + } + + app.sqlite.transaction(() => { + for (const agentId of toAdd) applyAssignment(agentId, true) + for (const agentId of toRemove) applyAssignment(agentId, false) + })() + + auditMutation(app, config, req, { + action: 'policy_set.agents.update', + targetType: 'app_resource', + targetId: set.id, + summary: `Назначение набора ${set.name} обновлено`, + details: { + set_id: set.id, + added: toAdd, + removed: toRemove, + }, + }) + return { + agent_ids: repos.listAgentIdsForSet(app.db, set.id), + added: toAdd, + removed: toRemove, + } + }, + ) } diff --git a/apps/api/src/services/bulk-ops.test.ts b/apps/api/src/services/bulk-ops.test.ts new file mode 100644 index 0000000..fcf15d0 --- /dev/null +++ b/apps/api/src/services/bulk-ops.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, afterAll } from 'vitest' +import { buildApp } from '../app.js' +import type { AppConfig } from '../config.js' + +const testConfig: AppConfig = { + databaseUrl: 'sqlite::memory:', + jwtSecret: 'test', + jwtTtlHours: 24, + serverPort: 8080, + staticDir: null, + logLevel: 'error', + authRequired: false, + authIssuer: 'https://auth.test', + authPortalUrl: 'http://localhost:5175', + publicBaseUrl: 'https://fw.example.com', + enrollSeed: 'test-seed', + corsOrigins: [], + secretKey: null, +} + +async function createInvitedAgent( + app: Awaited>, + name: string, +): Promise { + const created = await app.inject({ + method: 'POST', + url: '/api/v1/install-links', + payload: { name, platform: 'linux' }, + }) + expect(created.statusCode).toBe(201) + return (created.json() as { agent_id: string }).agent_id +} + +describe('bulk agent operations', () => { + const appPromise = buildApp({ memory: true, config: testConfig }) + + afterAll(async () => { + const app = await appPromise + await app.close() + }) + + it('approve-bulk approves invited agents in one request', async () => { + const app = await appPromise + await app.ready() + + const a1 = await createInvitedAgent(app, 'bulk-01') + const a2 = await createInvitedAgent(app, 'bulk-02') + const a3 = await createInvitedAgent(app, 'bulk-03') + + const bulk = await app.inject({ + method: 'POST', + url: '/api/v1/agents/approve-bulk', + payload: { agent_ids: [a1, a2, a3] }, + }) + expect(bulk.statusCode).toBe(200) + const body = bulk.json() as { items: { id: string; status: string }[] } + expect(body.items.map((i) => i.id).sort()).toEqual([a1, a2, a3].sort()) + expect(body.items.every((i) => i.status === 'approved')).toBe(true) + + // shared default set assigned on approve + for (const id of [a1, a2, a3]) { + const sets = await app.inject({ + method: 'GET', + url: `/api/v1/agents/${id}/policy-sets`, + }) + const items = (sets.json() as { items: { set_id: string }[] }).items + expect(items.some((s) => s.set_id === 'set-shared-default')).toBe(true) + } + + // repeated bulk is a no-op (already approved) + const again = await app.inject({ + method: 'POST', + url: '/api/v1/agents/approve-bulk', + payload: { agent_ids: [a1] }, + }) + expect(again.statusCode).toBe(200) + expect((again.json() as { items: unknown[] }).items).toEqual([]) + }) + + it('PUT /policy-sets/:id/agents replaces assignment of the set', async () => { + const app = await appPromise + await app.ready() + + const a1 = await createInvitedAgent(app, 'assign-01') + const a2 = await createInvitedAgent(app, 'assign-02') + await app.inject({ + method: 'POST', + url: '/api/v1/agents/approve-bulk', + payload: { agent_ids: [a1, a2] }, + }) + + const created = await app.inject({ + method: 'POST', + url: '/api/v1/policy-sets', + payload: { name: 'bulk-assign-set' }, + }) + const setId = (created.json() as { id: string }).id + + // assign both + const put = await app.inject({ + method: 'PUT', + url: `/api/v1/policy-sets/${setId}/agents`, + payload: { agent_ids: [a1, a2] }, + }) + expect(put.statusCode).toBe(200) + expect((put.json() as { added: string[] }).added.sort()).toEqual( + [a1, a2].sort(), + ) + + // a1 keeps set when a2 removed; shared default preserved for both + const drop = await app.inject({ + method: 'PUT', + url: `/api/v1/policy-sets/${setId}/agents`, + payload: { agent_ids: [a1] }, + }) + expect(drop.statusCode).toBe(200) + const dropBody = drop.json() as { + agent_ids: string[] + removed: string[] + } + expect(dropBody.agent_ids).toEqual([a1]) + expect(dropBody.removed).toEqual([a2]) + + const setsA1 = ( + (await app.inject({ method: 'GET', url: `/api/v1/agents/${a1}/policy-sets` })) + .json() as { items: { set_id: string }[] } + ).items.map((s) => s.set_id) + const setsA2 = ( + (await app.inject({ method: 'GET', url: `/api/v1/agents/${a2}/policy-sets` })) + .json() as { items: { set_id: string }[] } + ).items.map((s) => s.set_id) + expect(setsA1).toContain(setId) + expect(setsA2).not.toContain(setId) + expect(setsA2).toContain('set-shared-default') + + // empty array clears the whole assignment + const clear = await app.inject({ + method: 'PUT', + url: `/api/v1/policy-sets/${setId}/agents`, + payload: { agent_ids: [] }, + }) + expect(clear.statusCode).toBe(200) + expect((clear.json() as { agent_ids: string[] }).agent_ids).toEqual([]) + + // unknown agent → 404, and nothing changed + const bad = await app.inject({ + method: 'PUT', + url: `/api/v1/policy-sets/${setId}/agents`, + payload: { agent_ids: ['no-such-agent'] }, + }) + expect(bad.statusCode).toBe(404) + }) +}) diff --git a/apps/web/src/components/route-error.tsx b/apps/web/src/components/route-error.tsx new file mode 100644 index 0000000..260d666 --- /dev/null +++ b/apps/web/src/components/route-error.tsx @@ -0,0 +1,66 @@ +import { Link } from '@tanstack/react-router' +import { CircleAlertIcon, SearchXIcon } from 'lucide-react' +import { Button } from '@evofw/ui/components/button' + +/** + * Route-level error/not-found fallbacks (ReUI Frame look, RU copy). + * Preview: https://reui.io/preview/base/feature-4 (centered error panel) + */ + +/** Error thrown by the root guard while the browser is already navigating to the portal. */ +const PORTAL_REDIRECT_MESSAGE = 'redirecting to portal' + +export function RouteErrorComponent({ + error, + reset, +}: { + error: Error + reset: () => void +}) { + if (error.message === PORTAL_REDIRECT_MESSAGE) return null + + return ( +
+ +
+

Что-то пошло не так

+

+ Раздел не загрузился. Проверьте соединение с API и попробуйте снова. +

+ {import.meta.env.DEV && error.message ? ( +

+ {error.message} +

+ ) : null} +
+
+ + +
+
+ ) +} + +export function RouteNotFoundComponent() { + return ( +
+ +
+

Страница не найдена

+

+ Адрес не существует или был перемещён. +

+
+ +
+ ) +} diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index f4c783f..186fcb2 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -6,6 +6,10 @@ import { isTokenValid, redirectToPortalLogin, } from '@/lib/auth' +import { + RouteErrorComponent, + RouteNotFoundComponent, +} from '@/components/route-error' export type RouterContext = { queryClient: QueryClient @@ -22,4 +26,6 @@ export const Route = createRootRouteWithContext()({ } }, component: () => , + errorComponent: RouteErrorComponent, + notFoundComponent: RouteNotFoundComponent, }) diff --git a/apps/web/src/routes/_auth/agents/index.tsx b/apps/web/src/routes/_auth/agents/index.tsx index 50b229d..822d455 100644 --- a/apps/web/src/routes/_auth/agents/index.tsx +++ b/apps/web/src/routes/_auth/agents/index.tsx @@ -125,21 +125,46 @@ function AgentsPage() { [navigate], ) + /** Optimistically patch the agents list; returns a rollback fn. */ + const patchAgentsCache = useCallback( + (patch: (items: Agent[]) => Agent[]) => { + const prev = qc.getQueryData<{ items: Agent[] }>(['agents']) + if (prev) qc.setQueryData(['agents'], { items: patch(prev.items) }) + return () => { + if (prev) qc.setQueryData(['agents'], prev) + } + }, + [qc], + ) + const approve = useMutation({ mutationFn: (id: string) => apiFetch(`/api/v1/agents/${id}/approve`, { method: 'POST' }), + onMutate: (id) => + patchAgentsCache((items) => + items.map((a) => (a.id === id ? { ...a, status: 'approved' } : a)), + ), onSuccess: () => { toast.success('Агент одобрен') void qc.invalidateQueries({ queryKey: ['agents'] }) }, - onError: (e: Error) => toast.error(e.message), + onError: (e: Error, _id, rollback) => { + rollback?.() + toast.error(e.message) + }, }) const approveAllPending = useMutation({ - mutationFn: async (ids: string[]) => { - await Promise.all( - ids.map((id) => - apiFetch(`/api/v1/agents/${id}/approve`, { method: 'POST' }), + mutationFn: (ids: string[]) => + apiFetch('/api/v1/agents/approve-bulk', { + method: 'POST', + body: JSON.stringify({ agent_ids: ids }), + }), + onMutate: (ids) => { + const target = new Set(ids) + return patchAgentsCache((items) => + items.map((a) => + target.has(a.id) ? { ...a, status: 'approved' } : a, ), ) }, @@ -147,12 +172,16 @@ function AgentsPage() { toast.success('Все pending одобрены') void qc.invalidateQueries({ queryKey: ['agents'] }) }, - onError: (e: Error) => toast.error(e.message), + onError: (e: Error, _ids, rollback) => { + rollback?.() + toast.error(e.message) + }, }) const removeAgent = useMutation({ mutationFn: (id: string) => apiFetch(`/api/v1/agents/${id}`, { method: 'DELETE' }), + onMutate: (id) => patchAgentsCache((items) => items.filter((a) => a.id !== id)), onSuccess: (_data, id) => { toast.success('Агент удалён') setDeleteAgentId(null) @@ -162,7 +191,10 @@ function AgentsPage() { void qc.invalidateQueries({ queryKey: ['agents'] }) void qc.invalidateQueries({ queryKey: ['dashboard'] }) }, - onError: (e: Error) => toast.error(e.message), + onError: (e: Error, _id, rollback) => { + rollback?.() + toast.error(e.message) + }, }) const items = agentsQ.data?.items ?? [] diff --git a/apps/web/src/routes/_auth/rules/$setId.tsx b/apps/web/src/routes/_auth/rules/$setId.tsx index a923fd2..d5c4d85 100644 --- a/apps/web/src/routes/_auth/rules/$setId.tsx +++ b/apps/web/src/routes/_auth/rules/$setId.tsx @@ -44,7 +44,7 @@ import { import { DataGrid } from '@/components/reui/data-grid/data-grid' import { DataGridTable } from '@/components/reui/data-grid/data-grid-table' import { getCoreRowModel, useReactTable } from '@tanstack/react-table' -import type { Agent } from '@evofw/shared' +import type { Agent, PolicySet } from '@evofw/shared' import { Skeleton } from '@evofw/ui/components/skeleton' import { Frame, @@ -107,33 +107,33 @@ function PolicySetDetailPage() { method: 'PATCH', body: JSON.stringify(body), }), + onMutate: (body) => { + const key = ['policy-sets', setId] as const + const prev = qc.getQueryData(key) + if (prev && body.enabled !== undefined) { + qc.setQueryData(key, { ...prev, enabled: body.enabled }) + } + return () => { + if (prev) qc.setQueryData(key, prev) + } + }, onSuccess: () => { toast.success('Набор обновлён') void qc.invalidateQueries({ queryKey: ['policy-sets'] }) void qc.invalidateQueries({ queryKey: ['agents'] }) }, - onError: (e: Error) => toast.error(e.message), + onError: (e: Error, _body, rollback) => { + rollback?.() + toast.error(e.message) + }, }) const saveAgents = useMutation({ - mutationFn: async () => { - const allAgents = agentsQ.data?.items ?? [] - await Promise.all( - allAgents.map(async (a) => { - const current = await apiFetch<{ - items: { set_id: string }[] - }>(`/api/v1/agents/${a.id}/policy-sets`) - const others = current.items - .map((i) => i.set_id) - .filter((id) => id !== setId) - const next = assignedIds.includes(a.id) ? [...others, setId] : others - await apiFetch(`/api/v1/agents/${a.id}/policy-sets`, { - method: 'PUT', - body: JSON.stringify({ set_ids: next }), - }) - }), - ) - }, + mutationFn: () => + apiFetch(`/api/v1/policy-sets/${setId}/agents`, { + method: 'PUT', + body: JSON.stringify({ agent_ids: assignedIds }), + }), onSuccess: () => { toast.success('Назначение агентов сохранено') setSelectedAgents(null) diff --git a/apps/web/src/routes/_auth/settings.tsx b/apps/web/src/routes/_auth/settings.tsx index d0de260..cfdc1b9 100644 --- a/apps/web/src/routes/_auth/settings.tsx +++ b/apps/web/src/routes/_auth/settings.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from '@tanstack/react-router' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { PageHeader, PageShell } from '@/components/reui-kit' import { Frame, @@ -32,8 +32,13 @@ function SettingsPage() { const settingsQ = useQuery(settingsQueryOptions()) const [form, setForm] = useState>({}) + // Seed the form once — a background refetch must not wipe in-progress edits. + const initialized = useRef(false) useEffect(() => { - if (settingsQ.data) setForm(settingsQ.data) + if (settingsQ.data && !initialized.current) { + initialized.current = true + setForm(settingsQ.data) + } }, [settingsQ.data]) const save = useMutation({ @@ -133,6 +138,7 @@ function SettingsPage() { save.mutate()} isLoading={save.isPending} + disabled={!initialized.current} loadingLabel="Сохранение…" > Сохранить diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 1232fb5..d984b27 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -6,10 +6,44 @@ import { TanStackRouterVite } from '@tanstack/router-plugin/vite' export default defineConfig({ plugins: [ - TanStackRouterVite({ routesDirectory: './src/routes', target: 'react' }), + TanStackRouterVite({ + routesDirectory: './src/routes', + target: 'react', + autoCodeSplitting: true, + }), react(), tailwindcss(), ], + build: { + rollupOptions: { + output: { + manualChunks(id) { + if ( + /node_modules[\\/](recharts|victory-vendor|d3-[a-z-]+|react-smooth|recharts-scale)[\\/]/.test( + id, + ) + ) { + return 'charts' + } + if (/node_modules[\\/]@dnd-kit[\\/]/.test(id)) return 'dnd' + if ( + /node_modules[\\/](react|react-dom|scheduler|clsx|tailwind-merge)[\\/]/.test( + id, + ) + ) { + return 'react' + } + if (/node_modules[\\/]@tanstack[\\/]react-router[\\/]/.test(id)) { + return 'router' + } + if (/node_modules[\\/]@tanstack[\\/]react-query[\\/]/.test(id)) { + return 'query' + } + return undefined + }, + }, + }, + }, resolve: { alias: { '@': path.resolve(__dirname, './src'), diff --git a/packages/shared/src/contracts.ts b/packages/shared/src/contracts.ts index 3d782f1..6e06d16 100644 --- a/packages/shared/src/contracts.ts +++ b/packages/shared/src/contracts.ts @@ -181,6 +181,11 @@ export const putAgentPolicySetsBodySchema = z.object({ set_ids: z.array(z.string()), }) +/** Shared body for bulk agent operations (approve-bulk, policy-set assignment). Empty array = clear assignment. */ +export const agentIdsBodySchema = z.object({ + agent_ids: z.array(z.string().min(1)).max(1000), +}) + export const createOverrideBodySchema = z.object({ cidr: z.string().min(1), action: policyActionSchema, diff --git a/packages/ui/package.json b/packages/ui/package.json index 90a1b48..2a8049e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -27,7 +27,7 @@ "lucide-react": "^0.468.0", "next-themes": "^0.4.6", "react-day-picker": "^9.4.0", - "recharts": "^2.15.0", + "recharts": "^3.8.0", "sonner": "^1.7.0", "tailwind-merge": "^3.0.0", "tw-animate-css": "^1.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39ce101..127328e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,8 +296,8 @@ importers: specifier: ^19.0.0 version: 19.2.7(react@19.2.7) recharts: - specifier: ^2.15.0 - version: 2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: ^3.8.0 + version: 3.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@18.3.1)(react@19.2.7)(redux@5.0.1) sonner: specifier: ^1.7.0 version: 1.7.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -2974,9 +2974,6 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dom-helpers@5.2.1: - resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dompurify@3.4.13: resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} @@ -3195,9 +3192,6 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -3223,10 +3217,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-equals@5.4.1: - resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==} - engines: {node: '>=6.0.0'} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -4628,12 +4618,6 @@ packages: '@types/react': optional: true - react-smooth@4.0.4: - resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -4649,12 +4633,6 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 - react-transition-group@4.4.5: - resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} - peerDependencies: - react: '>=16.6.0' - react-dom: '>=16.6.0' - react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -4701,17 +4679,6 @@ packages: real-require@1.0.0: resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} - recharts-scale@0.4.5: - resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - - recharts@2.15.4: - resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} - engines: {node: '>=14'} - deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - recharts@3.8.0: resolution: {integrity: sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==} engines: {node: '>=18'} @@ -5383,9 +5350,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - victory-vendor@36.9.2: - resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} @@ -8069,11 +8033,6 @@ snapshots: dependencies: path-type: 4.0.0 - dom-helpers@5.2.1: - dependencies: - '@babel/runtime': 7.29.7 - csstype: 3.2.3 - dompurify@3.4.13: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -8288,8 +8247,6 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter3@4.0.7: {} - eventemitter3@5.0.4: {} execa@8.0.1: @@ -8327,8 +8284,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-equals@5.4.1: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9566,14 +9521,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - react-smooth@4.0.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - fast-equals: 5.4.1 - prop-types: 15.8.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-transition-group: 4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): dependencies: get-nonce: 1.0.1 @@ -9588,15 +9535,6 @@ snapshots: prop-types: 15.8.1 react: 19.2.7 - react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - '@babel/runtime': 7.29.7 - dom-helpers: 5.2.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react@19.2.7: {} read-package-up@11.0.0: @@ -9655,23 +9593,6 @@ snapshots: real-require@1.0.0: {} - recharts-scale@0.4.5: - dependencies: - decimal.js-light: 2.5.1 - - recharts@2.15.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - clsx: 2.1.1 - eventemitter3: 4.0.7 - lodash: 4.18.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-is: 18.3.1 - react-smooth: 4.0.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - recharts-scale: 0.4.5 - tiny-invariant: 1.3.3 - victory-vendor: 36.9.2 - recharts@3.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@18.3.1)(react@19.2.7)(redux@5.0.1): dependencies: '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) @@ -10351,23 +10272,6 @@ snapshots: vary@1.1.2: {} - victory-vendor@36.9.2: - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - victory-vendor@37.3.6: dependencies: '@types/d3-array': 3.2.2