From eb0b822d4807e4139cbc68a656889d9150e5830b Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Wed, 1 Nov 2023 07:49:53 +0300 Subject: [PATCH 1/3] refactor: fix typescript issues and update react table version fix: fix a few refactor issue fix: fix failing build chore: move the columns to canary directory --- src/components/Canary/CanaryTableColumns.tsx | 134 ++++++++ .../Canary/Columns/{index.js => index.tsx} | 89 +++--- src/components/Canary/Rows/{lib.js => lib.ts} | 34 ++- .../Canary/{grouping.js => grouping.ts} | 15 +- src/components/Canary/table.tsx | 288 +++++++----------- 5 files changed, 335 insertions(+), 225 deletions(-) create mode 100644 src/components/Canary/CanaryTableColumns.tsx rename src/components/Canary/Columns/{index.js => index.tsx} (78%) rename src/components/Canary/Rows/{lib.js => lib.ts} (85%) rename src/components/Canary/{grouping.js => grouping.ts} (75%) diff --git a/src/components/Canary/CanaryTableColumns.tsx b/src/components/Canary/CanaryTableColumns.tsx new file mode 100644 index 000000000..8002d1053 --- /dev/null +++ b/src/components/Canary/CanaryTableColumns.tsx @@ -0,0 +1,134 @@ +import { CellContext, ColumnDef } from "@tanstack/react-table"; +import { HealthCheck } from "../../api/types/health"; +import { IoChevronForwardOutline } from "react-icons/io5"; +import { + LastTransistionCell, + LatencyCell, + TitleCell, + UptimeCell +} from "./Columns"; +import { Status } from "../Status"; +import { dateDiff } from "../../utils/date"; + +function ExpandArrow({ row }: CellContext) { + return row.getCanExpand() ? ( +
+
+ +
+
+ ) : null; +} + +type TableColumnsProps = { + showNamespaceTags: boolean; + hideNamespacePrefix: boolean; +}; + +export function getCanaryTableColumns({ + showNamespaceTags, + hideNamespacePrefix +}: TableColumnsProps): ColumnDef[] { + return [ + { + id: "expander", + cell: ExpandArrow, + enableSorting: false, + meta: { cellClassName: "py-2" } + }, + { + header: "Checks", + accessorKey: "name", + id: "name", + meta: { + cellClassName: "py-2 px-2 overflow-hidden overflow-ellipsis relative" + }, + enableSorting: true, + cell: ({ ...props }: CellContext) => ( + + ) + }, + { + header: "Health", + accessorKey: "status", + meta: { + cellClassName: "w-36 overflow-hidden overflow-ellipsis relative" + }, + enableSorting: true, + cell: ({ getValue, row }: CellContext) => { + const value = getValue(); + if (!value) { + return null; + } + if (typeof value === "object") { + return ; + } + const date = new Date().toISOString().split(".")[0]; + const lastRutime = row.original.lastRuntime; + const showTime = dateDiff(date, lastRutime, "minute") > 15; + return ( +
+ + {showTime && ( + + )} +
+ ); + } + }, + { + header: "Last Transition", + accessorKey: "last_transition_time", + meta: { + cellClassName: "w-36 overflow-hidden overflow-ellipsis relative" + }, + enableSorting: false, + cell: ({ getValue, row }: CellContext) => { + const value = getValue(); + if (!value) { + return null; + } + return ; + } + }, + { + header: "Uptime", + accessorKey: "uptime", + meta: { + cellClassName: "w-28 overflow-hidden overflow-ellipsis relative" + }, + enableSorting: true, + cell: ({ getValue }: CellContext) => { + const value = getValue(); + if (!value) { + return null; + } + const newValue = value?.uptime ?? value; + return ; + } + }, + { + header: "Latency", + accessorKey: "latency", + meta: { + cellClassName: "w-28 overflow-hidden overflow-ellipsis relative" + }, + enableSorting: true, + cell: ({ getValue }: CellContext) => { + const value = getValue(); + if (!value) { + return null; + } + return ; + } + } + ]; +} diff --git a/src/components/Canary/Columns/index.js b/src/components/Canary/Columns/index.tsx similarity index 78% rename from src/components/Canary/Columns/index.js rename to src/components/Canary/Columns/index.tsx index a70ade761..dcc836fbb 100644 --- a/src/components/Canary/Columns/index.js +++ b/src/components/Canary/Columns/index.tsx @@ -11,12 +11,14 @@ import style from "../index.module.css"; import { Status } from "../../Status"; import { dateDiff, relativeDateTime } from "../../../utils/date"; import clsx from "clsx"; +import { CellContext } from "@tanstack/react-table"; +import { HealthCheck } from "../../../api/types/health"; -export function Cell({ state, value, row, column }) { +export function Cell({ state, value, row, column }: any) { const { pivotCellType } = state; if (column.id === "name") { - return TitleCell({ row }); + return TitleCell({ row } as any); } if (pivotCellType === "uptime" || column.id === "uptime") { if (value == null) { @@ -66,7 +68,7 @@ export function Cell({ state, value, row, column }) { return null; } -export function LastTransistionCell({ value }) { +export function LastTransistionCell({ value }: any) { const hasAgoString = relativeDateTime(value).indexOf("ago") > -1; const hasYesterdayString = relativeDateTime(value).indexOf("yesterday") > -1; return ( @@ -89,27 +91,37 @@ export function LastTransistionCell({ value }) { ); } -export function HealthCell({ value }) { +export function HealthCell({ value }: any) { return ; } -export function UptimeCell({ value }) { +export function UptimeCell({ value }: any) { return ( ); } -export function LatencyCell({ value }) { +export function LatencyCell({ value }: any) { return ; } -export function TitleCell({ row }) { +type TitleCellProps = { + hideNamespacePrefix?: boolean; + showNamespaceTags?: boolean; +} & CellContext; + +export function TitleCell({ + row, + hideNamespacePrefix = false, + showNamespaceTags = false +}: TitleCellProps) { const rowValues = - row.original?.pivoted === true - ? row.original[row.original.valueLookup] ?? null + (row.original as any)?.pivoted === true + ? // @ts-expect-error + row.original[row.original.valueLookup] ?? null : row.original; let title = GetName(rowValues); - if (row.hideNamespacePrefix) { + if (hideNamespacePrefix) { title = removeNamespacePrefix(title, rowValues); } @@ -124,20 +136,22 @@ export function TitleCell({ row }) { - {row.canExpand && rowValues.subRows && rowValues?.subRows.length > 1 && ( - <span className="ml-1 flex items-center"> - <Badge - className="ml-1" - colorClass="bg-gray-200 text-gray-800" - roundedClass="rounded-xl" - text={rowValues?.subRows.length} - size="xs" - /> - </span> - )} - {row.showNamespaceTags ? ( + {row.getCanExpand() && + rowValues.subRows && + rowValues?.subRows.length > 1 && ( + <span className="ml-1 flex items-center"> + <Badge + className="ml-1" + colorClass="bg-gray-200 text-gray-800" + roundedClass="rounded-xl" + text={rowValues?.subRows.length} + size="xs" + /> + </span> + )} + {showNamespaceTags ? ( rowValues.namespaces ? ( <Badge className="ml-2" @@ -160,13 +174,13 @@ export function TitleCell({ row }) { ); } -function getSortString(original) { +function getSortString(original: any) { // case insensitive name sorting, with namespace as a secondary sort const sortString = `${original.sortKey?.toLowerCase()}${original.namespace?.toLowerCase()}`; return sortString; } -function getPivotSortValueOrDefault(a, b, pivotAccessor) { +function getPivotSortValueOrDefault(a: any, b: any, pivotAccessor: any) { if (pivotAccessor === "name") { const aValue = a.original?.pivoted === true @@ -195,9 +209,9 @@ function getPivotSortValueOrDefault(a, b, pivotAccessor) { return { aValue, bValue }; } -export function getSortType(pivotCellTypeOrAccessor, pivotAccessor) { - const sortTypes = { - latency: (a, b) => { +export function getSortType(pivotCellTypeOrAccessor: any, pivotAccessor: any) { + const sortTypes: any = { + latency: (a: any, b: any) => { const { aValue, bValue } = getPivotSortValueOrDefault( a, b, @@ -206,7 +220,7 @@ export function getSortType(pivotCellTypeOrAccessor, pivotAccessor) { return getLatency(aValue) < getLatency(bValue) ? -1 : 1; }, - uptime: (a, b) => { + uptime: (a: any, b: any) => { const { aValue, bValue } = getPivotSortValueOrDefault( a, b, @@ -214,7 +228,7 @@ export function getSortType(pivotCellTypeOrAccessor, pivotAccessor) { ); return getUptimeScore(aValue) < getUptimeScore(bValue) ? 1 : -1; }, - checkStatuses: (a, b) => { + checkStatuses: (a: any, b: any) => { const { aValue, bValue } = getPivotSortValueOrDefault( a, b, @@ -224,7 +238,7 @@ export function getSortType(pivotCellTypeOrAccessor, pivotAccessor) { ? 1 : -1; }, - name: (a, b) => { + name: (a: any, b: any) => { const { aValue, bValue } = getPivotSortValueOrDefault( a, b, @@ -234,7 +248,7 @@ export function getSortType(pivotCellTypeOrAccessor, pivotAccessor) { } }; return pivotAccessor === "name" - ? sortTypes[pivotAccessor] + ? sortTypes[pivotAccessor as any] : sortTypes[pivotCellTypeOrAccessor] ?? "alphanumeric"; } @@ -243,7 +257,7 @@ export function makeColumnsForPivot({ pivotCellType, firstColumns, cellClass = `px-5 py-2` -} = {}) { +}: any = {}) { // For syntax https://stackoverflow.com/a/26578323/15581317 const columns = []; for (const [, value] of pivotSet.entries()) { @@ -262,9 +276,12 @@ export function makeColumnsForPivot({ return [...firsts, ...columns]; } -export function getColumns({ columnObject, pivotCellType = null }) { - return Object.entries(columnObject).reduce((acc, [k, v], i) => { - const { Header, accessor, cellClass, Cell: IncomingCell, id } = v; +export function getColumns({ + columnObject, + pivotCellType = null +}: Record<any, any>) { + return Object.entries(columnObject).reduce((acc: any, [k, v], i) => { + const { Header, accessor, cellClass, Cell: IncomingCell, id } = v as any; const pivotAccessor = accessor == null || typeof accessor === "function" ? id == null diff --git a/src/components/Canary/Rows/lib.js b/src/components/Canary/Rows/lib.ts similarity index 85% rename from src/components/Canary/Rows/lib.js rename to src/components/Canary/Rows/lib.ts index 6c70dc926..721004fe3 100644 --- a/src/components/Canary/Rows/lib.js +++ b/src/components/Canary/Rows/lib.ts @@ -2,8 +2,15 @@ import { GetName } from "../data"; import { removeNamespacePrefix } from "../utils"; import { isPlainObject } from "../../../lib/isPlainObject"; import { aggregate } from "../aggregate"; +import { HealthCheck } from "../../../api/types/health"; -export function makeRow({ row, hideNamespacePrefix = true } = {}) { +export function makeRow({ + row = {}, + hideNamespacePrefix = true +}: { + row: any; + hideNamespacePrefix?: boolean; +}) { return { ...row, name: GetName(row), @@ -14,9 +21,9 @@ export function makeRow({ row, hideNamespacePrefix = true } = {}) { }; } -export function filterRowsByPivotSet(rows, pivotSet) { - return Array.from(pivotSet).reduce((accPivots, pivot) => { - const filteredRows = rows.reduce((acc, row) => { +export function filterRowsByPivotSet(rows: any, pivotSet: any) { + return Array.from(pivotSet).reduce((accPivots: any, pivot: any) => { + const filteredRows = rows.reduce((acc: any, row: any) => { if (row[pivot] != null) { acc[acc.length] = row[pivot]; } @@ -26,18 +33,23 @@ export function filterRowsByPivotSet(rows, pivotSet) { accPivots[pivot] = aggregate(pivot, filteredRows); } return accPivots; - }, {}); + }, {} as Record<string, any>); } export function prepareRows({ - tableData, + tableData = [], hideNamespacePrefix = true, pivotBy = null, pivotLookup = null +}: { + tableData?: HealthCheck[]; + hideNamespacePrefix?: boolean; + pivotBy?: string | null; + pivotLookup?: string | null; }) { if (pivotBy == null || pivotBy === "none") { const values = tableData?.reduce( - (acc, row, i) => { + (acc: any, row, i) => { acc.rows[i] = makeRow({ row, hideNamespacePrefix }); return acc; }, @@ -48,7 +60,7 @@ export function prepareRows({ } if (pivotBy != null && pivotBy !== "none") { const values = tableData.reduce( - (acc, row) => { + (acc: any, row: any) => { const { rows: { length } } = acc; @@ -70,11 +82,11 @@ export function prepareRows({ if (subRows.rows.length === 0) { return acc; } - subRows.meta.pivotSet.forEach((value) => { + subRows.meta.pivotSet.forEach((value: any) => { acc.meta.pivotSet.add(value); }); const newRow = { ...row, subRows: subRows.rows }; - const aggregateFilters = filterRowsByPivotSet( + const aggregateFilters: any = filterRowsByPivotSet( subRows.rows, acc.meta.pivotSet ); @@ -110,7 +122,7 @@ export function prepareRows({ } return keyObjectAcc; }, - {} + {} as Record<string, any> ); if (Object.keys(keyObject).length === 0) { return acc; diff --git a/src/components/Canary/grouping.js b/src/components/Canary/grouping.ts similarity index 75% rename from src/components/Canary/grouping.js rename to src/components/Canary/grouping.ts index 45d534e9a..b60ad63a4 100644 --- a/src/components/Canary/grouping.js +++ b/src/components/Canary/grouping.ts @@ -1,12 +1,14 @@ +import { HealthCheck } from "../../api/types/health"; + // process table groupings, given a list of checks and a 'groupBy' object -export function getGroupedChecks(checks, groupBy) { +export function getGroupedChecks(checks: HealthCheck[] = [], groupBy?: string) { if ( groupBy === "name" || groupBy === "description" || groupBy === "canary_name" ) { - const groupedChecks = {}; - const groupNames = []; + const groupedChecks: Record<string, HealthCheck[]> = {}; + const groupNames: string[] = []; checks.forEach((check) => { const value = check[groupBy] || "(none)"; if (groupNames.indexOf(value) === -1) { @@ -17,11 +19,12 @@ export function getGroupedChecks(checks, groupBy) { }); return groupedChecks; } - const groupedChecks = { Others: [] }; + + const groupedChecks: Record<string, HealthCheck[]> = { Others: [] }; const groupNames = ["Others"]; checks.forEach((check) => { let hasValidGroup = false; - let groupName; + let groupName: any; if (check.labels) { const labelKeys = Object.keys(check.labels); @@ -39,7 +42,7 @@ export function getGroupedChecks(checks, groupBy) { }); } if (hasValidGroup) { - groupedChecks[groupName].push(check); + groupedChecks[groupName!].push(check); } else { groupedChecks.Others.push(check); } diff --git a/src/components/Canary/table.tsx b/src/components/Canary/table.tsx index 11decb088..a8f0cc2bc 100644 --- a/src/components/Canary/table.tsx +++ b/src/components/Canary/table.tsx @@ -1,20 +1,24 @@ +import { + SortingState, + flexRender, + getCoreRowModel, + getExpandedRowModel, + getGroupedRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import clsx from "clsx"; import React, { useEffect, useMemo, useState } from "react"; import { TiArrowSortedDown, TiArrowSortedUp } from "react-icons/ti"; -import { useTable, useSortBy, useExpanded } from "react-table"; -import { getAggregatedGroupedChecks } from "./aggregate"; -import { getGroupedChecks } from "./grouping"; +import { useSearchParams } from "react-router-dom"; +import { HealthCheck } from "../../api/types/health"; +import { getCanaryTableColumns } from "./CanaryTableColumns"; +import { useCheckSetEqualityForPreviousVsCurrent } from "../Hooks/useCheckSetEqualityForPreviousVsCurrent"; import { getColumns, makeColumnsForPivot } from "./Columns"; -import { - decodeUrlSearchParams, - encodeObjectToUrlSearchParams, - useUpdateParams -} from "./url"; import { columnObject, firstColumns } from "./Columns/columns"; import { prepareRows } from "./Rows/lib"; -import { useCheckSetEqualityForPreviousVsCurrent } from "../Hooks/useCheckSetEqualityForPreviousVsCurrent"; -import { useSearchParams } from "react-router-dom"; -import { HealthCheck } from "../../api/types/health"; -import clsx from "clsx"; +import { getAggregatedGroupedChecks } from "./aggregate"; +import { getGroupedChecks } from "./grouping"; const styles = { outerDivClass: "border-l border-r border-gray-300 overflow-y-auto", @@ -31,13 +35,6 @@ const styles = { expandArrowIconClass: "ml-6 flex" }; -const sortByValidValues = new Map([ - ["name", null], - ["checkStatuses", null], - ["uptime", null], - ["latency", null] -]); - type CanaryChecksProps = { checks?: HealthCheck[]; labels?: string[]; @@ -82,7 +79,6 @@ export function CanaryTable({ }, [params, checks, groupBy, groupSingleItems]); const { rows, meta } = useMemo( - // @ts-expect-error () => prepareRows({ tableData, hideNamespacePrefix, pivotBy, pivotLookup }), [hideNamespacePrefix, tableData, pivotBy, pivotLookup] ); @@ -95,33 +91,14 @@ export function CanaryTable({ pivotBy != null && pivotBy !== "none"; - const columns = useMemo(() => { - if (shouldPivot) { - return makeColumnsForPivot({ - pivotSet: meta.pivotSet, - pivotCellType, - firstColumns - }); - } - return getColumns({ - columnObject, - pivotCellType: null - }); - // isNewPivotSet checks the old pivotSet against the new on, so it's a much better test - // than 'meta.pivotSet', which will probably be referentially new on every fetch of data. - // Testing the set directly meant we won't have to rebuild the columns, which is predicted to - // be an expensive operation. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pivotCellType, isNewPivotSet, pivotBy, pivotLookup]); - return ( <Table data={rows} - columns={columns} labels={labels} pivotCellType={shouldPivot ? pivotCellType : null} - onUnexpandableRowClick={onCheckClick} + onHealthCheckClick={onCheckClick} hasGrouping={groupBy !== "no-group"} + groupBy={groupBy} showNamespaceTags={showNamespaceTags} hideNamespacePrefix={hideNamespacePrefix} theadStyle={theadStyle} @@ -132,155 +109,128 @@ export function CanaryTable({ type TableProps = { data: any[]; - columns: any[]; labels?: string[]; pivotCellType: string | null; hasGrouping: boolean; - onUnexpandableRowClick: (check: HealthCheck) => void; + onHealthCheckClick: (check: HealthCheck) => void; showNamespaceTags?: boolean; hideNamespacePrefix?: boolean; theadStyle?: React.CSSProperties; + groupBy?: string; }; export function Table({ data, - columns, + labels, pivotCellType, hasGrouping = false, - onUnexpandableRowClick, + onHealthCheckClick, showNamespaceTags = false, hideNamespacePrefix = false, theadStyle = {}, + groupBy = "canary_name", ...rest }: TableProps) { + console.log({ showNamespaceTags }); + const [params, setParams] = useSearchParams(); + + const sortByValue = params.get("sortBy") || "name"; + const sortDesc = params.get("sortDesc") === "true" || false; + const rowFinder = (row: any) => { const rowValues = row?.pivoted === true ? row[row.valueLookup] ?? null : row; return rowValues?.subRows || []; }; - const { - state: tableState, - getTableProps, - getTableBodyProps, - headerGroups, - rows, - // @ts-expect-error - setSortBy, - prepareRow, - toggleHideColumn, - // @ts-expect-error - toggleRowExpanded - } = useTable( - { - columns, - data, - // @ts-expect-error - disableMultiSort: true, - autoResetSortBy: false, - autoResetExpanded: false, - getSubRows: rowFinder, - useControlledState: (state) => - useMemo( - () => ({ - ...state, - pivotCellType - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [state, pivotCellType] - ) - }, - useSortBy, - useExpanded - ); - - // @ts-expect-error - const tableSortState = tableState?.sortBy; - - // Hide expander column if there is no grouping - useEffect(() => { - toggleHideColumn("expander", !hasGrouping); - }, [hasGrouping, toggleHideColumn]); - - // Set table's sort state according to url params on page load or url change - useEffect(() => { - const searchParams = window.location.search; - const decodedParams = decodeUrlSearchParams(searchParams); - const { sortBy, sortDesc } = decodedParams; - - // check for validity, else reset to default values - const sortByChecked = sortByValidValues.has(sortBy) ? sortBy : "name"; - const sortDescChecked = typeof sortDesc === "boolean" ? sortDesc : false; - - setSortBy([{ id: sortByChecked, desc: sortDescChecked }]); - }, [setSortBy]); - - const updateParams = useUpdateParams(); + // ensure sorting works when state isn't managed by caller + const [sortBy, setSortBy] = useState<SortingState>(); - // Table-state changes will trigger url changes useEffect(() => { - const updateURLBySortState = (sortBy: string, sortDesc: boolean) => { - const searchParams = window.location.search; - const decodedParams = decodeUrlSearchParams(searchParams); + setSortBy([{ id: sortByValue, desc: sortDesc }]); + }, [setSortBy, sortByValue, sortDesc]); - // check for validity, else reset to default values - const sortByChecked = sortByValidValues.has(sortBy) ? sortBy : "name"; - const sortDescChecked = typeof sortDesc === "boolean" ? sortDesc : false; + const columns = useMemo( + () => + getCanaryTableColumns({ + showNamespaceTags, + hideNamespacePrefix + }), + [] + ); - const newFormState = { - ...decodedParams, - sortBy: sortByChecked, - sortDesc: sortDescChecked - }; - const encoded = encodeObjectToUrlSearchParams(newFormState); - if (window.location.search !== `?${encoded}`) { - updateParams(newFormState); + const table = useReactTable<HealthCheck>({ + columns: columns, + data, + enableMultiSort: false, + autoResetAll: false, + getSubRows: rowFinder, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: hasGrouping ? getExpandedRowModel() : undefined, + getGroupedRowModel: hasGrouping ? getGroupedRowModel() : undefined, + getSortedRowModel: getSortedRowModel(), + enableSortingRemoval: true, + onSortingChange: (sorting) => { + if (typeof sorting === "function") { + const newSortBy = sorting(sortBy ?? []); + setSortBy(newSortBy); + if (newSortBy.length === 0) { + params.delete("sortBy"); + params.delete("sortDesc"); + } else { + params.set("sortBy", newSortBy[0].id); + params.set("sortDesc", newSortBy[0].desc.toString()); + } + setParams(params); + return; } - }; - - if (tableSortState.length > 0) { - updateURLBySortState(tableSortState[0].id, tableSortState[0].desc); + setSortBy(sorting); + if (sorting.length === 0) { + params.delete("sortBy"); + params.delete("sortDesc"); + } else { + params.set("sortBy", sorting[0].id); + params.set("sortDesc", sorting[0].desc.toString()); + } + setParams(params); + }, + enableHiding: true, + state: { + columnVisibility: hasGrouping ? { expander: true } : undefined, + sorting: sortBy } - }, [tableSortState, updateParams]); + }); return ( <div className={styles.outerDivClass} {...rest}> <div className={styles.topBgClass} /> - <table - className={styles.tableClass} - style={{ borderSpacing: "0" }} - {...getTableProps()} - > + <table className={styles.tableClass} style={{ borderSpacing: "0" }}> <thead className={styles.theadClass} style={theadStyle}> - {headerGroups.map((headerGroup) => ( - <tr - className={styles.theadRowClass} - {...headerGroup.getHeaderGroupProps()} - > + {table.getHeaderGroups().map((headerGroup) => ( + <tr className={styles.theadRowClass} key={headerGroup.id}> {headerGroup.headers.map((column) => ( <th className={clsx(styles.theadHeaderClass)} - // @ts-expect-error - {...column.getHeaderProps(column.getSortByToggleProps())} + key={column.id} // Table header onClick sorting override: // sortDesc cannot be null, only either true/false - onClick={() => - // @ts-expect-error - column.toggleSortBy( - // @ts-expect-error - column.isSortedDesc != null ? !column.isSortedDesc : false - ) - } + onClick={column.column.getToggleSortingHandler()} > - {/* @ts-expect-error */} - <div className={clsx("flex select-none", column.cellClass)}> - {column.render("Header")} + <div + className={clsx( + "flex select-none", + (column.column.columnDef?.meta as Record<string, string>) + ?.cellClass + )} + > + {flexRender( + column.column.columnDef.header, + column.getContext() + )} <span> - {/* @ts-expect-error */} - {column.isSorted ? ( - // @ts-expect-error - column.isSortedDesc ? ( + {column.column.getIsSorted() ? ( + column.column.getIsSorted() ? ( <TiArrowSortedUp /> ) : ( <TiArrowSortedDown /> @@ -295,42 +245,36 @@ export function Table({ </tr> ))} </thead> - <tbody className={styles.tbodyClass} {...getTableBodyProps()}> - {rows.length > 0 && - rows.map((row) => { - prepareRow(row); - // @ts-expect-error - row.showNamespaceTags = showNamespaceTags; - // @ts-expect-error - row.hideNamespacePrefix = hideNamespacePrefix; + <tbody className={styles.tbodyClass}> + {table.getRowModel().rows.length > 0 && + table.getRowModel().rows.map((row) => { return ( <tr - // @ts-expect-error key={row.id} className={`${styles.tbodyRowClass} ${ - // @ts-expect-error - row.canExpand ? styles.tbodyRowExpandableClass : "" + row.getCanExpand() ? styles.tbodyRowExpandableClass : "" }`} style={{}} onClick={ - // @ts-expect-error - row.canExpand - ? () => toggleRowExpanded(row.id) - : () => onUnexpandableRowClick(row.original) + row.getCanExpand() + ? () => row.getToggleExpandedHandler()() + : () => onHealthCheckClick(row.original) } - {...row.getRowProps()} > - {row.cells.map((cell) => ( + {row.getVisibleCells().map((cell) => ( <td - // @ts-expect-error - key={cell.column.Header} + key={cell.column.id} className={`${styles.tbodyDataClass} ${ - // @ts-expect-error - cell.column.cellClass || "" + cell.column || "" + } ${ + (cell.column.columnDef?.meta as Record<string, any>) + ?.cellClassName || "" }`} - {...cell.getCellProps()} > - {cell.render("Cell")} + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} </td> ))} </tr> @@ -338,7 +282,7 @@ export function Table({ })} </tbody> </table> - {rows.length <= 0 && ( + {table.getRowModel().rows.length <= 0 && ( <div className="flex items-center justify-center py-20 px-2 border-b border-gray-300 text-center text-gray-400"> No data available </div> From 0d4c6bb63bbe649947214bdc0e545b12d8448826 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe <wmmaina7@gmail.com> Date: Wed, 1 Nov 2023 08:48:26 +0300 Subject: [PATCH 2/3] feat: treat / in names as grouping Closes #1453 fix: group by / instead of - chore: normalize group checks fix: fix issue reading original name --- src/components/Canary/Rows/lib.ts | 6 +-- src/components/Canary/grouping.ts | 62 ++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/components/Canary/Rows/lib.ts b/src/components/Canary/Rows/lib.ts index 721004fe3..11598bcf3 100644 --- a/src/components/Canary/Rows/lib.ts +++ b/src/components/Canary/Rows/lib.ts @@ -1,8 +1,8 @@ -import { GetName } from "../data"; -import { removeNamespacePrefix } from "../utils"; +import { HealthCheck } from "../../../api/types/health"; import { isPlainObject } from "../../../lib/isPlainObject"; import { aggregate } from "../aggregate"; -import { HealthCheck } from "../../../api/types/health"; +import { GetName } from "../data"; +import { removeNamespacePrefix } from "../utils"; export function makeRow({ row = {}, diff --git a/src/components/Canary/grouping.ts b/src/components/Canary/grouping.ts index b60ad63a4..2589a6997 100644 --- a/src/components/Canary/grouping.ts +++ b/src/components/Canary/grouping.ts @@ -1,12 +1,9 @@ +import { update } from "lodash"; import { HealthCheck } from "../../api/types/health"; // process table groupings, given a list of checks and a 'groupBy' object export function getGroupedChecks(checks: HealthCheck[] = [], groupBy?: string) { - if ( - groupBy === "name" || - groupBy === "description" || - groupBy === "canary_name" - ) { + if (groupBy === "description" || groupBy === "canary_name") { const groupedChecks: Record<string, HealthCheck[]> = {}; const groupNames: string[] = []; checks.forEach((check) => { @@ -20,6 +17,61 @@ export function getGroupedChecks(checks: HealthCheck[] = [], groupBy?: string) { return groupedChecks; } + if (groupBy === "name") { + // when grouping by name, we want to split name by /, and group each part + const groupedChecks: Record<string, HealthCheck[]> = {}; + checks.forEach((check) => { + const value = check[groupBy] || "(none)"; + if (!value.includes("/")) { + update(groupedChecks, value, (current) => { + if (!current) { + return [check]; + } + return [...current, check]; + }); + return; + } + const path = value.split("/")[0]; + update(groupedChecks, path, (current) => { + const updatedCheck = { + ...check, + // if the name is a path, we want to show the last part of the path + name: value.split("/", 1)[1], + originalName: value + }; + if (!current) { + return [updatedCheck]; + } + return [...current, updatedCheck]; + }); + }); + + const normalizedGroupedChecks: Record<string, HealthCheck[]> = {}; + + Object.entries(groupedChecks).forEach(([key, value]) => { + // if there are multiple checks with the same name, we show the name with + // the prefix of the path + if (value.length > 1) { + normalizedGroupedChecks[key] = value; + return; + } + // if the name is a path, and there is only one check with that name, we + // want to show the full name as it won't be grouped + if ((value?.[0] as any)?.originalName?.includes("/")) { + normalizedGroupedChecks[key] = value.map((check) => ({ + ...check, + // replace the name with the full path + name: (check as any).originalName + })); + return; + } + // if the name is not a path, we do nothing + normalizedGroupedChecks[key] = value; + }); + + return normalizedGroupedChecks; + } + const groupedChecks: Record<string, HealthCheck[]> = { Others: [] }; const groupNames = ["Others"]; checks.forEach((check) => { From ab221057858751d4610c0a22596f67a997dedfef Mon Sep 17 00:00:00 2001 From: Maina Wycliffe <wmmaina7@gmail.com> Date: Thu, 2 Nov 2023 14:48:58 +0300 Subject: [PATCH 3/3] feat: for uptime, show passing/failing for groups Closes #1434 fix: use total subrows and remove space --- src/components/Canary/CanaryTableColumns.tsx | 13 ++++++++++++- src/components/Canary/table.tsx | 3 --- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/Canary/CanaryTableColumns.tsx b/src/components/Canary/CanaryTableColumns.tsx index 8002d1053..6768ea776 100644 --- a/src/components/Canary/CanaryTableColumns.tsx +++ b/src/components/Canary/CanaryTableColumns.tsx @@ -106,7 +106,18 @@ export function getCanaryTableColumns({ cellClassName: "w-28 overflow-hidden overflow-ellipsis relative" }, enableSorting: true, - cell: ({ getValue }: CellContext<HealthCheck, any>) => { + cell: ({ getValue, row }: CellContext<HealthCheck, any>) => { + if (row.getCanExpand()) { + const subRows = row.subRows; + const passed = subRows.filter((r) => r.original.uptime.passed).length; + + return ( + <> + {passed}/{subRows.length} + </> + ); + } + const value = getValue(); if (!value) { return null; diff --git a/src/components/Canary/table.tsx b/src/components/Canary/table.tsx index a8f0cc2bc..42dbb6ed1 100644 --- a/src/components/Canary/table.tsx +++ b/src/components/Canary/table.tsx @@ -14,8 +14,6 @@ import { useSearchParams } from "react-router-dom"; import { HealthCheck } from "../../api/types/health"; import { getCanaryTableColumns } from "./CanaryTableColumns"; import { useCheckSetEqualityForPreviousVsCurrent } from "../Hooks/useCheckSetEqualityForPreviousVsCurrent"; -import { getColumns, makeColumnsForPivot } from "./Columns"; -import { columnObject, firstColumns } from "./Columns/columns"; import { prepareRows } from "./Rows/lib"; import { getAggregatedGroupedChecks } from "./aggregate"; import { getGroupedChecks } from "./grouping"; @@ -132,7 +130,6 @@ export function Table({ groupBy = "canary_name", ...rest }: TableProps) { - console.log({ showNamespaceTags }); const [params, setParams] = useSearchParams(); const sortByValue = params.get("sortBy") || "name";