From 762f8c6238e9fe3a63fdfe75fb57a20f07062899 Mon Sep 17 00:00:00 2001 From: withlin Date: Fri, 26 Dec 2025 14:27:36 +0000 Subject: [PATCH] app/vmui: allow table column reordering - add drag-and-drop ordering in table settings and store order in URL - render table columns in chosen order and restyle drag handle - add TableLogs order unit test Testing: npm run lint:local; npm test Fixes: #714 --- .../Table/TableSettings/TableSettings.tsx | 72 +++++++++++++++++++ .../components/Table/TableSettings/style.scss | 69 ++++++++++++++++++ .../Views/TableView/TableLogs.test.tsx | 28 ++++++++ .../components/Views/TableView/TableLogs.tsx | 11 ++- 4 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.test.tsx diff --git a/app/vmui/packages/vmui/src/components/Table/TableSettings/TableSettings.tsx b/app/vmui/packages/vmui/src/components/Table/TableSettings/TableSettings.tsx index 717372b2e5..a226cad275 100644 --- a/app/vmui/packages/vmui/src/components/Table/TableSettings/TableSettings.tsx +++ b/app/vmui/packages/vmui/src/components/Table/TableSettings/TableSettings.tsx @@ -41,6 +41,8 @@ const TableSettings: FC = ({ const [searchColumn, setSearchColumn] = useState(""); const [indexFocusItem, setIndexFocusItem] = useState(-1); + const [draggingColumn, setDraggingColumn] = useState(null); + const [dragOverColumn, setDragOverColumn] = useState(null); const customColumns = useMemo(() => { return selectedColumns.filter(col => !columns.includes(col)); @@ -122,6 +124,43 @@ const TableSettings: FC = ({ onChangeColumns(columnsArray); }, []); + const handleDragStart = (column: string) => (e: DragEvent) => { + setDraggingColumn(column); + e.dataTransfer?.setData("text/plain", column); + }; + + const handleDragOver = (column: string) => (e: DragEvent) => { + e.preventDefault(); + if (dragOverColumn === column) return; + setDragOverColumn(column); + }; + + const handleDragLeave = () => { + setDragOverColumn(null); + }; + + const handleDrop = (targetColumn: string) => (e: DragEvent) => { + e.preventDefault(); + if (!draggingColumn || draggingColumn === targetColumn) return; + + const fromIndex = selectedColumns.indexOf(draggingColumn); + const toIndex = selectedColumns.indexOf(targetColumn); + if (fromIndex === -1 || toIndex === -1) return; + + const updatedColumns = [...selectedColumns]; + updatedColumns.splice(fromIndex, 1); + updatedColumns.splice(toIndex, 0, draggingColumn); + + handleChangeDisplayColumns(updatedColumns); + setDragOverColumn(null); + setDraggingColumn(null); + }; + + const handleDragEnd = () => { + setDragOverColumn(null); + setDraggingColumn(null); + }; + return (
@@ -193,6 +232,39 @@ const TableSettings: FC = ({ ))}
+ {!!selectedColumns.length && ( +
+
+ Drag to reorder selected columns +
+
+ {selectedColumns.map((col) => ( +
+
+ ))} +
+
+ )} {toggleTableCompact && tableCompact !== undefined && (
diff --git a/app/vmui/packages/vmui/src/components/Table/TableSettings/style.scss b/app/vmui/packages/vmui/src/components/Table/TableSettings/style.scss index 770607cc6e..fafec269b8 100644 --- a/app/vmui/packages/vmui/src/components/Table/TableSettings/style.scss +++ b/app/vmui/packages/vmui/src/components/Table/TableSettings/style.scss @@ -88,5 +88,74 @@ } } } + + &-columns-order { + padding: 0 $padding-global $padding-global; + display: flex; + flex-direction: column; + gap: $padding-small; + + &__title { + font-size: $font-size; + font-weight: bold; + } + + &__list { + display: flex; + flex-direction: column; + gap: $padding-small; + } + + &__item { + display: grid; + grid-template-columns: 16px 1fr; + align-items: center; + gap: $padding-small; + padding: $padding-small $padding-global; + border: $border-divider; + border-radius: $border-radius-small; + background-color: $color-background-block; + cursor: grab; + + &_dragging { + cursor: grabbing; + opacity: 0.7; + } + + &_over { + background-color: $color-hover-black; + border-color: $color-secondary; + } + } + + &__drag { + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + color: $color-text-secondary; + + &:before { + content: ""; + display: block; + width: 10px; + height: 2px; + background-color: currentColor; + border-radius: 2px; + box-shadow: + 0 -4px 0 currentColor, + 0 4px 0 currentColor; + } + } + + &__label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-decoration: none; + font-family: $font-family-monospace; + } + } } } diff --git a/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.test.tsx b/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.test.tsx new file mode 100644 index 0000000000..d21d1eafdd --- /dev/null +++ b/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from "@testing-library/preact"; +import { describe, expect, it } from "vitest"; +import TableLogs from "./TableLogs"; + +const logs = [ + { _time: "2025-01-01T00:00:00Z", file: "a.go", _msg: "first message" }, + { _time: "2025-01-01T00:01:00Z", file: "b.go", _msg: "second message" }, +]; + +describe("TableLogs", () => { + it("renders columns in the provided display order", () => { + render( + + ); + + const headers = screen + .getAllByRole("columnheader") + .map((header) => header.textContent?.trim() || ""); + + expect(headers.slice(0, 3)).toEqual(["_msg", "file", "_time"]); + }); +}); 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 46b1a63273..09906ac3f5 100644 --- a/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.tsx +++ b/app/vmui/packages/vmui/src/components/Views/TableView/TableLogs.tsx @@ -45,12 +45,19 @@ const TableLogs: FC = ({ logs, displayColumns, tableCompact, col })); }, [columns]); + const tableColumnsMap = useMemo(() => { + return new Map(tableColumns.map((col) => [String(col.key), col])); + }, [tableColumns]); const filteredColumns = useMemo(() => { if (tableCompact) return compactColumns; if (!displayColumns?.length) return []; - return tableColumns.filter(c => displayColumns.includes(c.key as string)); - }, [tableColumns, displayColumns, tableCompact]); + return displayColumns.reduce((acc, key) => { + const column = tableColumnsMap.get(key); + if (column) acc.push(column); + return acc; + }, []); + }, [tableColumnsMap, displayColumns, tableCompact]); const paginationOffset = useMemo(() => { const startIndex = (page - 1) * rowsPerPage;