diff --git a/src/locales/ar.json b/src/locales/ar.json index e2f2f4b..19b3bed 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -129,6 +129,13 @@ "search_cluster_namespace_tooltip_events": "تصفية مشغلات الأحداث المطابقة لكلاستر في هذا النيم سبيس", "search_cluster_name_tooltip_events": "تصفية مشغلات الأحداث المطابقة لكلاستر بهذا الاسم", "search_cluster_namespace_tooltip_classifiers": "تصفية المصنّفات المطابقة لكلاستر في هذا النيم سبيس", - "search_cluster_name_tooltip_classifiers": "تصفية المصنّفات المطابقة لكلاستر بهذا الاسم" + "search_cluster_name_tooltip_classifiers": "تصفية المصنّفات المطابقة لكلاستر بهذا الاسم", + "drift": "الانحراف", + "drift_truncated": "شمل آخر انحراف موارد أكثر مما أمكن الإبلاغ عنه.", + "drift_resources_not_reported": "تم اكتشاف انحراف، لكن لم يتم الإبلاغ عن الموارد المتأثرة.", + "helm_release": "إصدار Helm", + "drift_history": "سجل الانحراف", + "drift_last_detected": "آخر انحراف تم اكتشافه في", + "detected": "تم الاكتشاف" } } diff --git a/src/locales/en.json b/src/locales/en.json index 012220c..3add67b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -177,6 +177,13 @@ "edit": "Edit", "delete": "Delete", "kind_hint_cluster_profile": "Cluster scoped, matches clusters across all namespaces.", - "kind_hint_profile": "Namespace scoped, matches clusters in the same namespace." + "kind_hint_profile": "Namespace scoped, matches clusters in the same namespace.", + "drift": "Drift", + "drift_truncated": "The most recent drift involved more resources than could be reported.", + "drift_resources_not_reported": "Drift was detected, but the affected resources were not reported.", + "helm_release": "Helm release", + "drift_history": "Drift history", + "drift_last_detected": "Last drift detected at", + "detected": "Detected" } } diff --git a/src/locales/fr.json b/src/locales/fr.json index 470775f..b5cdd4a 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -130,6 +130,13 @@ "search_cluster_namespace_tooltip_events": "Filtre les déclencheurs d'événements correspondant à un cluster dans cet espace de noms", "search_cluster_name_tooltip_events": "Filtre les déclencheurs d'événements correspondant à un cluster portant ce nom", "search_cluster_namespace_tooltip_classifiers": "Filtre les classificateurs correspondant à un cluster dans cet espace de noms", - "search_cluster_name_tooltip_classifiers": "Filtre les classificateurs correspondant à un cluster portant ce nom" + "search_cluster_name_tooltip_classifiers": "Filtre les classificateurs correspondant à un cluster portant ce nom", + "drift": "Dérive", + "drift_truncated": "La dernière dérive concernait plus de ressources que celles qui ont pu être signalées.", + "drift_resources_not_reported": "Une dérive a été détectée, mais les ressources concernées n'ont pas été signalées.", + "helm_release": "Release Helm", + "drift_history": "Historique des dérives", + "drift_last_detected": "Dernière dérive détectée le", + "detected": "Détectée" } } diff --git a/src/modules/clusters/cluster-information/components/addonsTable/DriftDialog.tsx b/src/modules/clusters/cluster-information/components/addonsTable/DriftDialog.tsx new file mode 100644 index 0000000..c351a7a --- /dev/null +++ b/src/modules/clusters/cluster-information/components/addonsTable/DriftDialog.tsx @@ -0,0 +1,141 @@ +import { GitCompare } from "lucide-react"; +import { 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 { Button } from "@/lib/components/ui/inputs/button"; +import { DriftedResource, DriftHistory } from "@/types/addon.types"; + +interface DriftDialogProps { + featureID?: string; + history: DriftHistory; +} + +// 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 the timestamp +const cellClassName = "max-w-none whitespace-nowrap"; + +// kubectl notation: Kind.group, or just Kind for resources in the core group +function formatKind(resource: DriftedResource): string { + return resource.group ? `${resource.kind}.${resource.group}` : resource.kind; +} + +function formatHelmRelease(resource: DriftedResource): string { + if (!resource.helmReleaseName) { + return ""; + } + return resource.helmReleaseNamespace + ? `${resource.helmReleaseNamespace}/${resource.helmReleaseName}` + : resource.helmReleaseName; +} + +function formatDetectedTime(detectedTime: string): string { + const time = new Date(detectedTime); + return `${time.toLocaleDateString("en-US")} ${time.toLocaleTimeString( + "en-US", + { hour: "2-digit", minute: "2-digit", second: "2-digit" }, + )}`; +} + +export const DriftDialog = ({ featureID, history }: DriftDialogProps) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const resources = history.resources ?? []; + const showHelmRelease = resources.some((resource) => + Boolean(resource.helmReleaseName), + ); + + return ( + <> + + + + + + {t("common.drift_history")} + {featureID ? `: ${featureID}` : ""} + + + {t("common.drift_last_detected")}{" "} + {formatDetectedTime(history.lastDetectedTime)} + + + {resources.length === 0 ? ( +

+ {t("common.drift_resources_not_reported")} +

+ ) : ( +
+ + + + {t("common.kind")} + {t("common.namespace")} + {t("common.name")} + {t("common.detected")} + {showHelmRelease && ( + {t("common.helm_release")} + )} + + + + {resources.map((resource, index) => ( + + + {formatKind(resource)} + + + {resource.namespace || "-"} + + + {resource.name} + + + {formatDetectedTime(resource.detectedTime)} + + {showHelmRelease && ( + + {formatHelmRelease(resource)} + + )} + + ))} + +
+
+ )} + {history.truncated && ( +

+ {t("common.drift_truncated")} +

+ )} +
+
+ + ); +}; diff --git a/src/modules/clusters/cluster-information/components/addonsTable/config/Columns.ts b/src/modules/clusters/cluster-information/components/addonsTable/config/Columns.ts index a68afd0..558a499 100644 --- a/src/modules/clusters/cluster-information/components/addonsTable/config/Columns.ts +++ b/src/modules/clusters/cluster-information/components/addonsTable/config/Columns.ts @@ -31,9 +31,9 @@ export const profileColumns: Column[] = [ keys: AddonTableTypes.FAILURE_MESSAGE, }, { - label: "", - className: "", - keys: AddonTableTypes.ACTION, + label: "common.drift", + className: "hidden sm:table-cell", + keys: AddonTableTypes.DRIFT, }, ]; diff --git a/src/modules/clusters/cluster-information/components/addonsTable/rows/ProfileRow.tsx b/src/modules/clusters/cluster-information/components/addonsTable/rows/ProfileRow.tsx index b021bcf..280d56a 100644 --- a/src/modules/clusters/cluster-information/components/addonsTable/rows/ProfileRow.tsx +++ b/src/modules/clusters/cluster-information/components/addonsTable/rows/ProfileRow.tsx @@ -9,29 +9,20 @@ import { FailureMessage } from "@/lib/components/ui/feedback/failureMessage"; import { AddonData } from "@/types/addon.types"; import { AddonColumn, AddonTableTypes } from "@/types/addonTable.types"; import { colorFromStatus, isNotProvisioned } from "@/lib/utils"; -import { Button } from "@/lib/components/ui/inputs/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/lib/components/ui/inputs/dropdown-menu"; -import { ExternalLink, MoreHorizontal } from "lucide-react"; import { McpButton } from "@/lib/components/ui/inputs/mcp-button"; import { useParams } from "react-router-dom"; import { useMcp } from "@/hooks/useMcp"; import { getClusterInfoType } from "@/utils/GetClusterInfoType"; import { ClusterType } from "@/types/cluster.types"; import { useTranslation } from "react-i18next"; +import { DriftDialog } from "@/modules/clusters/cluster-information/components/addonsTable/DriftDialog"; interface ProfileRowProps { row: AddonData; columns: AddonColumn[]; - onOpenRepo?: (url: string | undefined) => void; } -export const ProfileRow = ({ row, columns, onOpenRepo }: ProfileRowProps) => { +export const ProfileRow = ({ row, columns }: ProfileRowProps) => { const { t } = useTranslation(); const { tab: type, name, namespace } = useParams(); const { debugProfileClusterQuery } = useMcp( @@ -124,29 +115,15 @@ export const ProfileRow = ({ row, columns, onOpenRepo }: ProfileRowProps) => { ); } - if (key === AddonTableTypes.ACTION) { + if (key === AddonTableTypes.DRIFT) { return ( - - - - - - Actions - {row.repoURL && ( - onOpenRepo && onOpenRepo(row.repoURL)} - > - repoURL - - )} - Edit - Delete - - + {row.driftHistory && ( + + )} ); } @@ -155,7 +132,9 @@ export const ProfileRow = ({ row, columns, onOpenRepo }: ProfileRowProps) => { {key .split("/") .map((k: string) => ( -
{row[k as keyof AddonData]}
+
+ {row[k as keyof AddonData] as unknown as string} +
)) .filter(Boolean)} diff --git a/src/types/addon.types.ts b/src/types/addon.types.ts index 9553559..fafc770 100644 --- a/src/types/addon.types.ts +++ b/src/types/addon.types.ts @@ -13,8 +13,24 @@ export type AddonTableData = { profiles?: AddonData[]; }; export const addonTypes: AddonTypes[] = Object.values(AddonTypes); +export type DriftedResource = { + group?: string; + kind: string; + namespace?: string; + name: string; + helmReleaseNamespace?: string; + helmReleaseName?: string; + detectedTime: string; +}; +export type DriftHistory = { + lastDetectedTime: string; + resources?: DriftedResource[]; + truncated?: boolean; +}; export type AddonData = { failureMessage?: string; + featureID?: string; + driftHistory?: DriftHistory; icon?: string; lastAppliedTime?: string; status?: string; diff --git a/src/types/addonTable.types.ts b/src/types/addonTable.types.ts index 4c9fda4..3e21772 100644 --- a/src/types/addonTable.types.ts +++ b/src/types/addonTable.types.ts @@ -11,6 +11,7 @@ export enum AddonTableTypes { ACTION = "action", ICON = "icon", FAILURE_MESSAGE = "failureMessage", + DRIFT = "driftHistory", } export interface AddonColumn { label: string;