diff --git a/package.json b/package.json index 83e030fc..f15347a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@netdata/netdata-ui", - "version": "5.5.5", + "version": "5.5.6", "description": "netdata UI kit", "main": "dist/index.js", "module": "dist/es6/index.js", diff --git a/src/components/table/groupByControl.test.js b/src/components/table/groupByControl.test.js new file mode 100644 index 00000000..1349152f --- /dev/null +++ b/src/components/table/groupByControl.test.js @@ -0,0 +1,41 @@ +import React from "react" +import { renderWithProviders, screen, waitFor } from "testUtilities" +import Table from "./table" + +describe("Table Group By control", () => { + it("keeps configured grouping active when the header control is hidden", async () => { + const tableRef = { current: null } + + renderWithProviders( + null, + }, + ]} + enableGroupByControl={false} + getRowId={row => row.id} + groupByColumns={{ status: { columns: ["status"], name: "Status" } }} + grouping="status" + tableRef={tableRef} + /> + ) + + await waitFor(() => expect(tableRef.current).not.toBeNull()) + + expect(screen.queryByTestId("tableGroupBy")).not.toBeInTheDocument() + expect(tableRef.current.getState().grouping).toEqual(["status"]) + expect(tableRef.current.getGroupedRowModel().rows).toHaveLength(1) + expect(tableRef.current.getGroupedRowModel().rows[0]).toMatchObject({ + groupingColumnId: "status", + groupingValue: "live", + }) + }) +}) diff --git a/src/components/table/header/groupBy.js b/src/components/table/header/groupBy.js index 4fe99568..7320e2f4 100644 --- a/src/components/table/header/groupBy.js +++ b/src/components/table/header/groupBy.js @@ -43,7 +43,7 @@ const HeaderGroupBy = ({ grouping, groupByColumns, onGroupBy, tableMeta, dataGa, menuPortalTarget={document.body} onChange={({ value }) => onGroupBy(value)} options={Object.values(groupByOptions)} - styles={{ size: "tiny", minWidth: 120 }} + styles={{ size: "tiny", minWidth: 120, ...tableMeta.groupBySelectStyles }} value={groupByOptions[grouping] || groupByOptions.default} /> diff --git a/src/components/table/header/groupBy.test.js b/src/components/table/header/groupBy.test.js new file mode 100644 index 00000000..5df487f8 --- /dev/null +++ b/src/components/table/header/groupBy.test.js @@ -0,0 +1,35 @@ +import React from "react" +import { renderWithProviders, screen } from "testUtilities" +import HeaderGroupBy from "./groupBy" + +const longGroup = "label:kubernetes.io/metadata.name" +const groupByColumns = { + [longGroup]: { name: longGroup }, +} +const dataColumns = [{ id: longGroup, name: longGroup }] + +const renderGroupBy = tableMeta => + renderWithProviders( + {}} + tableMeta={tableMeta} + /> + ) + +describe("HeaderGroupBy", () => { + it("keeps the existing 120 px minimum by default", () => { + renderGroupBy({}) + + expect(screen.getByTestId("tableGroupByFilterControl")).toHaveStyle({ minWidth: "120px" }) + }) + + it("accepts a reusable Select style override for long values", () => { + renderGroupBy({ groupBySelectStyles: { minWidth: 240 } }) + + expect(screen.getByText(longGroup)).toBeInTheDocument() + expect(screen.getByTestId("tableGroupByFilterControl")).toHaveStyle({ minWidth: "240px" }) + }) +}) diff --git a/src/components/table/header/index.js b/src/components/table/header/index.js index aec2f4df..7896ff49 100644 --- a/src/components/table/header/index.js +++ b/src/components/table/header/index.js @@ -10,6 +10,7 @@ const Header = ({ hasSearch, onSearch, groupByColumns, + enableGroupByControl = true, grouping, onGroupBy, tableMeta, @@ -27,7 +28,13 @@ const Header = ({ [] ) - if (!title && !groupByColumns && !hasSearch && !bulkActions && !enableColumnVisibility) + if ( + !title && + !(enableGroupByControl && groupByColumns) && + !hasSearch && + !bulkActions && + !enableColumnVisibility + ) return null return ( @@ -59,14 +66,16 @@ const Header = ({ /> )} - + {enableGroupByControl && ( + + )} {children} ) diff --git a/src/components/table/header/index.test.js b/src/components/table/header/index.test.js new file mode 100644 index 00000000..0d706cb0 --- /dev/null +++ b/src/components/table/header/index.test.js @@ -0,0 +1,25 @@ +import React from "react" +import { renderWithProviders, screen } from "testUtilities" +import Header from "." + +const props = { + dataColumns: [{ id: "group", name: "Group" }], + groupByColumns: { group: { columns: ["group"], name: "Group" } }, + grouping: "group", + onGroupBy: () => {}, + tableMeta: {}, +} + +describe("Table Header", () => { + it("shows the Group By control by default", () => { + renderWithProviders(
) + + expect(screen.getByTestId("tableGroupBy")).toBeInTheDocument() + }) + + it("can hide the Group By control without removing grouping configuration", () => { + renderWithProviders(
) + + expect(screen.queryByTestId("tableGroupBy")).not.toBeInTheDocument() + }) +}) diff --git a/src/components/table/headerActionsOrder.test.js b/src/components/table/headerActionsOrder.test.js new file mode 100644 index 00000000..437296bc --- /dev/null +++ b/src/components/table/headerActionsOrder.test.js @@ -0,0 +1,43 @@ +import React from "react" +import { renderWithProviders } from "testUtilities" +import Table from "./table" + +const data = [{ id: "node-1", name: "Node 1" }] +const dataColumns = [ + { + id: "name", + accessorKey: "name", + header: "Name", + cell: ({ getValue }) => getValue(), + }, +] + +const renderTable = props => + renderWithProviders( +
row.id} + headerChildren={} + {...props} + /> + ) + +const expectBefore = (first, second) => { + expect(first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() +} + +describe("Table header action order", () => { + it("keeps custom children before built-in actions by default", () => { + const { getByTestId } = renderTable() + + expectBefore(getByTestId("trailing-control"), getByTestId("bulk-actions")) + }) + + it("can place built-in actions before custom trailing controls", () => { + const { getByTestId } = renderTable({ headerActionsBeforeChildren: true }) + + expectBefore(getByTestId("bulk-actions"), getByTestId("trailing-control")) + }) +}) diff --git a/src/components/table/index.d.ts b/src/components/table/index.d.ts index dc35ca7d..ce222983 100644 --- a/src/components/table/index.d.ts +++ b/src/components/table/index.d.ts @@ -10,6 +10,7 @@ import { } from "@tanstack/table-core" import { ComponentType, Key, MutableRefObject, ReactNode, RefObject, UIEventHandler } from "react" import { Virtualizer } from "@tanstack/react-virtual" +import { StylesConfig } from "react-select" import { supportedBulkActions } from "./header/actions/useActions" import { supportedRowActions } from "./useColumns/useRowActions" @@ -82,6 +83,21 @@ export type OverflowTooltipProps = { options?: OverflowTooltipOptions } +export type TableGroupBySelectStyles = StylesConfig & { + minWidth?: number | string + size?: string +} + +export type TableMeta = { + bulkActionsStyles?: Record + cellStyles?: Record + groupByContainerStyles?: Record + groupBySelectStyles?: TableGroupBySelectStyles + headStyles?: Record + searchContainerStyles?: Record + searchStyles?: Record +} + export type TableProps = { data: Array dataColumns: Array> @@ -123,6 +139,9 @@ export type TableProps = { columnVisibility?: VisibilityTableState enableColumnVisibility?: boolean enableColumnPinning?: boolean + enableGroupByControl?: boolean + headerActionsBeforeChildren?: boolean + headerChildren?: ReactNode onGlobalSearchChange?: (value: any) => void onRowSelected?: (value: any) => void onClickRow?: (value: any) => void @@ -137,6 +156,7 @@ export type TableProps = { */ testPrefixCallback?: (rowData: D) => string largeDataOptions?: LargeDataOptions + meta?: TableMeta | ((...args: Array) => TableMeta) } declare const Table: (props: TableProps) => JSX.Element diff --git a/src/components/table/table.js b/src/components/table/table.js index ee82d2e6..99eba7fc 100644 --- a/src/components/table/table.js +++ b/src/components/table/table.js @@ -49,8 +49,10 @@ const tableDefaultProps = { enableColumnPinning: false, enableColumnReordering: false, enableColumnVisibility: false, + enableGroupByControl: true, enableResizing: false, globalFilterFn: includesString, + headerActionsBeforeChildren: false, onColumnVisibilityChange: noop, onColumnOrderChange: noop, onSortingChange: noop, @@ -73,6 +75,7 @@ const Table = memo(props => { const { bulkActions, headerChildren, + headerActionsBeforeChildren = tableDefaultProps.headerActionsBeforeChildren, data, dataColumns, @@ -117,6 +120,7 @@ const Table = memo(props => { grouping: defaultGrouping, onGroupByChange: groupingChangeCb, groupByColumns, + enableGroupByControl = tableDefaultProps.enableGroupByControl, onRowSelected, @@ -292,6 +296,19 @@ const Table = memo(props => { if (tableRef) tableRef.current = table const { getHasNextPage, loading, warning } = virtualizeOptions + const headerActions = ( + + ) return ( { hasSearch={!!onSearch} onSearch={onGlobalFilterChange} groupByColumns={groupByColumns} + enableGroupByControl={enableGroupByControl} onGroupBy={onGroupingChange} grouping={grouping} tableMeta={tableMeta} @@ -317,18 +335,9 @@ const Table = memo(props => { bulkActions={bulkActions} enableCustomSearch={enableCustomSearch} > + {headerActionsBeforeChildren && headerActions} {headerChildren || null} - + {!headerActionsBeforeChildren && headerActions} { + it("owns metric gaps as a semantic color in both themes", () => { + expect(DefaultTheme.colors.metricGap).toBe(rawColors.neutral.metricGapLight) + expect(DarkTheme.colors.metricGap).toBe(rawColors.neutral.metricGapDark) + }) + + it("owns a contrasting Map search highlight in both themes", () => { + expect(DefaultTheme.colors.mapSearchHighlight).toBe(rawColors.blue.blue100) + expect(DarkTheme.colors.mapSearchHighlight).toBe(rawColors.blue.blue150) + }) +}) diff --git a/src/theme/rawColors.js b/src/theme/rawColors.js index 65cfe35a..d66c58f4 100644 --- a/src/theme/rawColors.js +++ b/src/theme/rawColors.js @@ -151,6 +151,8 @@ const rawColors = { gunmetal: "#282C34", darkGunmetal: "#21252B", eerieBlack: "#181c20", + metricGapDark: "#312E27", + metricGapLight: "#E0DCD5", // Grey shades grey05: "#040505", grey10: "#080A0A",