From 9ac64f92cc97a807ae8450846d5ff826886d7fc1 Mon Sep 17 00:00:00 2001 From: Gianluca Mardente Date: Sat, 17 Oct 2026 14:17:30 +0200 Subject: [PATCH] feat: Show the number of matching resources per cluster on an EventTrigger and list them on demand An EventTrigger can match hundreds of resources per cluster. The details page listed every one of them inside each cluster card, which made the page long and slow. Each cluster card now shows the number of matching resources, and clicking it opens a dialog with the list. --- src/api-client/endpoints.ts | 1 + src/assets/admiral-sveltopus.png | Bin 0 -> 32063 bytes src/lib/components/ui/feedback/emptyData.tsx | 8 +- src/lib/components/ui/layout/Sidebar.tsx | 10 + src/locales/ar.json | 4 + src/locales/en.json | 4 + src/locales/fr.json | 4 + src/modules/enterprise/EnterprisePage.tsx | 36 ++++ .../components/ClusterMatchCard.tsx | 48 +++-- .../components/EventResourcesDialog.tsx | 184 ++++++++++++++++++ .../event-details/hooks/useEventResources.ts | 45 +++++ src/types/event.types.ts | 9 +- src/utils/eventResources.test.ts | 68 +++++++ src/utils/eventResources.ts | 64 ++++++ 14 files changed, 459 insertions(+), 26 deletions(-) create mode 100644 src/assets/admiral-sveltopus.png create mode 100644 src/modules/events/event-details/components/EventResourcesDialog.tsx create mode 100644 src/modules/events/event-details/hooks/useEventResources.ts create mode 100644 src/utils/eventResources.test.ts create mode 100644 src/utils/eventResources.ts diff --git a/src/api-client/endpoints.ts b/src/api-client/endpoints.ts index 3cfa51e..d4e0c10 100644 --- a/src/api-client/endpoints.ts +++ b/src/api-client/endpoints.ts @@ -15,6 +15,7 @@ export const API_ENDPOINTS = { MCP_CLASSIFIER_PIPELINE_DEBUG: "/analyzeClassifierPipeline", EVENTS: "/events", EVENT: "/event", + EVENT_RESOURCES: "/eventResources", CLASSIFIERS: "/classifiers", CLASSIFIER: "/classifier", STATS: "/stats", diff --git a/src/assets/admiral-sveltopus.png b/src/assets/admiral-sveltopus.png new file mode 100644 index 0000000000000000000000000000000000000000..48c66cac83650f2d769a9157fa1f6c421e4f2faa GIT binary patch literal 32063 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
-
- -
+ {/* Decorative: the text below says what is missing */} +

There’s no {name} found. diff --git a/src/lib/components/ui/layout/Sidebar.tsx b/src/lib/components/ui/layout/Sidebar.tsx index f971a23..0189aa1 100644 --- a/src/lib/components/ui/layout/Sidebar.tsx +++ b/src/lib/components/ui/layout/Sidebar.tsx @@ -7,6 +7,7 @@ import { Badge } from "@/lib/components/ui/data-display/badge"; import React from "react"; import { cn } from "@/lib/utils"; import { Book, LifeBuoy, ExternalLink } from "lucide-react"; +import mascot from "@/assets/admiral-sveltopus.png"; interface SidebarProps { isOpen: boolean; @@ -129,6 +130,15 @@ export function Sidebar({ isOpen }: SidebarProps) {

+ {/* Decorative. Only when expanded and the window is tall enough: the sidebar clips what does not fit, + and Docs and Support must stay visible */} + {isOpen && ( + + )} {/* Separator before Support/Docs */}
diff --git a/src/locales/ar.json b/src/locales/ar.json index 9149276..5172010 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -54,6 +54,10 @@ "trigger_event": "تشغيل الحدث", "resources": "الموارد", "no_resources": "لم يتم العثور على موارد", + "matching_resources": "الموارد المطابقة", + "matching_resources_count": "الموارد المطابقة: {{count}}", + "matching_resources_no_match": "لا توجد موارد مطابقة لهذه المرشحات.", + "matching_resources_error": "تعذّر تحميل الموارد المطابقة.", "running": "جاري التشغيل", "no_resource_selectors": "لم يتم تحديد محددات الموارد.", "resource_selector": "محدد الموارد", diff --git a/src/locales/en.json b/src/locales/en.json index b56c4c4..c63e566 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -54,6 +54,10 @@ "trigger_event": "Trigger Event", "resources": "Resources", "no_resources": "No resources found", + "matching_resources": "Matching resources", + "matching_resources_count": "Matching resources: {{count}}", + "matching_resources_no_match": "No matching resources for the filters.", + "matching_resources_error": "The matching resources could not be loaded.", "running": "Running", "no_resource_selectors": "No resource selectors defined.", "resource_selector": "Resource Selector", diff --git a/src/locales/fr.json b/src/locales/fr.json index e6c64a2..d0abe23 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -55,6 +55,10 @@ "trigger_event": "Déclencher l'événement", "resources": "Ressources", "no_resources": "Aucune ressource trouvée", + "matching_resources": "Ressources correspondantes", + "matching_resources_count": "Ressources correspondantes : {{count}}", + "matching_resources_no_match": "Aucune ressource correspondante pour ces filtres.", + "matching_resources_error": "Les ressources correspondantes n'ont pas pu être chargées.", "running": "En cours", "no_resource_selectors": "Aucun sélecteur de ressource défini.", "resource_selector": "Sélecteur de ressource", diff --git a/src/modules/enterprise/EnterprisePage.tsx b/src/modules/enterprise/EnterprisePage.tsx index 9ec57e0..e856065 100644 --- a/src/modules/enterprise/EnterprisePage.tsx +++ b/src/modules/enterprise/EnterprisePage.tsx @@ -8,6 +8,10 @@ import { CheckCircle2, Zap, CalendarDays, + HeartPulse, + Lock, + PackageCheck, + Compass, } from "lucide-react"; import { Button } from "@/lib/components/ui/inputs/button"; import { Logo } from "@/lib/components/assets/logo/logo"; @@ -59,6 +63,22 @@ const FEATURES: Feature[] = [ tag: "Deployment", href: "https://projectsveltos.io/latest/deployment_order/progressive_rollout/", }, + { + title: "Job-Based Health Validation", + description: + "Run a Kubernetes Job after a deployment, such as a smoke test or a connectivity probe, and use its outcome as the health check.", + icon: HeartPulse, + tag: "Health", + href: "https://projectsveltos.io/latest/addons/clusterprofile/#job-based-health-validation", + }, + { + title: "Least-Privilege Agents", + description: + "Limit drift detection and the agents to specific namespaces instead of cluster-wide access. One annotation configures both.", + icon: Lock, + tag: "Security", + href: "https://projectsveltos.io/latest/features/configuration_drift/#namespace-scoped-watch-mode", + }, { title: "AI-Powered Management", description: @@ -79,6 +99,22 @@ const FEATURES: Feature[] = [ tag: "Support", href: "https://website.projectsveltos.io/pricing/", }, + { + title: "Priority Patches & Feature Delivery", + description: + "CVE-patched images before the next release, with your bug fixes and feature requests prioritized.", + icon: PackageCheck, + tag: "Support", + href: "https://website.projectsveltos.io/pricing/", + }, + { + title: "Architectural Guidance", + description: + "Expert guidance on architecture and best practices for running Sveltos in production.", + icon: Compass, + tag: "Support", + href: "https://website.projectsveltos.io/pricing/", + }, ]; const TRUST_ITEMS = [ diff --git a/src/modules/events/event-details/components/ClusterMatchCard.tsx b/src/modules/events/event-details/components/ClusterMatchCard.tsx index 6fa65f0..f3f406c 100644 --- a/src/modules/events/event-details/components/ClusterMatchCard.tsx +++ b/src/modules/events/event-details/components/ClusterMatchCard.tsx @@ -1,13 +1,14 @@ import { useNavigate } from "react-router-dom"; import { ExternalLink } from "lucide-react"; import { Button } from "@/lib/components/ui/inputs/button"; -import { ClusterEventMatch, Resource } from "@/types/event.types"; +import { ClusterEventMatch } from "@/types/event.types"; import { Badge } from "@/lib/components/ui/data-display/badge"; import { FailedFlag } from "@/lib/components/ui/data-display/failed-flag"; import { ReadyFlag } from "@/lib/components/ui/data-display/ready-flag"; import { useTranslation } from "react-i18next"; import { McpButton } from "@/lib/components/ui/inputs/mcp-button"; import * as React from "react"; +import { EventResourcesDialog } from "@/modules/events/event-details/components/EventResourcesDialog"; import { useEventClusterDebugMcp } from "@/modules/events/hooks/debugMcp"; import { typeFromPath } from "@/utils/GetPathFromType"; import { getClusterInfoType } from "@/utils/GetClusterInfoType"; @@ -21,6 +22,7 @@ export const ClusterMatchCard = ({ }) => { const { t } = useTranslation(); const navigate = useNavigate(); + const [resourcesOpen, setResourcesOpen] = React.useState(false); const McpQuery = useEventClusterDebugMcp({ namespace: cluster.clusterNamespace, @@ -59,27 +61,31 @@ export const ClusterMatchCard = ({
-
- {t("common.resources")} -
-
- {cluster.resources?.map((res: Resource, idx: number) => ( -
0 ? ( + <> +
- ))} - {(!cluster.resources || cluster.resources.length === 0) && ( -
- {t("common.no_resources")} -
- )} -
+ {t("common.matching_resources_count", { + count: cluster.matchingResourcesCount, + })} + + + + ) : ( +
+ {t("common.no_resources")} +
+ )}
diff --git a/src/modules/events/event-details/components/EventResourcesDialog.tsx b/src/modules/events/event-details/components/EventResourcesDialog.tsx new file mode 100644 index 0000000..62e9c4b --- /dev/null +++ b/src/modules/events/event-details/components/EventResourcesDialog.tsx @@ -0,0 +1,184 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/lib/components/ui/data-display/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/lib/components/ui/data-display/table"; +import { LoadingTableRow } from "@/lib/components/ui/feedback/loadingTableRow"; +import { Input } from "@/lib/components/ui/inputs/input"; +import useDebounce from "@/hooks/useDebounce"; +import { usePagination } from "@/hooks/usePagination"; +import useEventResources from "@/modules/events/event-details/hooks/useEventResources"; +import { appConfig } from "@/config/app"; +import { ClusterType } from "@/types/cluster.types"; +import { + EVENT_RESOURCES_PAGE_SIZE, + EventResourcesFilters, + hasEventResourcesFilters, + noEventResourcesFilters, +} from "@/utils/eventResources"; + +interface EventResourcesDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + eventName: string; + clusterNamespace: string; + clusterName: string; + clusterType: ClusterType; +} + +// Table wraps itself in a min-h-120 div: reset it so a short list neither +// leaves blank space nor shows a scrollbar +const tableContainerClassName = "max-h-96 overflow-y-auto [&>div]:min-h-0"; + +// TableCell truncates at 6rem by default, which cuts names +const cellClassName = "max-w-none break-words whitespace-normal"; + +const filterDelayMs = 300; + +// EventResourcesDialog lists, one page at a time, the resources matching the EventSource of an EventTrigger in +// a cluster. A cluster can match hundreds, so they are requested only once the dialog is opened. +export const EventResourcesDialog = ({ + open, + onOpenChange, + eventName, + clusterNamespace, + clusterName, + clusterType, +}: EventResourcesDialogProps) => { + const { t } = useTranslation(); + const [page, setPage] = useState(appConfig.defaultPage); + // What is typed, and what is asked for: the second follows the first after a short pause + const [typed, setTyped] = useState( + noEventResourcesFilters, + ); + const [filters, setFilters] = useState( + noEventResourcesFilters, + ); + + useDebounce( + () => { + setFilters(typed); + }, + [typed], + filterDelayMs, + ); + + // Another page of the previous filters would make no sense + useEffect(() => { + setPage(appConfig.defaultPage); + }, [filters]); + + const query = useEventResources( + { eventName, clusterNamespace, clusterName, clusterType }, + page, + filters, + open, + ); + const resources = query.data?.resources ?? []; + const total = query.data?.totalResources ?? 0; + + const [PaginationUI] = usePagination( + total, + page, + setPage, + EVENT_RESOURCES_PAGE_SIZE, + ); + + const handleFilterChange = + (key: keyof EventResourcesFilters) => + (event: React.ChangeEvent) => + setTyped((current) => ({ ...current, [key]: event.target.value })); + + return ( + + + + {t("common.matching_resources")} + + {`${eventName} · ${clusterNamespace}/${clusterName}`} + + +
+ + + +
+
+ + + + {t("common.kind")} + {t("common.namespace")} + {t("common.name")} + + + {query.isLoading ? ( + + ) : ( + + {resources.length > 0 ? ( + resources.map((resource, index) => ( + + + {resource.group + ? `${resource.kind}.${resource.group}` + : resource.kind} + + + {resource.namespace || "-"} + + + {resource.name} + + + )) + ) : ( + + + {query.isError + ? t("common.matching_resources_error") + : hasEventResourcesFilters(filters) + ? t("common.matching_resources_no_match") + : t("common.no_resources")} + + + )} + + )} +
+
+
+ +
+
+
+ ); +}; diff --git a/src/modules/events/event-details/hooks/useEventResources.ts b/src/modules/events/event-details/hooks/useEventResources.ts new file mode 100644 index 0000000..c61d840 --- /dev/null +++ b/src/modules/events/event-details/hooks/useEventResources.ts @@ -0,0 +1,45 @@ +import { useQuery, UseQueryResult } from "@tanstack/react-query"; + +import client from "@/api-client/apiClient"; +import { API_ENDPOINTS } from "@/api-client/endpoints"; +import { ClusterType } from "@/types/cluster.types"; +import { EventResourcesResponse } from "@/types/event.types"; +import { + buildEventResourcesParams, + EventResourcesFilters, +} from "@/utils/eventResources"; + +type EventResourcesTarget = { + eventName: string; + clusterNamespace: string; + clusterName: string; + clusterType: ClusterType; +}; + +const fetchEventResources = async ( + target: EventResourcesTarget, + page: number, + filters: EventResourcesFilters, +): Promise => { + const { data } = await client.get(API_ENDPOINTS.EVENT_RESOURCES, { + params: buildEventResourcesParams(target, page, filters), + }); + return data as EventResourcesResponse; +}; + +// useEventResources loads one page of the resources matching an EventTrigger in a cluster. Nothing is requested +// until enabled is true: the dialog showing them asks for them only once opened. +const useEventResources = ( + target: EventResourcesTarget, + page: number, + filters: EventResourcesFilters, + enabled: boolean, +): UseQueryResult => { + return useQuery({ + queryKey: ["eventResources", target, page, filters], + queryFn: () => fetchEventResources(target, page, filters), + enabled, + }); +}; + +export default useEventResources; diff --git a/src/types/event.types.ts b/src/types/event.types.ts index 38beba4..253847c 100644 --- a/src/types/event.types.ts +++ b/src/types/event.types.ts @@ -14,10 +14,17 @@ export type ClusterEventMatch = { clusterName: string; clusterKind: string; failureMessage?: string; - resources?: Resource[]; + // Number of resources matching the EventSource in the cluster. They are loaded, paginated, on demand. + matchingResourcesCount: number; paused?: boolean; }; +export type EventResourcesResponse = { + // Number of resources matching the filters, before pagination + totalResources: number; + resources: Resource[]; +}; + export type LabelFilter = { key: string; operation: string; diff --git a/src/utils/eventResources.test.ts b/src/utils/eventResources.test.ts new file mode 100644 index 0000000..7f426de --- /dev/null +++ b/src/utils/eventResources.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; + +import { + buildEventResourcesParams, + EVENT_RESOURCES_PAGE_SIZE, + hasEventResourcesFilters, + noEventResourcesFilters, +} from "@/utils/eventResources"; + +describe("buildEventResourcesParams", () => { + const target = { + eventName: "service-network-policy", + clusterNamespace: "default", + clusterName: "prod", + clusterType: "SveltosCluster" as const, + }; + + it("asks for the first page of a cluster without filters", () => { + expect( + buildEventResourcesParams(target, 1, noEventResourcesFilters), + ).toEqual({ + event_name: "service-network-policy", + namespace: "default", + name: "prod", + type: "sveltos", + skip: 0, + limit: EVENT_RESOURCES_PAGE_SIZE, + }); + }); + + it("skips the pages before the one asked for", () => { + const params = buildEventResourcesParams( + { ...target, clusterType: "ClusterAPI" }, + 3, + noEventResourcesFilters, + ); + + expect(params.type).toBe("capi"); + expect(params.skip).toBe(2 * EVENT_RESOURCES_PAGE_SIZE); + }); + + it("sends only the filters which are set, without surrounding spaces", () => { + const params = buildEventResourcesParams(target, 1, { + kind: " Service ", + namespace: " ", + name: "front", + }); + + expect(params.resource_kind).toBe("Service"); + expect(params.resource_name).toBe("front"); + expect(params).not.toHaveProperty("resource_namespace"); + }); +}); + +describe("hasEventResourcesFilters", () => { + it("is false when no filter is set, or only spaces are", () => { + expect(hasEventResourcesFilters(noEventResourcesFilters)).toBe(false); + expect( + hasEventResourcesFilters({ kind: " ", namespace: "", name: "" }), + ).toBe(false); + }); + + it("is true when a filter is set", () => { + expect( + hasEventResourcesFilters({ kind: "", namespace: "", name: "web" }), + ).toBe(true); + }); +}); diff --git a/src/utils/eventResources.ts b/src/utils/eventResources.ts new file mode 100644 index 0000000..ecbce05 --- /dev/null +++ b/src/utils/eventResources.ts @@ -0,0 +1,64 @@ +import { ClusterType } from "@/types/cluster.types"; +import { getClusterInfoType } from "@/utils/GetClusterInfoType"; +import { getItemsToSkip } from "@/utils/getItemsToSkip"; + +export const EVENT_RESOURCES_PAGE_SIZE = 10; + +export type EventResourcesFilters = { + kind: string; + namespace: string; + name: string; +}; + +export const noEventResourcesFilters: EventResourcesFilters = { + kind: "", + namespace: "", + name: "", +}; + +type EventResourcesTarget = { + eventName: string; + clusterNamespace: string; + clusterName: string; + clusterType: ClusterType; +}; + +// buildEventResourcesParams returns the query parameters of ui-backend /eventResources: the page asked for +// and the filters which are set. +export const buildEventResourcesParams = ( + target: EventResourcesTarget, + page: number, + filters: EventResourcesFilters, +): Record => { + const params: Record = { + event_name: target.eventName, + namespace: target.clusterNamespace, + name: target.clusterName, + type: getClusterInfoType(target.clusterType), + skip: getItemsToSkip(page, EVENT_RESOURCES_PAGE_SIZE), + limit: EVENT_RESOURCES_PAGE_SIZE, + }; + + const kind = filters.kind.trim(); + const namespace = filters.namespace.trim(); + const name = filters.name.trim(); + if (kind) { + params.resource_kind = kind; + } + if (namespace) { + params.resource_namespace = namespace; + } + if (name) { + params.resource_name = name; + } + + return params; +}; + +// hasEventResourcesFilters is true when at least one filter is set +export const hasEventResourcesFilters = ( + filters: EventResourcesFilters, +): boolean => + Boolean( + filters.kind.trim() || filters.namespace.trim() || filters.name.trim(), + );