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 (
+ <>
+
+
+ >
+ );
+};
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 (