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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ IMAGE_NAME ?= dashboard
ARCH ?= $(shell go env GOARCH)
OS ?= $(shell uname -s | tr A-Z a-z)
export CONTROLLER_IMG ?= $(REGISTRY)/$(IMAGE_NAME)
TAG ?= v1.16.0
TAG ?= main

# Directories.
TOOLS_DIR := hack/tools
Expand Down
4 changes: 2 additions & 2 deletions manifest/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ spec:
spec:
containers:
- name: dashboard
image: docker.io/projectsveltos/dashboard:v1.16.0
image: docker.io/projectsveltos/dashboard:main
ports:
- containerPort: 5173
env:
- name: VITE_APP_VERSION
value: v1.16.0
value: main
---
apiVersion: v1
kind: Service
Expand Down
1 change: 1 addition & 0 deletions src/api-client/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const API_ENDPOINTS = {
RESOURCES: "/resources",
HELM_CHART: "/helmcharts",
PROFILE_STATUSES: "/getClusterStatus",
CLUSTER_HEALTH: "/clusterHealth",
CAPI_CLUSTERS: "/capiclusters",
SVELTOS_CLUSTERS: "/sveltosclusters",
PROFILES: "/profiles",
Expand Down
77 changes: 77 additions & 0 deletions src/lib/components/ui/data-display/health-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { MouseEvent } from "react";
import {
CheckCircle2,
CircleDashed,
LucideIcon,
PauseCircle,
Timer,
XCircle,
} from "lucide-react";
import { useTranslation } from "react-i18next";

import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/lib/components/ui/data-display/tooltip";
import { HealthStatus } from "@/types/health.types";
import { healthStatusStyle } from "@/utils/healthStatus";

const icons: Record<HealthStatus, LucideIcon> = {
Healthy: CheckCircle2,
Progressing: Timer,
Suspended: PauseCircle,
Degraded: XCircle,
NoData: CircleDashed,
};

interface HealthBadgeProps {
// Nothing is shown when the status is unknown: the resources are not monitored.
status?: HealthStatus;
// When set the badge is a button, for instance to open the unhealthy resources.
onClick?: () => void;
}

// HealthBadge shows the health reported by health monitoring: the resources a profile deploys and any other
// HealthCheck. It is shown for a monitored cluster or profile only (a profile with monitoring enabled, or a
// cluster with health reports), so a missing badge means "not monitored", never "healthy".
export const HealthBadge = ({ status, onClick }: HealthBadgeProps) => {
const { t } = useTranslation();
if (!status) {
return null;
}

const Icon = icons[status];
const style = healthStatusStyle[status];
const icon = <Icon className={`h-3.5 w-3.5 shrink-0 ${style.textClass}`} />;

const handleClick = (event: MouseEvent) => {
// The badge sits in clickable cards and rows
event.stopPropagation();
onClick?.();
};

return (
<Tooltip>
<TooltipTrigger asChild>
{onClick ? (
<button
type="button"
className="inline-flex cursor-pointer"
aria-label={t("common.health_tooltip", {
status: t(style.labelKey),
})}
onClick={handleClick}
>
{icon}
</button>
) : (
<span className="inline-flex cursor-default">{icon}</span>
)}
</TooltipTrigger>
<TooltipContent>
<p>{t("common.health_tooltip", { status: t(style.labelKey) })}</p>
</TooltipContent>
</Tooltip>
);
};
35 changes: 35 additions & 0 deletions src/lib/components/ui/data-display/health-status-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useTranslation } from "react-i18next";

import { Badge } from "@/lib/components/ui/data-display/badge";
import { HealthStatus } from "@/types/health.types";
import { healthStatusStyle } from "@/utils/healthStatus";

interface HealthStatusLabelProps {
status: HealthStatus;
// When set the label is a button, for instance to open the unhealthy resources.
onClick?: () => void;
}

// HealthStatusLabel shows a health status as a coloured badge with its name.
export const HealthStatusLabel = ({
status,
onClick,
}: HealthStatusLabelProps) => {
const { t } = useTranslation();
const style = healthStatusStyle[status];
const badge = <Badge className={style.badgeClass}>{t(style.labelKey)}</Badge>;

if (!onClick) {
return badge;
}

return (
<button
type="button"
className="inline-flex cursor-pointer"
onClick={onClick}
>
{badge}
</button>
);
};
15 changes: 14 additions & 1 deletion src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@
"helm_release": "إصدار Helm",
"drift_history": "سجل الانحراف",
"drift_last_detected": "آخر انحراف تم اكتشافه في",
"detected": "تم الاكتشاف"
"detected": "تم الاكتشاف",
"health_healthy": "سليم",
"health_progressing": "قيد التقدم",
"health_degraded": "متدهور",
"health_suspended": "معلق",
"health_no_data": "لا توجد بيانات",
"health_tooltip": "مراقبة الصحة: {{status}}",
"health_message": "الرسالة",
"health_check": "HealthCheck: {{name}}",
"health_not_enabled": "لا شيء قيد المراقبة على هذا الكلاستر: لا يوجد بروفايل مفعّل لمراقبة الصحة ولا توجد تقارير صحة.",
"health_no_data_yet": "مراقبة الصحة مفعّلة، لكن لم يتم استلام أي تقرير بعد.",
"health_all_healthy": "جميع الموارد المراقبة سليمة.",
"health_no_match": "لا توجد موارد غير سليمة تطابق عوامل التصفية.",
"health": "الصحة"
}
}
15 changes: 14 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@
"helm_release": "Helm release",
"drift_history": "Drift history",
"drift_last_detected": "Last drift detected at",
"detected": "Detected"
"detected": "Detected",
"health_healthy": "Healthy",
"health_progressing": "Progressing",
"health_degraded": "Degraded",
"health_suspended": "Suspended",
"health_no_data": "No data",
"health_tooltip": "Health monitoring: {{status}}",
"health_message": "Message",
"health_check": "HealthCheck: {{name}}",
"health_not_enabled": "Nothing is monitored on this cluster: no profile has health monitoring enabled and there are no health reports.",
"health_no_data_yet": "Health monitoring is enabled, but no report has been received yet.",
"health_all_healthy": "All the monitored resources are healthy.",
"health_no_match": "No unhealthy resources match the filters.",
"health": "Health"
}
}
15 changes: 14 additions & 1 deletion src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,19 @@
"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"
"detected": "Détectée",
"health_healthy": "Sain",
"health_progressing": "En cours",
"health_degraded": "Dégradé",
"health_suspended": "Suspendu",
"health_no_data": "Aucune donnée",
"health_tooltip": "Surveillance de santé : {{status}}",
"health_message": "Message",
"health_check": "HealthCheck : {{name}}",
"health_not_enabled": "Rien n'est surveillé sur ce cluster : aucun profil n'a activé la surveillance de santé et aucun rapport de santé n'existe.",
"health_no_data_yet": "La surveillance de santé est activée, mais aucun rapport n'a encore été reçu.",
"health_all_healthy": "Toutes les ressources surveillées sont saines.",
"health_no_match": "Aucune ressource défaillante ne correspond aux filtres.",
"health": "Santé"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "react-router-dom";

import { appConfig } from "@/config/app";
import { ClusterType } from "@/types/cluster.types";
import { HealthResourcesTable } from "@/modules/health/HealthResourcesTable";
import { clusterHealthColumns } from "@/modules/health/healthColumns";
import { healthSearchConfig } from "@/modules/health/healthSearchConfig";
import { useClusterHealth } from "@/modules/clusters/cluster-information/hooks/useClusterHealth";
import { pickSearchParams } from "@/utils/pickSearchParams";

// ClusterHealthView lists the resources of the cluster of the page which are not healthy. The filters live in
// the URL, like the other addons tabs: opened from the health badge of a profile, the profile filter is already
// set, and clearing it lists the unhealthy resources of every profile.
export function ClusterHealthView() {
const { tab: clusterType, name, namespace } = useParams();
const [searchParams] = useSearchParams();
const [page, setPage] = useState(appConfig.defaultPage);

const filters = pickSearchParams(healthSearchConfig, searchParams);
const filtersKey = JSON.stringify(filters);

// Another page of the previous filters would make no sense
useEffect(() => {
setPage(appConfig.defaultPage);
}, [filtersKey]);

const query = useClusterHealth(
namespace ?? "",
name ?? "",
clusterType as ClusterType,
page,
filters,
);

return (
<HealthResourcesTable
rows={query.data?.resources ?? []}
total={query.data?.totalResources ?? 0}
state={query.data?.state}
hasFilters={Object.keys(filters).length > 0}
loading={query.isLoading}
page={page}
onPageChange={setPage}
columns={clusterHealthColumns}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,49 @@ import {
} from "@/lib/components/ui/navigation/tabs";

import { AddonsTable } from "@/modules/clusters/cluster-information/components/addonsTable/AddonsTable";
import { useState } from "react";
import { useSearchParams } from "react-router-dom";

import { AddonTableData, addonTypes, AddonTypes } from "@/types/addon.types";
import {
AddonTableData,
addonTypes,
AddonTypes,
TableAddonTypes,
} from "@/types/addon.types";
import { appConfig } from "@/config/app";
import { SearchQueryParamInput } from "@/lib/components/ui/inputs/SearchQueryParamInput";
import { typeConfig } from "@/modules/clusters/cluster-information/components/addonsTable/config/typeConfig";
import { hasSearchConfig } from "@/modules/clusters/cluster-information/components/addonsTable/utils/addonsTableUtils";
import { getSearchConfig } from "@/modules/clusters/cluster-information/components/addonsTable/utils/addonsTableUtils";
import { ClusterHealthView } from "@/modules/clusters/cluster-information/components/ClusterHealthView";
import { omitSearchParams } from "@/utils/pickSearchParams";
import {
ADDONS_TAB_PARAM,
addonTabFromParam,
addonTabSlug,
} from "@/utils/addonTabs";
import { useTranslation } from "react-i18next";

interface ResourceTableProps {
addonsData: Record<AddonTypes, AddonTableData>;
addonsData: Record<TableAddonTypes, AddonTableData>;
loading: boolean;

setPage: (page: number, type: AddonTypes) => void;
setPage: (page: number, type: TableAddonTypes) => void;
}

export function Addons({ addonsData, setPage, loading }: ResourceTableProps) {
const { t } = useTranslation();
/* By default, we show helm charts , this will be hardcoded for now */
const [activeTab, setActiveTab] = useState<AddonTypes>(AddonTypes.HELM);
/* The tab is in the URL, so it can be linked to. By default, we show helm charts */
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = addonTabFromParam(searchParams.get(ADDONS_TAB_PARAM));

const handleChangeTab = (tab: AddonTypes) => {
setActiveTab(tab);
setPage(appConfig.defaultPage, tab);
// The filters belong to the tab being left: some tabs share filters (a profile health badge opens the
// unhealthy resources already filtered by profile), which must not follow the user back to the Profiles tab
const next = omitSearchParams(getSearchConfig(activeTab), searchParams);
next.set(ADDONS_TAB_PARAM, addonTabSlug(tab));
setSearchParams(next, { replace: true });
// The unhealthy resources tab keeps its own page
if (tab !== AddonTypes.UNHEALTHY) {
setPage(appConfig.defaultPage, tab);
}
};
return (
<div className="grid auto-rows-max items-start">
Expand Down Expand Up @@ -79,35 +98,29 @@ export function Addons({ addonsData, setPage, loading }: ResourceTableProps) {
<CardContent>
<TabsList>
{addonTypes.map((tab: AddonTypes) => (
<TabsTrigger
key={tab}
value={tab}
onClick={() => setActiveTab(tab)}
>
<TabsTrigger key={tab} value={tab}>
{tab}
</TabsTrigger>
))}
</TabsList>

{/* Remounted per tab, so its fields start from the URL (a badge can open a tab already filtered) */}
<SearchQueryParamInput
searchConfig={
hasSearchConfig(typeConfig[activeTab])
? (
typeConfig[activeTab] as {
searchConfig: { key: string; placeholder: string }[];
}
).searchConfig
: []
}
key={activeTab}
searchConfig={getSearchConfig(activeTab)}
/>
{addonTypes.map((type: AddonTypes) => (
<TabsContent key={type} value={type}>
<AddonsTable
type={type}
loading={loading}
setPage={setPage}
data={addonsData[type]}
/>
{type === AddonTypes.UNHEALTHY ? (
<ClusterHealthView />
) : (
<AddonsTable
type={type}
loading={loading}
setPage={setPage}
data={addonsData[type]}
/>
)}
</TabsContent>
))}
</CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import { useTranslation } from "react-i18next";
import { FC, useState } from "react";
import { getRowsAndTotal } from "@/modules/clusters/cluster-information/components/addonsTable/utils/addonsTableUtils";
import { typeConfig } from "@/modules/clusters/cluster-information/components/addonsTable/config/typeConfig";
import { AddonTableData, AddonTypes } from "@/types/addon.types";
import { AddonTableData, TableAddonTypes } from "@/types/addon.types";

interface AddonsTableProps {
data: AddonTableData;
type: AddonTypes;
setPage: (page: number, type: AddonTypes) => void;
type: TableAddonTypes;
setPage: (page: number, type: TableAddonTypes) => void;
loading: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export const DriftDialog = ({ featureID, history }: DriftDialogProps) => {
<Button
variant="outline"
size="sm"
// Only shown when a drift was detected: amber, like the other warnings, so it stands out
className="border-amber-500 bg-amber-50 text-amber-700 hover:bg-amber-100 hover:text-amber-800 dark:border-amber-400 dark:bg-amber-950 dark:text-amber-300 dark:hover:bg-amber-900 dark:hover:text-amber-200"
title={formatDetectedTime(history.lastDetectedTime)}
onClick={() => setOpen(true)}
>
Expand Down
Loading
Loading