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",