From dd8f354d20f8eb29f522e93671168b1800a2b619 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Wed, 28 Aug 2024 10:19:30 +0300 Subject: [PATCH] fix: show two columns, when the other one is empty fix: show two columns when no none headline properties chore: remove console.log chore: update spacing fix: show two property columns when no analytics fix: fix issues with cards styling fix: scroll issues fix: refactor and re-sort components statuses chore: remove console logs --- src/components/Canary/HealthChecksSummary.tsx | 59 --- .../Incidents/IncidentCardSummary/index.tsx | 91 ---- src/components/StatusLine/StatusLine.tsx | 18 +- .../Topology/TopologyCard/CardMetrics.tsx | 4 + .../Topology/TopologyCard/Property.tsx | 1 - .../TopologyCard/TopologCardStatuses.tsx | 409 ++++++++++++++++++ .../Topology/TopologyCard/TopologyCard.tsx | 198 +++++---- .../TopologyCardPropertiesColumn.tsx | 10 +- .../TopologyConfigAnalysisLine.tsx | 92 ---- .../TopologyCard/Utils/FormatProperties.tsx | 78 ++-- src/ui/CustomScroll/index.tsx | 8 +- 11 files changed, 586 insertions(+), 382 deletions(-) delete mode 100644 src/components/Canary/HealthChecksSummary.tsx delete mode 100644 src/components/Incidents/IncidentCardSummary/index.tsx create mode 100644 src/components/Topology/TopologyCard/TopologCardStatuses.tsx delete mode 100644 src/components/Topology/TopologyCard/TopologyConfigAnalysisLine.tsx diff --git a/src/components/Canary/HealthChecksSummary.tsx b/src/components/Canary/HealthChecksSummary.tsx deleted file mode 100644 index 34f7a850d..000000000 --- a/src/components/Canary/HealthChecksSummary.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useMemo } from "react"; -import { AiFillHeart } from "react-icons/ai"; -import { StatusLine, StatusLineProps } from "../StatusLine/StatusLine"; - -type HealthChecksSummaryProps = React.HTMLProps & { - target?: string; - checks?: { - healthy: number; - warning: number; - unhealthy: number; - }; -}; - -export function HealthChecksSummary({ - checks, - target = "", - ...rest -}: HealthChecksSummaryProps) { - const statusLineInfo = useMemo(() => { - if (!checks) { - return null; - } - - const data: StatusLineProps = { - label: "Health Checks", - icon: , - url: "/health", - statuses: [ - ...(checks.healthy > 0 - ? [ - { - label: checks.healthy.toString(), - color: "green" as const - } - ] - : []), - ...(checks.unhealthy > 0 - ? [ - { - label: checks.unhealthy.toString(), - color: "red" as const - } - ] - : []), - ...(checks.warning > 0 - ? [{ label: checks.warning, color: "orange" as const }] - : []) - ] - }; - - return data; - }, [checks]); - - if (!checks || !Object.entries(checks).length || !statusLineInfo) { - return null; - } - - return ; -} diff --git a/src/components/Incidents/IncidentCardSummary/index.tsx b/src/components/Incidents/IncidentCardSummary/index.tsx deleted file mode 100644 index 81e44408a..000000000 --- a/src/components/Incidents/IncidentCardSummary/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useMemo } from "react"; -import { IncidentSeverity } from "../../../api/types/incident"; - -import { Topology } from "../../../api/types/topology"; -import { StatusLine, StatusLineProps } from "../../StatusLine/StatusLine"; -import { severityItems, typeItems } from "../data"; - -const chipColorFromSeverity = ( - severity: IncidentSeverity -): "green" | "orange" | "red" | "gray" => { - switch (severity) { - case IncidentSeverity.Low: - case IncidentSeverity.Medium: - return "green"; - case IncidentSeverity.High: - return "orange"; - case IncidentSeverity.Critical: - case IncidentSeverity.Blocker: - return "red"; - default: - return "green"; - } -}; - -type IncidentSummaryTypes = keyof typeof typeItems; - -type IncidentCardSummaryProps = { - target?: string; - topology: Pick; -}; - -export default function IncidentCardSummary({ - topology, - target = "" -}: IncidentCardSummaryProps) { - const statusLines: StatusLineProps[] = useMemo(() => { - const incidentSummary = Object.entries(topology?.summary?.incidents || {}); - return incidentSummary.map(([key, summary]) => { - // For presentation purposes, we combine Low and Medium into one - const { Low, ...rest } = summary; - const summaryWithLowMediumCombined = { - ...rest, - Medium: (summary.Medium ?? 0) + (summary.Low ?? 0) - }; - const statusLine: StatusLineProps = { - icon: typeItems[key as IncidentSummaryTypes].icon, - label: typeItems[key as IncidentSummaryTypes].description, - url: `/incidents?type=${key}&component=${topology.id}`, - statuses: [] - }; - const type = typeItems[key as IncidentSummaryTypes].value; - Object.entries(summaryWithLowMediumCombined).forEach( - ([key, value], i) => { - if (value <= 0) { - return; - } - const severityObject = - Object.values(severityItems).find( - (values) => values.value === key - ) || severityItems.Low; - const item = { - label: value.toString(), - color: chipColorFromSeverity(key as IncidentSeverity), - url: `/incidents?severity=${severityObject.value}&component=${topology.id}&type=${type}` - }; - statusLine.statuses.push(item); - } - ); - return statusLine; - }); - }, [topology]); - - if (!topology.summary?.incidents) { - return null; - } - - return ( - <> - {statusLines.map((statusLine, index) => { - return ( - - ); - })} - - ); -} diff --git a/src/components/StatusLine/StatusLine.tsx b/src/components/StatusLine/StatusLine.tsx index ec0cfed48..618d9ede9 100644 --- a/src/components/StatusLine/StatusLine.tsx +++ b/src/components/StatusLine/StatusLine.tsx @@ -20,15 +20,21 @@ export type StatusLineData = { export type StatusLineProps = React.HTMLProps & StatusLineData; -const renderIcon = (icon: string | React.ReactNode) => { +interface RenderIconProps { + icon: string | React.ReactNode; +} + +const RenderIcon: React.FC = ({ icon }) => { if (!icon) { return null; } if (typeof icon === "object") { - return icon; + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{icon}; } else if (typeof icon === "string") { - return ; + return ; } + return null; }; const StatusInfoEntry = ({ @@ -46,14 +52,14 @@ const StatusInfoEntry = ({ to={statusInfo.url} target={target || ""} > - {statusInfo.icon && renderIcon(statusInfo.icon)} + {statusInfo.icon && } ); } else { return ( - {statusInfo.icon && renderIcon(statusInfo.icon)} + {statusInfo.icon && } ); @@ -74,7 +80,7 @@ export function StatusLine({ className={clsx("flex flex-row items-center space-x-1", className)} {...rest} > - {icon && renderIcon(icon)} + {icon && } {url && ( { + if (!items || items.length === 0) { + return null; + } + return (
{items.map((item) => ( diff --git a/src/components/Topology/TopologyCard/Property.tsx b/src/components/Topology/TopologyCard/Property.tsx index 1e578a37e..0f7af977d 100644 --- a/src/components/Topology/TopologyCard/Property.tsx +++ b/src/components/Topology/TopologyCard/Property.tsx @@ -14,7 +14,6 @@ export function PropertyDisplay({ className = "", ...props }: PropertyDisplayProps) { - console.log("property display", property); const { name, icon, color } = property; const label = NodePodPropToLabelMap[name as keyof typeof NodePodPropToLabelMap] || name; diff --git a/src/components/Topology/TopologyCard/TopologCardStatuses.tsx b/src/components/Topology/TopologyCard/TopologCardStatuses.tsx new file mode 100644 index 000000000..4c2664aac --- /dev/null +++ b/src/components/Topology/TopologyCard/TopologCardStatuses.tsx @@ -0,0 +1,409 @@ +import { IncidentSeverity } from "@flanksource-ui/api/types/incident"; +import { Topology } from "@flanksource-ui/api/types/topology"; +import { InsightTypeToIcon } from "@flanksource-ui/components/Configs/Insights/ConfigInsightsIcon"; +import { + severityItems, + typeItems +} from "@flanksource-ui/components/Incidents/data"; +import { + StatusInfo, + StatusLine, + StatusLineProps +} from "@flanksource-ui/components/StatusLine/StatusLine"; +import { CustomScroll } from "@flanksource-ui/ui/CustomScroll"; +import clsx from "clsx"; +import { useMemo } from "react"; +import { AiFillHeart } from "react-icons/ai"; +import { MdOutlineInsights } from "react-icons/md"; + +const severityToColorMap = (severity: string) => { + if (severity === "critical") { + return "red"; + } + if (severity === "high") { + return "orange"; + } + if (severity === "warning") { + return "yellow"; + } + if (severity === "info") { + return "gray"; + } + if (severity === "low") { + return "green"; + } + if (severity === "medium") { + return "green"; + } + return "gray"; +}; + +const chipColorFromSeverity = ( + severity: IncidentSeverity +): "green" | "orange" | "red" | "gray" => { + switch (severity) { + case IncidentSeverity.Low: + case IncidentSeverity.Medium: + return "green"; + case IncidentSeverity.High: + return "orange"; + case IncidentSeverity.Critical: + case IncidentSeverity.Blocker: + return "red"; + default: + return "green"; + } +}; + +function getStatuses(summary?: Topology["summary"], url?: string) { + if (!summary) { + return []; + } + const statuses: StatusInfo[] = []; + if (summary.healthy && summary.healthy > 0) { + statuses.push({ + url: url ? `${url}?status=healthy` : "", + label: summary.healthy.toString(), + color: "green" + }); + } + if (summary.unhealthy && summary.unhealthy > 0) { + statuses.push({ + url: url ? `${url}?status=unhealthy` : "", + label: summary.unhealthy.toString(), + color: "red" + }); + } + if (summary.warning && summary.warning > 0) { + statuses.push({ + url: url ? `${url}?status=warning` : "", + label: summary.warning.toString(), + color: "orange" + }); + } + if (summary.unknown && summary.unknown > 0) { + statuses.push({ + url: url ? `${url}?status=unknown` : "", + label: summary.unknown.toString(), + color: "gray" + }); + } + return statuses; +} + +function insightStatuses(topology: Topology): StatusLineProps | undefined { + const insights = topology?.summary?.insights; + if (!insights) { + return undefined; + } + + const analysisToCountMap = Object.entries(insights) + .map(([type, severityMap]) => { + const severityMapWithLowMediumCombined = { + ...severityMap, + medium: (severityMap.medium ?? 0) + (severityMap.low ?? 0), + low: undefined + }; + + return Object.entries(severityMapWithLowMediumCombined) + .filter(([_, count]) => count) + .map(([severity, count], index) => { + const icon = + index === 0 ? : null; + const label = count ?? 0; + // const key = `${type}-${severity}`; + const url = `/catalog/insights?component=${topology.id}&type=${type}&severity=${severity}`; + return { + color: severityToColorMap(severity as IncidentSeverity), + icon, + label, + url + } satisfies StatusInfo; + }); + }) + .flatMap((x) => x); + + return { + icon: , + label: "Insights", + url: `/catalog/insights?component=${topology.id}`, + statuses: Object.values(analysisToCountMap) + }; +} + +// checked and looks good +function getTopologyHealthStatuses(topology: Topology) { + if (!topology?.summary?.checks) { + return undefined; + } + + const checks = topology.summary.checks; + + const data: StatusLineProps = { + label: "Health Checks", + icon: , + url: "/health", + statuses: [ + ...(checks.healthy > 0 + ? [ + { + label: checks.healthy.toString(), + color: "green" as const + } + ] + : []), + ...(checks.unhealthy > 0 + ? [ + { + label: checks.unhealthy.toString(), + color: "red" as const + } + ] + : []), + ...(checks.warning > 0 + ? [{ label: checks.warning, color: "orange" as const }] + : []) + ] + }; + + return data; +} + +function getTopologyHealthSummary( + topology: Topology, + viewType: "individual_level" | "children_level" +) { + const data: StatusLineProps = { + icon: "", + label: "", + url: "", + statuses: [] + }; + const childrenSummary = { + healthy: 0, + unhealthy: 0, + warning: 0 + }; + topology.components?.forEach((component) => { + childrenSummary.healthy += component.summary?.checks?.healthy || 0; + childrenSummary.unhealthy += component.summary?.checks?.unhealthy || 0; + childrenSummary.warning += component.summary?.checks?.warning || 0; + }); + const noSummary = !( + childrenSummary.healthy || + childrenSummary.unhealthy || + childrenSummary.warning + ); + if (viewType === "individual_level") { + data.icon = topology.icon; + data.label = topology.name; + data.url = `/topology/${topology.id}`; + data.statuses = getStatuses(topology?.summary, `/topology/${topology.id}`); + } else { + data.label = `Health Summary: ${noSummary ? "NA" : ""}`; + data.statuses = getStatuses(childrenSummary); + } + return data; +} + +function topologiesHealthSummaries(topology: Topology) { + return topology?.components + ?.sort((a, b) => { + // we want to move unhealthy components to the top, then warning, then healthy + if (a.status === "unhealthy" && b.status !== "unhealthy") { + return -1; + } + if (a.status === "warning" && b.status === "healthy") { + return -1; + } + if (a.status === "healthy" && b.status !== "healthy") { + return 1; + } + return 0; + }) + .map((component) => + getTopologyHealthSummary(component, "individual_level") + ); +} + +// checked and looks good +function incidentsStatuses(topology: Topology) { + type IncidentSummaryTypes = keyof typeof typeItems; + + const incidentSummary = Object.entries(topology?.summary?.incidents || {}); + return incidentSummary.map(([key, summary]) => { + // For presentation purposes, we combine Low and Medium into one + const { Low, ...rest } = summary; + const summaryWithLowMediumCombined = { + ...rest, + Medium: (summary.Medium ?? 0) + (summary.Low ?? 0) + }; + const statusLine: StatusLineProps = { + icon: typeItems[key as IncidentSummaryTypes].icon, + label: typeItems[key as IncidentSummaryTypes].description, + url: `/incidents?type=${key}&component=${topology.id}`, + statuses: [] + }; + const type = typeItems[key as IncidentSummaryTypes].value; + Object.entries(summaryWithLowMediumCombined).forEach(([key, value], i) => { + if (value <= 0) { + return; + } + const severityObject = + Object.values(severityItems).find((values) => values.value === key) || + severityItems.Low; + const item = { + label: value.toString(), + color: chipColorFromSeverity(key as IncidentSeverity), + url: `/incidents?severity=${severityObject.value}&component=${topology.id}&type=${type}` + }; + statusLine.statuses.push(item); + }); + return statusLine; + }); +} + +type TopologyCardStatusesProps = { + topology?: Topology; + isPropertiesPanelEmpty: boolean; +}; + +export default function TopologyCardStatuses({ + topology, + isPropertiesPanelEmpty = false +}: TopologyCardStatusesProps) { + const statusLines = useMemo(() => { + if (!topology) { + return []; + } + + const healthSummary = + getTopologyHealthSummary(topology, "individual_level") ?? []; + const healthChecks = getTopologyHealthStatuses(topology); + const insights = insightStatuses(topology); + const incidents = incidentsStatuses(topology) ?? []; + const components = topologiesHealthSummaries(topology) ?? []; + + return ( + [healthSummary, healthChecks, insights, ...incidents, ...components] + .filter((status) => status) + // remove empty statuses + .filter((status) => status!.statuses.length > 0) + .sort((a, b) => { + // count the number of red statuses + // if a has more red statuses than b, a should come first + // if b has more red statuses than a, b should come first + // if they have the same number of red statuses, compare the number of orange statuses + // if a has more orange statuses than b, a should come first + // if b has more orange statuses than a, b should come first + // if they have the same number of orange statuses, sort by + // alphabetical + const aRed = parseInt( + a?.statuses + ?.find((status) => status.color === "red") + ?.label.toString() ?? "0" + ); + + const bRed = parseInt( + b?.statuses + ?.find((status) => status.color === "red") + ?.label.toString() ?? "0" + ); + + if (aRed > bRed) { + return -1; + } + + if (aRed < bRed) { + return 1; + } + + const aOrange = parseInt( + a?.statuses + .find((status) => status.color === "orange") + ?.label.toString() ?? "0" + ); + + const bOrange = parseInt( + b?.statuses + .find((status) => status.color === "orange") + ?.label.toString() ?? "0" + ); + + if (aOrange > bOrange) { + return -1; + } + + if (aOrange < bOrange) { + return 1; + } + + const aYellow = parseInt( + a?.statuses + .find((status) => status.color === "red") + ?.label.toString() ?? "0" + ); + + const bYellow = parseInt( + b?.statuses + .find((status) => status.color === "red") + ?.label.toString() ?? "0" + ); + + if (aYellow > bYellow) { + return -1; + } + + if (aYellow < bYellow) { + return 1; + } + + const aGray = parseInt( + a?.statuses + .find((status) => status.color === "gray") + ?.label.toString() ?? "0" + ); + + const bGray = parseInt( + b?.statuses + .find((status) => status.color === "gray") + ?.label.toString() ?? "0" + ); + + if (aGray > bGray) { + return -1; + } + + if (aGray < bGray) { + return 1; + } + + return a!.label.localeCompare(b!.label); + }) as StatusLineProps[] + ); + + // we want to take all status and add them to a list + // then sort by unhealthy, then alphabetical order + }, [topology]); + + if (statusLines?.length === 0) { + return null; + } + + return ( + + {statusLines.map((status, index) => ( + + ))} + + ); +} diff --git a/src/components/Topology/TopologyCard/TopologyCard.tsx b/src/components/Topology/TopologyCard/TopologyCard.tsx index cc5eb073d..1ea20f68a 100644 --- a/src/components/Topology/TopologyCard/TopologyCard.tsx +++ b/src/components/Topology/TopologyCard/TopologyCard.tsx @@ -1,20 +1,17 @@ import { getTopology } from "@flanksource-ui/api/services/topology"; import { Topology } from "@flanksource-ui/api/types/topology"; import { Size } from "@flanksource-ui/types"; -import { CustomScroll } from "@flanksource-ui/ui/CustomScroll"; import { Icon } from "@flanksource-ui/ui/Icons/Icon"; import TopologyCardSkeletonLoader from "@flanksource-ui/ui/SkeletonLoader/TopologyCardSkeletonLoader"; import { useQuery } from "@tanstack/react-query"; import clsx from "clsx"; +import { isEmpty } from "lodash"; import { MouseEventHandler, useMemo } from "react"; import { Link, useParams, useSearchParams } from "react-router-dom"; import AgentName from "../../Agents/AgentName"; -import { HealthChecksSummary } from "../../Canary/HealthChecksSummary"; -import { HealthSummary } from "../../Canary/HealthSummary"; -import IncidentCardSummary from "../../Incidents/IncidentCardSummary"; import { CardMetrics } from "./CardMetrics"; +import TopologyCardStatuses from "./TopologCardStatuses"; import TopologyCardPropertiesColumn from "./TopologyCardPropertiesColumn"; -import { TopologyConfigAnalysisLine } from "./TopologyConfigAnalysisLine"; import { TopologyDropdownMenu } from "./TopologyDropdownMenu"; export enum ComponentHealth { @@ -85,29 +82,13 @@ export function TopologyCard({ [size] ); - const canShowChildHealth = () => { - let totalCount = 0; - if (topology?.summary?.checks) { - topology.summary.checks.healthy = topology.summary.checks.healthy || 0; - topology.summary.checks.unhealthy = - topology.summary.checks.unhealthy || 0; - topology.summary.checks.warning = topology.summary.checks.warning || 0; - Object.keys(topology.summary.checks).forEach((key) => { - totalCount += - topology.summary?.checks?.[key as keyof Topology["summary"]] || 0; - }); + const topologyLink = useMemo(() => { + if (!topology) { + return undefined; } - return ( - !topology?.components?.length && - (!topology?.is_leaf || (topology.is_leaf && totalCount !== 1)) - ); - }; - - const prepareTopologyLink = (topologyItem: Topology) => { - if (topologyItem.id === parentId && parentId) { - return ""; + if (topology.id === parentId && parentId) { + return undefined; } - const params = Object.fromEntries(searchParams.entries()); delete params.refererId; delete params.status; @@ -117,43 +98,77 @@ export function TopologyCard({ }) .join("&"); - const parentIdAsPerPath = (topologyItem.path || "").split(".").pop(); - return `/topology/${topologyItem.id}?${queryString}${ - parentId && parentIdAsPerPath !== parentId && parentId !== topologyItem.id + const parentIdAsPerPath = (topology.path || "").split(".").pop(); + return `/topology/${topology.id}?${queryString}${ + parentId && parentIdAsPerPath !== parentId && parentId !== topology.id ? `&refererId=${parentId}` : "" }`; - }; + }, [parentId, searchParams, topology]); - const sortedTopologyComponents = useMemo( - () => - topology?.components?.sort((a, b) => { - // we want to move unhealthy components to the top, then warning, then healthy - if (a.status === "unhealthy" && b.status !== "unhealthy") { - return -1; - } - if (a.status === "warning" && b.status === "healthy") { - return -1; - } - if (a.status === "healthy" && b.status !== "healthy") { - return 1; - } - return 0; - }), - [topology?.components] - ); + const { heading, properties, isPropertiesPanelEmpty } = useMemo(() => { + const allProperties = topology?.properties || []; + const properties = allProperties.filter((i) => !i.headline); + + const heading = allProperties.filter( + (i) => i.headline && (!!i.value || !!i.text || !!i.url) + ); + + const isPropertiesPanelShown = + properties.filter( + // we don't want to show properties that are hidden or have no value or + // text + (i) => !i.headline && i.type !== "hidden" && (i.text || i.value) + ).length > 0; + + return { + heading, + properties, + isPropertiesPanelEmpty: !isPropertiesPanelShown + }; + }, [topology?.properties]); + + const isAnalyticsPanelEmpty = useMemo(() => { + // if there are no insights, checks or components, we don't need to show the + // second panel at all + return ( + !topology?.summary?.insights && + !topology?.summary?.checks && + !( + (topology?.components ?? []).filter((i) => { + return ( + !isEmpty(i.summary) || + !isEmpty(i.summary?.checks) || + !isEmpty(i.summary?.insights) + ); + }).length > 0 + ) + ); + }, [ + topology?.components, + topology?.summary?.checks, + topology?.summary?.insights + ]); + + const isFooterEmpty = useMemo(() => { + if (metricsInFooter && heading.length > 0) { + return false; + } + if (isAnalyticsPanelEmpty && isPropertiesPanelEmpty) { + return true; + } + return false; + }, [ + heading.length, + isAnalyticsPanelEmpty, + isPropertiesPanelEmpty, + metricsInFooter + ]); if (topology == null) { return ; } - const allProperties = topology.properties || []; - const properties = allProperties.filter((i) => !i.headline); - - const heading = allProperties.filter( - (i) => i.headline && (!!i.value || !!i.text || !!i.url) - ); - return (
-
+

@@ -178,13 +198,11 @@ export function TopologyCard({ className="overflow-hidden truncate text-ellipsis align-middle text-15pxinrem font-bold leading-1.21rel" title={topology.name} > - {prepareTopologyLink(topology) && ( - - {topology.text || topology.name} - + {topologyLink ? ( + {topology.text || topology.name} + ) : ( + topology.text || topology.name )} - {!prepareTopologyLink(topology) && - (topology.text || topology.name)}

@@ -223,46 +241,26 @@ export function TopologyCard({ )}
-
- {metricsInFooter ? ( -
- -
- ) : ( - <> - - - - {canShowChildHealth() && ( - - )} - + {metricsInFooter && heading.length > 0 ? ( +
+ +
+ ) : ( + <> + - {topology?.id && } - {sortedTopologyComponents?.map((component: any) => ( - - ))} -
- - )} -
+ + + )} +
+ )}
); } diff --git a/src/components/Topology/TopologyCard/TopologyCardPropertiesColumn.tsx b/src/components/Topology/TopologyCard/TopologyCardPropertiesColumn.tsx index 0e48109c9..fb1cab671 100644 --- a/src/components/Topology/TopologyCard/TopologyCardPropertiesColumn.tsx +++ b/src/components/Topology/TopologyCard/TopologyCardPropertiesColumn.tsx @@ -1,13 +1,16 @@ import { Property } from "@flanksource-ui/api/types/topology"; import { CustomScroll } from "@flanksource-ui/ui/CustomScroll"; +import clsx from "clsx"; import { PropertyDisplay } from "./Property"; type TopologyCardPropertiesColumnProps = { properties: Property[]; + displayTwoColumns?: boolean; }; export default function TopologyCardPropertiesColumn({ - properties + properties, + displayTwoColumns = false }: TopologyCardPropertiesColumnProps) { // Filter out properties that are hidden, have no text or value, and are not a // headline property. @@ -21,7 +24,10 @@ export default function TopologyCardPropertiesColumn({ return ( { - if (severity === "critical") { - return "red"; - } - if (severity === "high") { - return "orange"; - } - if (severity === "warning") { - return "yellow"; - } - if (severity === "info") { - return "gray"; - } - if (severity === "low") { - return "green"; - } - if (severity === "medium") { - return "green"; - } - return "gray"; -}; - -type TopologyConfigAnalysisLineProps = { - target?: string; - topology: Pick; -}; - -export function TopologyConfigAnalysisLine({ - topology, - target = "" -}: TopologyConfigAnalysisLineProps) { - const insights = topology?.summary?.insights; - - const analysis: StatusLineData = useMemo(() => { - if (!insights) { - return { - icon: , - label: "Insights", - statuses: [] - }; - } - const analysisToCountMap: Record = {}; - - Object.entries(insights).forEach(([type, severityMap]) => { - const severityMapWithLowMediumCombined: typeof severityMap = { - ...severityMap, - medium: (severityMap.medium ?? 0) + (severityMap.low ?? 0), - low: undefined - }; - Object.entries(severityMapWithLowMediumCombined) - .filter(([_, count]) => count !== undefined) - .forEach(([severity, count], index) => { - const color = severityToColorMap(severity); - // Only show icon for first insight, otherwise it's too much - const icon = - index === 0 ? : null; - const label = count ?? 0; - const key = `${type}-${severity}`; - const url = `/catalog/insights?component=${topology.id}&type=${type}&severity=${severity}`; - analysisToCountMap[key] = { - color, - icon, - label, - url - }; - }); - }); - - return { - icon: , - label: "Insights", - url: `/catalog/insights?component=${topology.id}`, - statuses: Object.values(analysisToCountMap) - }; - }, [insights, topology.id]); - - if (!insights) { - return null; - } - - return ; -} diff --git a/src/components/Topology/TopologyCard/Utils/FormatProperties.tsx b/src/components/Topology/TopologyCard/Utils/FormatProperties.tsx index 6dd023cd9..fce63f113 100644 --- a/src/components/Topology/TopologyCard/Utils/FormatProperties.tsx +++ b/src/components/Topology/TopologyCard/Utils/FormatProperties.tsx @@ -13,8 +13,54 @@ type FormatPropertyProps = { isSidebar?: boolean; }; +export default function PropertyGauge({ + property, + derivedMax, + derivedValue, + isSidebar +}: FormatPropertyProps & { + derivedValue?: string; + derivedMax?: string; +}) { + if (!property) { + return null; + } + + const { value, max } = property; + + if (!value || !max) { + return null; + } + + const percent = (Number(value) / Number(max)) * 100; + + return ( + <> +
+
+ {derivedValue} +
+
+ +
+
+ + + ); +} + export function FormatPropertyURL({ property }: FormatPropertyProps) { - console.log("property", property); if (property == null) { return null; } @@ -103,7 +149,6 @@ export function FormatPropertyCPUMemory({ return null; } - const value = property.value; const max = property.max; const derivedMax = property.unit?.startsWith("milli") @@ -111,30 +156,13 @@ export function FormatPropertyCPUMemory({ : formatBytes(Number(property.max), 1); if (max) { - const percent = (Number(value) / Number(max)) * 100; return ( - <> -
-
- {derivedValue} -
-
- -
-
- - + ); } diff --git a/src/ui/CustomScroll/index.tsx b/src/ui/CustomScroll/index.tsx index fd07636c8..25fedbe7d 100644 --- a/src/ui/CustomScroll/index.tsx +++ b/src/ui/CustomScroll/index.tsx @@ -34,11 +34,7 @@ export const CustomScroll = ({ return (
@@ -51,7 +47,7 @@ export const CustomScroll = ({ setShowMore(false); }} className={clsx( - "bottom-0 m-auto w-full hover:underline", + "bottom-0 col-span-2 m-auto w-full hover:underline", showMoreClass )} >