Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "تم الاكتشاف"
}
}
9 changes: 8 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
9 changes: 8 additions & 1 deletion src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Button
variant="outline"
size="sm"
title={formatDetectedTime(history.lastDetectedTime)}
onClick={() => setOpen(true)}
>
<GitCompare className="w-4 h-4 mr-1" />
{t("common.drift_history")}
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-5xl">
<DialogHeader>
<DialogTitle>
{t("common.drift_history")}
{featureID ? `: ${featureID}` : ""}
</DialogTitle>
<DialogDescription>
{t("common.drift_last_detected")}{" "}
{formatDetectedTime(history.lastDetectedTime)}
</DialogDescription>
</DialogHeader>
{resources.length === 0 ? (
<p className="text-sm text-muted-foreground">
{t("common.drift_resources_not_reported")}
</p>
) : (
<div className={tableContainerClassName}>
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("common.kind")}</TableHead>
<TableHead>{t("common.namespace")}</TableHead>
<TableHead>{t("common.name")}</TableHead>
<TableHead>{t("common.detected")}</TableHead>
{showHelmRelease && (
<TableHead>{t("common.helm_release")}</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{resources.map((resource, index) => (
<TableRow key={index}>
<TableCell className={cellClassName}>
{formatKind(resource)}
</TableCell>
<TableCell className={cellClassName}>
{resource.namespace || "-"}
</TableCell>
<TableCell className="max-w-md break-words whitespace-normal">
{resource.name}
</TableCell>
<TableCell className={cellClassName}>
{formatDetectedTime(resource.detectedTime)}
</TableCell>
{showHelmRelease && (
<TableCell className={cellClassName}>
{formatHelmRelease(resource)}
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{history.truncated && (
<p className="text-xs text-muted-foreground">
{t("common.drift_truncated")}
</p>
)}
</DialogContent>
</Dialog>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -124,29 +115,15 @@ export const ProfileRow = ({ row, columns, onOpenRepo }: ProfileRowProps) => {
</TableCell>
);
}
if (key === AddonTableTypes.ACTION) {
if (key === AddonTableTypes.DRIFT) {
return (
<TableCell key={colIndex} className={column.className}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup="true" size="icon" variant="ghost">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
{row.repoURL && (
<DropdownMenuItem
onSelect={() => onOpenRepo && onOpenRepo(row.repoURL)}
>
<ExternalLink className={"w-4 h-4 mx-1"} /> repoURL
</DropdownMenuItem>
)}
<DropdownMenuItem disabled>Edit</DropdownMenuItem>
<DropdownMenuItem disabled>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{row.driftHistory && (
<DriftDialog
featureID={row.featureID}
history={row.driftHistory}
/>
)}
</TableCell>
);
}
Expand All @@ -155,7 +132,9 @@ export const ProfileRow = ({ row, columns, onOpenRepo }: ProfileRowProps) => {
{key
.split("/")
.map((k: string) => (
<div key={k}>{row[k as keyof AddonData]}</div>
<div key={k}>
{row[k as keyof AddonData] as unknown as string}
</div>
))
.filter(Boolean)}
</TableCell>
Expand Down
16 changes: 16 additions & 0 deletions src/types/addon.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/types/addonTable.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum AddonTableTypes {
ACTION = "action",
ICON = "icon",
FAILURE_MESSAGE = "failureMessage",
DRIFT = "driftHistory",
}
export interface AddonColumn {
label: string;
Expand Down
Loading