Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/vmui/packages/vmui/src/components/Table/Table.test.tsx
Original file line number Diff line number Diff line change
@@ -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<Row>[] = [
{ 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(<Table
tableId="t"
rows={rows}
columns={columns}
paginationOffset={[0, 10]}
/>);
expect(screen.queryByLabelText("Expand row")).toBeNull();
});

it("expands a row on chevron click", () => {
render(<Table
tableId="t"
rows={rows}
columns={columns}
paginationOffset={[0, 10]}
renderExpandedRow={(row) => <div data-testid="expanded">{row.msg}-details</div>}
/>);
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");
});
});
109 changes: 81 additions & 28 deletions app/vmui/packages/vmui/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 = <T extends object>({
tableId,
Expand All @@ -20,6 +22,7 @@ const Table = <T extends object>({
isActiveRow,
onClickRow,
actionsRender,
renderExpandedRow,
paginationOffset,
applyViewColumns = () => {
},
Expand All @@ -40,10 +43,27 @@ const Table = <T extends object>({
setOrderDir(defaultOrder?.dir || "desc");
}, [defaultOrder?.key, defaultOrder?.dir]);

const [offsetStart, offsetEnd] = paginationOffset;

const sortedList = useMemo(() => {
const [startIndex, endIndex] = paginationOffset;
return stableSort<T>(rows, getComparator(orderDir, orderBy)).slice(startIndex, endIndex);
}, [rows, orderBy, orderDir, paginationOffset]);
return stableSort<T>(rows, getComparator(orderDir, orderBy)).slice(offsetStart, offsetEnd);
}, [rows, orderBy, orderDir, offsetStart, offsetEnd]);

const [expandedRows, setExpandedRows] = useState<Set<number>>(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,
Expand All @@ -66,6 +86,7 @@ const Table = <T extends object>({
>
<thead className="vm-table-header">
<TableRow variant="header">
{renderExpandedRow && <th className="vm-table-cell vm-table-cell-header vm-table-cell_expand"/>}
{columns.map((column, idx) => (
<TableHeaderCell
key={column.key}
Expand All @@ -87,31 +108,63 @@ const Table = <T extends object>({
</thead>
<tbody className="vm-table-body">
{sortedList.map((row, rowIndex) => (
<TableRow
key={rowIndex}
isActive={isActiveRow && isActiveRow(row as T)}
onClick={(e) => onClickRow && onClickRow(row as T, e)}
>
{columns.map((col) => (
<TableCell
key={String(col.key)}
column={col}
columnPrefs={getColumnPrefs(col.key)}
row={row as T}
rowIdx={rowIndex}
/>
))}

{actionsRender && (
<TableCellActions
row={row as T}
actionsRender={actionsRender}
/>
)}
<Fragment key={rowIndex}>
<TableRow
isActive={isActiveRow && isActiveRow(row as T)}
onClick={(e) => onClickRow && onClickRow(row as T, e)}
>
{renderExpandedRow && (
<td className="vm-table-cell vm-table-cell_expand">
<button
type="button"
aria-label={expandedRows.has(rowIndex) ? "Collapse row" : "Expand row"}
aria-expanded={expandedRows.has(rowIndex)}
className={classNames({
"vm-table__expand-btn": true,
"vm-table__expand-btn_open": expandedRows.has(rowIndex),
})}
onClick={(e) => {
e.stopPropagation();
toggleExpanded(rowIndex);
}}
>
<ArrowDownIcon/>
</button>
</td>
)}

{columns.map((col) => (
<TableCell
key={String(col.key)}
column={col}
columnPrefs={getColumnPrefs(col.key)}
row={row as T}
rowIdx={rowIndex}
/>
))}

{actionsRender && (
<TableCellActions
row={row as T}
actionsRender={actionsRender}
/>
)}

{/* Spacer column fills remaining width */}
<td className="vm-table-cell vm-table-cell_empty"/>
</TableRow>
{/* Spacer column fills remaining width */}
<td className="vm-table-cell vm-table-cell_empty"/>
</TableRow>

{renderExpandedRow && expandedRows.has(rowIndex) && (
<tr className="vm-table-row vm-table-row_expanded">
<td
className="vm-table-cell vm-table-cell_expanded-content"
colSpan={1 + columns.length + (actionsRender ? 1 : 0) + 1}
>
{renderExpandedRow(row as T)}
</td>
</tr>
)}
</Fragment>
))}
</tbody>
</table>
Expand Down
27 changes: 27 additions & 0 deletions app/vmui/packages/vmui/src/components/Table/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions app/vmui/packages/vmui/src/components/Table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface TableProps<T> {
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];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -33,6 +34,7 @@ const TableLogs: FC<TableLogsProps> = ({ tableId, logs, columns, rowsPerPage, ap
defaultOrder={{ key: "_time", dir: "desc" }}
paginationOffset={offset}
applyViewColumns={applyViewColumns}
renderExpandedRow={(log) => <GroupLogsFields log={log}/>}
/>
<Pagination
currentPage={page}
Expand Down
1 change: 1 addition & 0 deletions docs/victorialogs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ according to the following docs:

## tip

* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): allow expanding a row in Table view to inspect all fields of a single log entry, reusing the Group view field list. See [#1630](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1630).
* BUGFIX: [cluster version](https://docs.victoriametrics.com/victorialogs/cluster/): evenly spread rerouted data across available `vlstorage` nodes. Previously, healthy nodes adjacent to unavailable nodes in the `-storageNode` list could receive much more data, resulting in uneven resource usage. See [#1548](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1548).
* BUGFIX: [data ingestion](https://docs.victoriametrics.com/victorialogs/data-ingestion/) and [querying](https://docs.victoriametrics.com/victorialogs/querying/): properly handle logs containing duplicate [stream field](https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields) names. Previously, [v1.52.0](https://github.com/VictoriaMetrics/VictoriaLogs/releases/tag/v1.52.0) could panic when ingesting such logs in single-node VictoriaLogs, drop them during ingestion in VictoriaLogs cluster, or panic when querying such data written by earlier releases. See [#1603](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1603) and [#1604](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1604).
* BUGFIX: [LogsQL](https://docs.victoriametrics.com/victorialogs/logsql/): fix [`week_range[Sun,Sun]` filter](https://docs.victoriametrics.com/victorialogs/logsql/#week-range-filter) when it is used inside the [`filter` pipe](https://docs.victoriametrics.com/victorialogs/logsql/#filter-pipe). Previously, it could fail to match rows on Sunday. See [#1335](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1335).
Expand Down