From def72c66a2cef7a75bd0f46f3206201afa12bd97 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Wed, 3 Jul 2024 08:44:08 +0300 Subject: [PATCH] feat: add group by type class for config summary Fixes #2048 chore: update config class label --- src/api/query-hooks/useConfigSummaryQuery.ts | 12 +- ...nfigSummaryTableVirtualAggregateColumn.tsx | 48 +++++++ .../Cells/ConfigSummaryVirtualColumnCell.tsx | 34 +++++ .../ConfigSummary/ConfigSummaryList.tsx | 129 +++++++++--------- .../utils/useGroupBySearchParam.tsx | 2 +- .../ConfigGroupByDropdown.tsx | 22 ++- src/components/Configs/ConfigsTypeIcon.tsx | 3 +- .../Topology/TopologyBreadcrumbs/index.tsx | 2 - src/pages/config/ConfigList.tsx | 9 +- 9 files changed, 187 insertions(+), 74 deletions(-) create mode 100644 src/components/Configs/ConfigSummary/Cells/ConfigSummaryTableVirtualAggregateColumn.tsx create mode 100644 src/components/Configs/ConfigSummary/Cells/ConfigSummaryVirtualColumnCell.tsx diff --git a/src/api/query-hooks/useConfigSummaryQuery.ts b/src/api/query-hooks/useConfigSummaryQuery.ts index 44ae1ae87..d5dd92a6d 100644 --- a/src/api/query-hooks/useConfigSummaryQuery.ts +++ b/src/api/query-hooks/useConfigSummaryQuery.ts @@ -13,7 +13,7 @@ export function useConfigSummaryQuery({ const [searchParams] = useSearchParams({ sortBy: "type", sortOrder: "asc", - groupBy: "type" + groupBy: "config_class,type" }); const hideDeletedConfigs = useHideDeletedConfigs(); const labels = searchParams.get("labels") ?? undefined; @@ -33,7 +33,8 @@ export function useConfigSummaryQuery({ }, [labels]); const req: ConfigSummaryRequest = { - groupBy, + // group by config_class is always done on the frontend + groupBy: groupBy?.filter((g) => g !== "config_class") || undefined, deleted: !hideDeletedConfigs, filter: filterSummaryByLabel, health: health ? tristateOutputToQueryParamValue(health) : undefined, @@ -50,6 +51,11 @@ export function useConfigSummaryQuery({ return useQuery({ queryKey: ["configs", "configSummary", req], queryFn: () => getConfigsSummary(req), - enabled + enabled, + select: (data) => + data.map((summary) => ({ + ...summary, + config_class: summary.type.split("::")[0] + })) }); } diff --git a/src/components/Configs/ConfigSummary/Cells/ConfigSummaryTableVirtualAggregateColumn.tsx b/src/components/Configs/ConfigSummary/Cells/ConfigSummaryTableVirtualAggregateColumn.tsx new file mode 100644 index 000000000..5c9b8c947 --- /dev/null +++ b/src/components/Configs/ConfigSummary/Cells/ConfigSummaryTableVirtualAggregateColumn.tsx @@ -0,0 +1,48 @@ +import { ConfigSummary } from "@flanksource-ui/api/types/configs"; +import { Badge } from "@flanksource-ui/ui/Badge/Badge"; +import { CellContext } from "@tanstack/react-table"; +import { IoChevronDown, IoChevronForward } from "react-icons/io5"; +import ConfigsTypeIcon from "../../ConfigsTypeIcon"; + +export function ConfigSummaryTableVirtualAggregateColumn({ + row +}: CellContext) { + if (row.getCanExpand()) { + const groupingValue = row.getGroupingValue(row.groupingColumnId!) as string; + const count = row.subRows.reduce((acc, row) => acc + row.original.count, 0); + return ( +
+ {row.getIsExpanded() ? : } + {row.groupingColumnId === "type" || + row.groupingColumnId === "config_class" ? ( + +
+ {row.groupingColumnId === "config_class" && ( + {groupingValue} + )} + +
+
+ ) : ( +
+ {groupingValue ? ( + {groupingValue} + ) : ( + (None) + )} + +
+ )} +
+ ); + } +} diff --git a/src/components/Configs/ConfigSummary/Cells/ConfigSummaryVirtualColumnCell.tsx b/src/components/Configs/ConfigSummary/Cells/ConfigSummaryVirtualColumnCell.tsx new file mode 100644 index 000000000..b28ae7376 --- /dev/null +++ b/src/components/Configs/ConfigSummary/Cells/ConfigSummaryVirtualColumnCell.tsx @@ -0,0 +1,34 @@ +import { ConfigSummary } from "@flanksource-ui/api/types/configs"; +import { Badge } from "@flanksource-ui/ui/Badge/Badge"; +import { CellContext } from "@tanstack/react-table"; +import { BiLabel } from "react-icons/bi"; + +export function ConfigSummaryVirtualColumnCell({ + getValue, + row, + groupByTags, + columnId +}: CellContext & { + groupByTags: string[]; + columnId: string; +}) { + const isTag = groupByTags.includes(columnId); + const value = getValue(); + + return ( +
+ {isTag && } + {value ? ( + {value} + ) : ( + (None) + )} + +
+ ); +} diff --git a/src/components/Configs/ConfigSummary/ConfigSummaryList.tsx b/src/components/Configs/ConfigSummary/ConfigSummaryList.tsx index d11a030de..d418000c2 100644 --- a/src/components/Configs/ConfigSummary/ConfigSummaryList.tsx +++ b/src/components/Configs/ConfigSummary/ConfigSummaryList.tsx @@ -1,10 +1,9 @@ import { ConfigSummary } from "@flanksource-ui/api/types/configs"; import { Badge } from "@flanksource-ui/ui/Badge/Badge"; import { DataTable } from "@flanksource-ui/ui/DataTable"; +import ChangeCount, { CountBar } from "@flanksource-ui/ui/Icons/ChangeCount"; import { CellContext, ColumnDef, Row } from "@tanstack/react-table"; import { useCallback, useMemo } from "react"; -import { BiLabel } from "react-icons/bi"; -import { IoChevronDown, IoChevronForward } from "react-icons/io5"; import { useSearchParams } from "react-router-dom"; import ConfigListCostCell from "../ConfigList/Cells/ConfigListCostCell"; import ConfigListDateCell from "../ConfigList/Cells/ConfigListDateCell"; @@ -14,7 +13,8 @@ import { ConfigSummaryHealthAggregateCell, ConfigSummaryHealthCell } from "./Cells/ConfigSummaryHealthCells"; -import ChangeCount, { CountBar } from "@flanksource-ui/ui/Icons/ChangeCount"; +import { ConfigSummaryTableVirtualAggregateColumn } from "./Cells/ConfigSummaryTableVirtualAggregateColumn"; +import { ConfigSummaryVirtualColumnCell } from "./Cells/ConfigSummaryVirtualColumnCell"; export function getConfigStatusColor(health?: ConfigSummary["health"]) { if (!health) { @@ -51,7 +51,12 @@ function ConfigSummaryTypeCell({ }, [configType]); return ( - +
{value} @@ -93,10 +98,55 @@ function ConfigSummaryAnalysisCell({ ); } +function ConfigSummaryAnalysisAggregateCell({ + row +}: CellContext) { + const subRows = row.subRows; + + const value = subRows.reduce((acc, row) => { + const analysis = row.original.analysis; + if (analysis) { + Object.entries(analysis).forEach(([key, value]) => { + acc[key] = (acc[key] || 0) + value; + }); + } + return acc; + }, {} as Record); + + return ( +
+ { + return { + count: value, + icon: ( + + ) + }; + })} + /> +
+ ); +} + const configSummaryColumns: ColumnDef[] = [ { header: "changes", accessorKey: "changes", + aggregatedCell: ({ getValue }) => { + const value = getValue(); + if (!value) { + return null; + } + return ; + }, cell: ({ getValue }: CellContext) => { const value = getValue(); if (!value) { @@ -118,6 +168,7 @@ const configSummaryColumns: ColumnDef[] = [ header: "analysis", accessorKey: "analysis", cell: ConfigSummaryAnalysisCell, + aggregatedCell: ConfigSummaryAnalysisAggregateCell, minSize: 30, maxSize: 100 }, @@ -176,7 +227,10 @@ export default function ConfigSummaryList({ const tags = groupBy .filter( (column) => - column !== "type" && column !== "health" && column !== "status" + column !== "type" && + column !== "health" && + column !== "status" && + column !== "config_class" ) .filter((column) => row.original[column as keyof ConfigSummary]); if (tags.length > 0) { @@ -202,65 +256,17 @@ export default function ConfigSummaryList({ accessorKey: column, maxSize: 250, minSize: 100, - aggregatedCell: ({ row }) => { - if (row.getCanExpand()) { - const groupingValue = row.getGroupingValue( - row.groupingColumnId! - ) as string; - const count = row.subRows.reduce( - (acc, row) => acc + row.original.count, - 0 - ); - return ( -
- {row.getIsExpanded() ? : } - {row.groupingColumnId === "type" ? ( - - - - ) : ( -
- {groupingValue ? ( - {groupingValue} - ) : ( - (None) - )} - -
- )} -
- ); - } - }, + aggregatedCell: ConfigSummaryTableVirtualAggregateColumn, cell: column === "type" ? ConfigSummaryTypeCell - : ({ getValue, row }: CellContext) => { - const isTag = groupByTags.includes(column); - const value = getValue(); - - return ( -
- {isTag && } - {value ? ( - {value} - ) : ( - (None) - )} - -
- ); - } + : (props) => ( + + ) } satisfies ColumnDef; }); return [...newColumns, ...configSummaryColumns]; @@ -283,6 +289,7 @@ export default function ConfigSummaryList({ isLoading={isLoading} className="max-w-full overflow-x-auto table-fixed table-auto" savePreferences={false} + expandAllRows={groupBy[0] === "config_class"} /> ); } diff --git a/src/components/Configs/ConfigSummary/utils/useGroupBySearchParam.tsx b/src/components/Configs/ConfigSummary/utils/useGroupBySearchParam.tsx index 1e635664d..9b7fda17d 100644 --- a/src/components/Configs/ConfigSummary/utils/useGroupBySearchParam.tsx +++ b/src/components/Configs/ConfigSummary/utils/useGroupBySearchParam.tsx @@ -9,7 +9,7 @@ import { useSearchParams } from "react-router-dom"; */ export default function useGroupBySearchParam(): string[] | undefined { const [searchParams] = useSearchParams({ - groupBy: "type" + groupBy: "config_class,type" }); const groupByProp = searchParams.get("groupBy") ?? undefined; diff --git a/src/components/Configs/ConfigsListFilters/ConfigGroupByDropdown.tsx b/src/components/Configs/ConfigsListFilters/ConfigGroupByDropdown.tsx index e57825962..4dfa52238 100644 --- a/src/components/Configs/ConfigsListFilters/ConfigGroupByDropdown.tsx +++ b/src/components/Configs/ConfigsListFilters/ConfigGroupByDropdown.tsx @@ -37,6 +37,11 @@ const items: GroupByOptions[] = [ label: "type", value: "type", icon: + }, + { + label: "provider", + value: "config_class", + icon: } ]; @@ -55,7 +60,7 @@ export default function ConfigGroupByDropdown({ if (configType) { return []; } - return ["type"]; + return ["config_class", "type"]; } return groupBy.split(",").map((v) => v.replace("__tag", "")) ?? []; }, [configType, params, searchParamKey]); @@ -66,9 +71,18 @@ export default function ConfigGroupByDropdown({ enabled: true, select: (tags) => { return [ - ...Object.values(items).filter( - (item) => !configType || item.value !== "type" - ), + ...Object.values(items).filter((item) => { + if (!configType) { + return false; + } + if (item.value === "type") { + return false; + } + if (item.value === "config_class") { + return false; + } + return true; + }), ...(tags && tags.length > 0 ? tags // ensure that the tags are unique diff --git a/src/components/Configs/ConfigsTypeIcon.tsx b/src/components/Configs/ConfigsTypeIcon.tsx index cfdffeafb..96c70b827 100644 --- a/src/components/Configs/ConfigsTypeIcon.tsx +++ b/src/components/Configs/ConfigsTypeIcon.tsx @@ -68,7 +68,8 @@ export default function ConfigsTypeIcon({ )} {showLabel && {value}} - {children && {children}} + {/* eslint-disable-next-line react/jsx-no-useless-fragment */} + {children && <>{children}}
); } diff --git a/src/components/Topology/TopologyBreadcrumbs/index.tsx b/src/components/Topology/TopologyBreadcrumbs/index.tsx index 831e03654..768e53783 100644 --- a/src/components/Topology/TopologyBreadcrumbs/index.tsx +++ b/src/components/Topology/TopologyBreadcrumbs/index.tsx @@ -20,8 +20,6 @@ function TopologyBreadcrumbItem({ }) { const { data: component } = useComponentNameQuery(topologyId, {}); - console.log(component); - if (!component) { return null; } diff --git a/src/pages/config/ConfigList.tsx b/src/pages/config/ConfigList.tsx index 11764a4d5..d39c5256d 100644 --- a/src/pages/config/ConfigList.tsx +++ b/src/pages/config/ConfigList.tsx @@ -20,7 +20,7 @@ export function ConfigListPage() { const [searchParams] = useSearchParams({ sortBy: "type", sortOrder: "asc", - groupBy: "type" + groupBy: "config_class,type" }); const configType = searchParams.get("configType") ?? undefined; @@ -103,7 +103,12 @@ export function ConfigListPage() { !["type", "config_class"].includes(g) + )?.[0] + } /> )}