diff --git a/app/vmui/packages/vmui/src/components/Table/Table.test.tsx b/app/vmui/packages/vmui/src/components/Table/Table.test.tsx new file mode 100644 index 0000000000..1c1905e65a --- /dev/null +++ b/app/vmui/packages/vmui/src/components/Table/Table.test.tsx @@ -0,0 +1,57 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, fireEvent, screen } from "@testing-library/preact"; +import Table from "./Table"; +import { Column } from "./types"; + +vi.mock("./hooks/useTableColumnPrefs", () => ({ + useTableColumnPrefs: () => ({ + getColumnPrefs: () => ({}), + updateColumnPref: () => { + /* no-op */ + }, + }), +})); + +type Row = { _time: string; msg: string }; + +const columns: Column[] = [ + { key: "_time", title: "_time", options: { sortable: false, resizable: false, draggable: false, menuEnabled: false } }, + { key: "msg", title: "msg", options: { sortable: false, resizable: false, draggable: false, menuEnabled: false } }, +]; +const rows: Row[] = [{ _time: "1", msg: "hello" }]; + +describe("Table renderExpandedRow", () => { + it("renders no expand controls without the prop", () => { + render(); + expect(screen.queryByLabelText("Expand row")).toBeNull(); + }); + + it("expands a row on chevron click", () => { + render(
{row.msg}-details
} + />); + expect(screen.queryByTestId("expanded")).toBeNull(); + + const expandBtn = screen.getByLabelText("Expand row"); + expect(expandBtn.getAttribute("aria-expanded")).toBe("false"); + + fireEvent.click(expandBtn); + expect(screen.getByTestId("expanded")).toHaveTextContent("hello-details"); + + const collapseBtn = screen.getByLabelText("Collapse row"); + expect(collapseBtn.getAttribute("aria-expanded")).toBe("true"); + + fireEvent.click(collapseBtn); + expect(screen.queryByTestId("expanded")).toBeNull(); + expect(screen.getByLabelText("Expand row").getAttribute("aria-expanded")).toBe("false"); + }); +}); diff --git a/app/vmui/packages/vmui/src/components/Table/Table.tsx b/app/vmui/packages/vmui/src/components/Table/Table.tsx index 1ef473cbf3..48e5708b5d 100644 --- a/app/vmui/packages/vmui/src/components/Table/Table.tsx +++ b/app/vmui/packages/vmui/src/components/Table/Table.tsx @@ -1,4 +1,5 @@ -import { useState, useMemo, useRef, useEffect } from "preact/compat"; +import { Fragment, useState, useMemo, useRef, useEffect } from "preact/compat"; +import classNames from "classnames"; import { getComparator, stableSort } from "./helpers"; import { OrderDir } from "../../types"; import TableHeaderCell from "./TableHeaderCell/TableHeaderCell"; @@ -11,6 +12,7 @@ import { Size, useResizeObserver } from "../../hooks/useResizeObserver"; import { useDebounceCallback } from "../../hooks/useDebounceCallback"; import { ColumnKey, TableProps } from "./types"; import { useDragColumn } from "./hooks/useDragColumn"; +import { ArrowDownIcon } from "../Main/Icons"; const Table = ({ tableId, @@ -20,6 +22,7 @@ const Table = ({ isActiveRow, onClickRow, actionsRender, + renderExpandedRow, paginationOffset, applyViewColumns = () => { }, @@ -40,10 +43,27 @@ const Table = ({ setOrderDir(defaultOrder?.dir || "desc"); }, [defaultOrder?.key, defaultOrder?.dir]); + const [offsetStart, offsetEnd] = paginationOffset; + const sortedList = useMemo(() => { - const [startIndex, endIndex] = paginationOffset; - return stableSort(rows, getComparator(orderDir, orderBy)).slice(startIndex, endIndex); - }, [rows, orderBy, orderDir, paginationOffset]); + return stableSort(rows, getComparator(orderDir, orderBy)).slice(offsetStart, offsetEnd); + }, [rows, orderBy, orderDir, offsetStart, offsetEnd]); + + const [expandedRows, setExpandedRows] = useState>(new Set()); + + useEffect(() => { + setExpandedRows(new Set()); + // reset on pagination bounds change, not on the paginationOffset tuple identity + // (the parent may recreate that array every render) + }, [rows, orderBy, orderDir, offsetStart, offsetEnd]); + + const toggleExpanded = (idx: number) => { + setExpandedRows(prev => { + const next = new Set(prev); + next.has(idx) ? next.delete(idx) : next.add(idx); + return next; + }); + }; const sortPack = useMemo(() => ({ key: orderBy, @@ -66,6 +86,7 @@ const Table = ({ > + {renderExpandedRow && {sortedList.map((row, rowIndex) => ( - onClickRow && onClickRow(row as T, e)} - > - {columns.map((col) => ( - - ))} - - {actionsRender && ( - - )} + + onClickRow && onClickRow(row as T, e)} + > + {renderExpandedRow && ( + + )} + + {columns.map((col) => ( + + ))} + + {actionsRender && ( + + )} - {/* Spacer column fills remaining width */} - + + + )} + ))}
} {columns.map((column, idx) => ( ({
+ + - + {/* Spacer column fills remaining width */} + + + + {renderExpandedRow && expandedRows.has(rowIndex) && ( +
+ {renderExpandedRow(row as T)} +
diff --git a/app/vmui/packages/vmui/src/components/Table/style.scss b/app/vmui/packages/vmui/src/components/Table/style.scss index a4422e6fca..11c9a3f68a 100644 --- a/app/vmui/packages/vmui/src/components/Table/style.scss +++ b/app/vmui/packages/vmui/src/components/Table/style.scss @@ -9,3 +9,30 @@ background-color: $color-background-block; font-variant-numeric: lining-nums tabular-nums; } + +.vm-table-cell_expand { + width: 24px; + padding: 0 4px; +} + +.vm-table__expand-btn { + display: flex; + align-items: center; + background: none; + border: none; + cursor: pointer; + padding: 2px; + + svg { + width: 14px; + transition: transform 150ms ease-in-out; + } + + &_open svg { + transform: rotate(180deg); + } +} + +.vm-table-row_expanded > .vm-table-cell_expanded-content { + padding: 0; +} diff --git a/app/vmui/packages/vmui/src/components/Table/types.ts b/app/vmui/packages/vmui/src/components/Table/types.ts index b8aaef7481..f2e004a89b 100644 --- a/app/vmui/packages/vmui/src/components/Table/types.ts +++ b/app/vmui/packages/vmui/src/components/Table/types.ts @@ -28,6 +28,7 @@ export interface TableProps { isActiveRow?: (row: T) => boolean; onClickRow?: (row: T, e: MouseEvent) => void; actionsRender?: (row: T) => ReactNode + renderExpandedRow?: (row: T) => ReactNode; applyViewColumns?: (action: ViewColumnsAction) => void; paginationOffset: [number, number]; } diff --git a/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.tsx b/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.tsx index 294feb3b2a..b29a26aba1 100644 --- a/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.tsx +++ b/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.tsx @@ -5,6 +5,7 @@ import Pagination from "../../Main/Pagination/Pagination"; import { useTableLogsColumns } from "./hooks/useTableLogsColumns"; import { useTableLogsPaginate } from "./hooks/useTableLogsPaginate"; import { ViewColumnsAction } from "../../Table/hooks/useTableColumnView"; +import GroupLogsFields from "../GroupView/GroupLogsFields"; interface TableLogsProps { tableId: string; @@ -33,6 +34,7 @@ const TableLogs: FC = ({ tableId, logs, columns, rowsPerPage, ap defaultOrder={{ key: "_time", dir: "desc" }} paginationOffset={offset} applyViewColumns={applyViewColumns} + renderExpandedRow={(log) => } />