From c422abe0f68cec6b2a8a7276a55da8799b8c550d Mon Sep 17 00:00:00 2001 From: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:46:42 -0500 Subject: [PATCH] HPCC-31304 ECL Watch v9 replace DetailsList with DataGrid Signed-off-by: Jeremy Clements <79224539+jeclrsg@users.noreply.github.com> --- esp/src/eclwatch/css/hpcc.css | 1 + esp/src/src-react/components/AppPanel.tsx | 2 +- esp/src/src-react/components/Files.tsx | 264 +++++++++++------- esp/src/src-react/components/Queries.tsx | 232 +++++++++------ esp/src/src-react/components/Workunits.tsx | 223 +++++++++------ .../components/controls/DataGrid.tsx | 59 ++++ .../src-react/components/controls/Grid.tsx | 185 +++++++++++- 7 files changed, 702 insertions(+), 264 deletions(-) create mode 100644 esp/src/src-react/components/controls/DataGrid.tsx diff --git a/esp/src/eclwatch/css/hpcc.css b/esp/src/eclwatch/css/hpcc.css index 26559935861..c97913c6c1f 100644 --- a/esp/src/eclwatch/css/hpcc.css +++ b/esp/src/eclwatch/css/hpcc.css @@ -25,6 +25,7 @@ body { font-family: Lucida Sans, Lucida Grande, Arial !important; font-size: 13px !important; padding: 0; + overflow: hidden; } button:hover { diff --git a/esp/src/src-react/components/AppPanel.tsx b/esp/src/src-react/components/AppPanel.tsx index 308b00ad22d..42d4f03c857 100644 --- a/esp/src/src-react/components/AppPanel.tsx +++ b/esp/src/src-react/components/AppPanel.tsx @@ -33,7 +33,7 @@ export const AppPanel: React.FunctionComponent = ({ const retVal = []; webLinks?.forEach(webLink => { webLink.Annotations.NamedValue.forEach(nv => { - retVal.push(); + retVal.push(); }); }); // Include HPCC Systems link when there are no other web links available diff --git a/esp/src/src-react/components/Files.tsx b/esp/src/src-react/components/Files.tsx index 14939bd6164..507768e06b4 100644 --- a/esp/src/src-react/components/Files.tsx +++ b/esp/src/src-react/components/Files.tsx @@ -1,18 +1,23 @@ import * as React from "react"; -import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Icon, Link } from "@fluentui/react"; +import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link } from "@fluentui/react"; +import { FolderZip20Regular, LockClosedFilled } from "@fluentui/react-icons"; +import { TableCellLayout, TableColumnDefinition, TableRowId, createTableColumn, Tooltip } from "@fluentui/react-components"; +import { WsDfu as HPCCWsDfu } from "@hpcc-js/comms"; import { scopedLogger } from "@hpcc-js/util"; +import { SizeMe } from "react-sizeme"; import * as WsDfu from "src/WsDfu"; -import { CreateDFUQueryStore, Get } from "src/ESPLogicalFile"; +import { CreateDFUQueryStore } from "src/ESPLogicalFile"; import { formatCost } from "src/Session"; import * as Utility from "src/Utility"; import { QuerySortItem } from "src/store/Store"; import nlsHPCC from "src/nlsHPCC"; import { useConfirm } from "../hooks/confirm"; +import { useUserTheme } from "../hooks/theme"; import { useMyAccount } from "../hooks/user"; import { useHasFocus, useIsMounted } from "../hooks/util"; import { HolyGrail } from "../layouts/HolyGrail"; import { pushParams } from "../util/history"; -import { FluentPagedGrid, FluentPagedFooter, useCopyButtons, useFluentStoreState, FluentColumns } from "./controls/Grid"; +import { FluentPagedDataGrid, FluentPagedFooter, useCopyButtons, useFluentStoreState } from "./controls/Grid"; import { AddToSuperfile } from "./forms/AddToSuperfile"; import { CopyFile } from "./forms/CopyFile"; import { DesprayFile } from "./forms/DesprayFile"; @@ -21,10 +26,11 @@ import { Filter } from "./forms/Filter"; import { RemoteCopy } from "./forms/RemoteCopy"; import { RenameFile } from "./forms/RenameFile"; import { ShortVerticalDivider } from "./Common"; -import { SizeMe } from "react-sizeme"; const logger = scopedLogger("src-react/components/Files.tsx"); +type LogicalFile = HPCCWsDfu.DFULogicalFile; + const FilterFields: Fields = { "LogicalName": { type: "string", label: nlsHPCC.Name, placeholder: nlsHPCC.somefile }, "Description": { type: "string", label: nlsHPCC.Description, placeholder: nlsHPCC.SomeDescription }, @@ -99,6 +105,15 @@ export const Files: React.FunctionComponent = ({ store }) => { + const { themeV9 } = useUserTheme(); + const footerStyles = React.useMemo(() => { + return { + zIndex: 2, + background: themeV9.colorNeutralBackground1, + borderTop: `1px solid ${themeV9.colorNeutralStroke1}` + }; + }, [themeV9]); + const hasFilter = React.useMemo(() => Object.keys(filter).length > 0, [filter]); const [showFilter, setShowFilter] = React.useState(false); @@ -117,6 +132,12 @@ export const Files: React.FunctionComponent = ({ total, setTotal, refreshTable } = useFluentStoreState({ page }); + const [selectedRows, setSelectedRows] = React.useState(new Set()); + const onSelectionChange = (items, rowIds) => { + setSelectedRows(rowIds); + setSelection(items); + }; + // Refresh on focus --- const isMounted = useIsMounted(); const hasFocus = useHasFocus(); @@ -136,104 +157,140 @@ export const Files: React.FunctionComponent = ({ return formatQuery(filter); }, [filter]); - const columns = React.useMemo((): FluentColumns => { + const columnSizingOptions = React.useMemo(() => { return { - col1: { - width: 16, - disabled: (item) => { - return item ? item.__hpcc_isDir : true; - }, - selectorType: "checkbox" - }, - IsProtected: { - headerIcon: "LockSolid", - headerTooltip: nlsHPCC.Protected, - width: 16, - sortable: false, - formatter: (_protected) => { - if (_protected === true) { - return ; - } - return ""; - }, - }, - IsCompressed: { - headerIcon: "ZipFolder", - headerTooltip: nlsHPCC.Compressed, - width: 16, - sortable: false, - formatter: (compressed) => { - if (compressed === true) { - return ; - } - return ""; - }, - }, - Name: { - label: nlsHPCC.LogicalName, - width: 360, - formatter: (name, row) => { - const file = Get(row.NodeGroup, name, row); - if (row.__hpcc_isDir) { - return name; - } - const url = "#/files/" + (row.NodeGroup ? row.NodeGroup + "/" : "") + name; - return <> - -   - {name} - ; - }, - }, - Owner: { label: nlsHPCC.Owner }, - SuperOwners: { label: nlsHPCC.SuperOwner, sortable: false }, - Description: { label: nlsHPCC.Description, sortable: false }, - NodeGroup: { label: nlsHPCC.Cluster }, - Records: { - label: nlsHPCC.Records, - formatter: (value, row) => { - return Utility.formatNum(row.IntRecordCount); - }, - }, - FileSize: { - label: nlsHPCC.Size, - formatter: (value, row) => { - return Utility.convertedSize(row.IntSize); - }, - }, - CompressedFileSizeString: { - label: nlsHPCC.CompressedSize, - formatter: (value, row) => { - return Utility.convertedSize(row.CompressedFileSize); - } - }, - Parts: { - label: nlsHPCC.Parts, width: 40, - }, - MinSkew: { - label: nlsHPCC.MinSkew, width: 60, formatter: (value, row) => value ? `${Utility.formatDecimal(value / 100)}%` : "" - }, - MaxSkew: { - label: nlsHPCC.MaxSkew, width: 60, formatter: (value, row) => value ? `${Utility.formatDecimal(value / 100)}%` : "" - }, - Modified: { label: nlsHPCC.ModifiedUTCGMT }, - Accessed: { label: nlsHPCC.LastAccessed }, - AtRestCost: { - label: nlsHPCC.FileCostAtRest, - formatter: (cost, row) => { - return `${formatCost(cost)}`; - }, - }, - AccessCost: { - label: nlsHPCC.FileAccessCost, - formatter: (cost, row) => { - return `${formatCost(cost)}`; - }, - } + IsProtected: { minWidth: 16, defaultWidth: 16 }, + IsCompressed: { minWidth: 16, defaultWidth: 16 }, + Name: { minWidth: 160, idealWidth: 360, defaultWidth: 360 }, + Owner: { minWidth: 90, idealWidth: 120, defaultWidth: 120 }, + SuperOwners: { minWidth: 90, idealWidth: 90, defaultWidth: 90 }, + Description: { minWidth: 60, idealWidth: 80, defaultWidth: 80 }, + Cluster: { minWidth: 64, defaultWidth: 64 }, + Records: { minWidth: 64, defaultWidth: 64 }, + Size: { minWidth: 32, defaultWidth: 64 }, + Parts: { minWidth: 32, defaultWidth: 64 }, + MinSkew: { minWidth: 64, defaultWidth: 64 }, + MaxSkew: { minWidth: 64, defaultWidth: 64 }, + Modified: { minWidth: 160, idealWidth: 160, defaultWidth: 160 }, + Accessed: { minWidth: 130, idealWidth: 130, defaultWidth: 130 }, }; }, []); - const copyButtons = useCopyButtons(columns, selection, "files"); + const columns: TableColumnDefinition[] = React.useMemo(() => [ + createTableColumn({ + columnId: "IsProtected", + renderHeaderCell: () => , + renderCell: (cell) => cell.IsProtected ? : "", + }), + createTableColumn({ + columnId: "IsCompressed", + renderHeaderCell: () => , + renderCell: (cell) => cell.IsCompressed ? : "", + }), + createTableColumn({ + columnId: "Name", + compare: (a, b) => a.Name?.localeCompare(b.Name), + renderHeaderCell: () => nlsHPCC.LogicalName, + renderCell: (cell) => { + return + {cell.Name} + ; + }, + }), + createTableColumn({ + columnId: "Owner", + compare: (a, b) => a.Owner?.localeCompare(b.Owner), + renderHeaderCell: () => nlsHPCC.Owner, + renderCell: (cell) => {cell?.Owner}, + }), + createTableColumn({ + columnId: "SuperOwners", + compare: (a, b) => a.SuperOwners?.localeCompare(b.SuperOwners), + renderHeaderCell: () => nlsHPCC.SuperOwner, + renderCell: (cell) => {cell?.SuperOwners}, + }), + createTableColumn({ + columnId: "Description", + compare: (a, b) => a.Description?.localeCompare(b.Description), + renderHeaderCell: () => nlsHPCC.Description, + renderCell: (cell) => {cell?.Description}, + }), + createTableColumn({ + columnId: "Cluster", + compare: (a, b) => a["Cluster"]?.localeCompare(b["Cluster"]), + renderHeaderCell: () => nlsHPCC.Cluster, + renderCell: (cell) => {cell["Cluster"]}, + }), + createTableColumn({ + columnId: "Records", + compare: (a, b) => a["Records"]?.localeCompare(b["Records"]), + renderHeaderCell: () => nlsHPCC.Records, + renderCell: (cell) => {cell["Records"]}, + }), + createTableColumn({ + columnId: "Size", + compare: (a, b) => a["Size"]?.localeCompare(b["Size"]), + renderHeaderCell: () => nlsHPCC.Size, + renderCell: (cell) => {cell["Size"]}, + }), + createTableColumn({ + columnId: "Parts", + compare: (a, b) => a["Parts"]?.localeCompare(b["Parts"]), + renderHeaderCell: () => nlsHPCC.Parts, + renderCell: (cell) => {cell["Parts"]}, + }), + createTableColumn({ + columnId: "MinSkew", + compare: (a, b) => a["MinSkew"] - b["MinSkew"], + renderHeaderCell: () => nlsHPCC.MinSkew, + renderCell: (cell) => {cell.MinSkew ? `${Utility.formatDecimal(cell.MinSkew / 100)}%` : ""}, + }), + createTableColumn({ + columnId: "MaxSkew", + compare: (a, b) => a["MaxSkew"] - b["MaxSkew"], + renderHeaderCell: () => nlsHPCC.MaxSkew, + renderCell: (cell) => {cell.MaxSkew ? `${Utility.formatDecimal(cell.MaxSkew / 100)}%` : ""}, + }), + createTableColumn({ + columnId: "Modified", + compare: (a, b) => a.Modified?.localeCompare(b.Modified), + renderHeaderCell: () => nlsHPCC.ModifiedUTCGMT, + renderCell: (cell) => {cell?.Modified}, + }), + createTableColumn({ + columnId: "Accessed", + compare: (a, b) => a.Accessed?.localeCompare(b.Accessed), + renderHeaderCell: () => nlsHPCC.LastAccessed, + renderCell: (cell) => {cell?.Accessed}, + }), + createTableColumn({ + columnId: "AtRestCost", + compare: (a, b) => a.AtRestCost - b.AtRestCost, + renderHeaderCell: () => nlsHPCC.FileCostAtRest, + renderCell: (cell) => {formatCost(cell.AtRestCost)}, + }), + createTableColumn({ + columnId: "AccessCost", + compare: (a, b) => a.AccessCost - b.AccessCost, + renderHeaderCell: () => nlsHPCC.FileAccessCost, + renderCell: (cell) => {formatCost(cell.AccessCost)}, + }), + ], []); + + const columnMap: Utility.ColumnMap = React.useMemo(() => { + const retVal: Utility.ColumnMap = {}; + columns.forEach((col, idx) => { + const columnId = col.columnId.toString(); + retVal[columnId] = { + id: `${columnId}_${idx}`, + field: columnId, + label: columnId + }; + }); + return retVal; + }, [columns]); + + const copyButtons = useCopyButtons(columnMap, selection, "files"); const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({ title: nlsHPCC.Delete, @@ -347,7 +404,7 @@ export const Files: React.FunctionComponent = ({ {({ size }) =>
- = ({ pageSize={pageSize} total={total} columns={columns} - height={`${size.height}px`} + sizingOptions={columnSizingOptions} + height={"calc(100vh - 176px)"} + onSelect={onSelectionChange} + selectedItems={selectedRows} setSelection={setSelection} setTotal={setTotal} refresh={refreshTable} - > + >
}
@@ -380,6 +440,6 @@ export const Files: React.FunctionComponent = ({ setPageSize={setPageSize} total={total} >} - footerStyles={{}} + footerStyles={footerStyles} />; }; diff --git a/esp/src/src-react/components/Queries.tsx b/esp/src/src-react/components/Queries.tsx index e11938a5734..3a05b7a891f 100644 --- a/esp/src/src-react/components/Queries.tsx +++ b/esp/src/src-react/components/Queries.tsx @@ -1,19 +1,26 @@ import * as React from "react"; -import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Icon, Link } from "@fluentui/react"; +import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link } from "@fluentui/react"; +import { TableCellLayout, TableColumnDefinition, TableRowId, createTableColumn, Tooltip } from "@fluentui/react-components"; +import { CheckmarkCircleFilled, ImportantFilled, ImportantRegular, PauseFilled, WarningRegular } from "@fluentui/react-icons"; +import { WsWorkunits as HPCCWsWorkunits } from "@hpcc-js/comms"; +import { SizeMe } from "react-sizeme"; import * as WsWorkunits from "src/WsWorkunits"; import * as ESPQuery from "src/ESPQuery"; +import { ColumnMap } from "src/Utility"; import nlsHPCC from "src/nlsHPCC"; import { QuerySortItem } from "src/store/Store"; import { useConfirm } from "../hooks/confirm"; +import { useUserTheme } from "../hooks/theme"; import { useMyAccount } from "../hooks/user"; import { useHasFocus, useIsMounted } from "../hooks/util"; import { HolyGrail } from "../layouts/HolyGrail"; import { pushParams } from "../util/history"; -import { FluentPagedGrid, FluentPagedFooter, useCopyButtons, useFluentStoreState, FluentColumns } from "./controls/Grid"; +import { FluentPagedDataGrid, FluentPagedFooter, useCopyButtons, useFluentStoreState } from "./controls/Grid"; import { Fields } from "./forms/Fields"; import { Filter } from "./forms/Filter"; import { ShortVerticalDivider } from "./Common"; -import { SizeMe } from "react-sizeme"; + +type Query = HPCCWsWorkunits.QuerySetQuery; const FilterFields: Fields = { "QueryID": { type: "string", label: nlsHPCC.ID, placeholder: nlsHPCC.QueryIDPlaceholder }, @@ -65,6 +72,15 @@ export const Queries: React.FunctionComponent = ({ store }) => { + const { themeV9 } = useUserTheme(); + const footerStyles = React.useMemo(() => { + return { + zIndex: 2, + background: themeV9.colorNeutralBackground1, + borderTop: `1px solid ${themeV9.colorNeutralStroke1}` + }; + }, [themeV9]); + const [showFilter, setShowFilter] = React.useState(false); const { currentUser } = useMyAccount(); const [uiState, setUIState] = React.useState({ ...defaultUIState }); @@ -75,6 +91,12 @@ export const Queries: React.FunctionComponent = ({ total, setTotal, refreshTable } = useFluentStoreState({ page }); + const [selectedRows, setSelectedRows] = React.useState(new Set()); + const onSelectionChange = (items, rowIds) => { + setSelectedRows(rowIds); + setSelection(items); + }; + const hasFilter = React.useMemo(() => Object.keys(filter).length > 0, [filter]); // Refresh on focus --- @@ -96,87 +118,134 @@ export const Queries: React.FunctionComponent = ({ return formatQuery(filter); }, [filter]); - const columns = React.useMemo((): FluentColumns => { + const columnSizingOptions = React.useMemo(() => { return { - col1: { - width: 16, - selectorType: "checkbox" - }, - Suspended: { - headerIcon: "Pause", - headerTooltip: nlsHPCC.Suspended, - width: 16, - sortable: false, - formatter: (suspended) => { - if (suspended === true) { - return ; - } - return ""; - } - }, - ErrorCount: { - headerIcon: "Warning", - headerTooltip: nlsHPCC.ErrorWarnings, - width: 16, - sortable: false, - formatter: (error) => { - if (error > 0) { - return ; - } - return ""; + Suspended: { minWidth: 16, defaultWidth: 16 }, + ErrorCount: { minWidth: 16, defaultWidth: 16 }, + MixedNodeStates: { minWidth: 16, defaultWidth: 16 }, + Activated: { minWidth: 16, defaultWidth: 16 }, + }; + }, []); + + const columns: TableColumnDefinition[] = React.useMemo(() => [ + createTableColumn({ + columnId: "Suspended", + renderHeaderCell: () => , + renderCell: (cell) => cell.Suspended ? : "", + }), + createTableColumn({ + columnId: "ErrorCount", + renderHeaderCell: () => , + renderCell: (cell) => cell.Clusters?.ClusterQueryState[0]?.Errors ? : "", + }), + createTableColumn({ + columnId: "MixedNodeStates", + renderHeaderCell: () => , + renderCell: (cell) => cell.Clusters?.ClusterQueryState[0]?.MixedNodeStates ? : "", + }), + createTableColumn({ + columnId: "Activated", + renderHeaderCell: () => , + renderCell: (cell) => cell.Activated ? : "", + }), + createTableColumn({ + columnId: "Id", + compare: (a, b) => a.Id.localeCompare(b.Id), + renderHeaderCell: () => nlsHPCC.ID, + renderCell: (cell) => {cell.Id}, + }), + createTableColumn({ + columnId: "priority", + compare: (a, b) => a.priority.localeCompare(b.priority), + renderHeaderCell: () => nlsHPCC.Priority, + renderCell: (cell) => {cell.priority}, + }), + createTableColumn({ + columnId: "Name", + compare: (a, b) => a.Name.localeCompare(b.Name), + renderHeaderCell: () => nlsHPCC.Name, + renderCell: (cell) => {cell.Name}, + }), + createTableColumn({ + columnId: "QuerySetId", + compare: (a, b) => a.QuerySetId.localeCompare(b.QuerySetId), + renderHeaderCell: () => nlsHPCC.Target, + renderCell: (cell) => {cell.QuerySetId}, + }), + createTableColumn({ + columnId: "Wuid", + compare: (a, b) => a.Wuid.localeCompare(b.Wuid), + renderHeaderCell: () => nlsHPCC.WUID, + renderCell: (cell) => {cell.Wuid}, + }), + createTableColumn({ + columnId: "Dll", + compare: (a, b) => a.Dll.localeCompare(b.Dll), + renderHeaderCell: () => nlsHPCC.CompileCost, + renderCell: (cell) => {cell.Dll}, + }), + createTableColumn({ + columnId: "PublishedBy", + compare: (a, b) => a.PublishedBy.localeCompare(b.PublishedBy), + renderHeaderCell: () => nlsHPCC.PublishedBy, + renderCell: (cell) => {cell.PublishedBy}, + }), + createTableColumn({ + columnId: "Status", + compare: (a, b) => { + let statusA = ""; + let statusB = ""; + if (a.Suspended) { + statusA = nlsHPCC.SuspendedByUser; } - }, - MixedNodeStates: { - headerIcon: "Error", - headerTooltip: nlsHPCC.MixedNodeStates, - width: 16, - sortable: false, - formatter: (mixed) => { - if (mixed === true) { - return ; + a?.Clusters?.ClusterQueryState?.some(state => { + if (state.Errors || state.State !== "Available") { + statusA = nlsHPCC.SuspendedByCluster; + } else if (state.MixedNodeStates) { + statusA = nlsHPCC.MixedNodeStates; } - return ""; - } - }, - Activated: { - headerIcon: "SkypeCircleCheck", - headerTooltip: nlsHPCC.Active, - width: 16, - formatter: (activated) => { - if (activated === true) { - return ; + }); + b?.Clusters?.ClusterQueryState?.some(state => { + if (state.Errors || state.State !== "Available") { + statusB = nlsHPCC.SuspendedByCluster; + } else if (state.MixedNodeStates) { + statusB = nlsHPCC.MixedNodeStates; } - return ""; - } - }, - Id: { - label: nlsHPCC.ID, - formatter: (Id, row) => { - return {Id}; - } - }, - priority: { - label: nlsHPCC.Priority, - width: 80, - formatter: (priority, row) => { - return priority === undefined ? "" : priority; - } + }); + return statusA.localeCompare(statusB); }, - Name: { label: nlsHPCC.Name }, - QuerySetId: { label: nlsHPCC.Target, sortable: true }, - Wuid: { - label: nlsHPCC.WUID, width: 100, - formatter: (Wuid, idx) => { - return {Wuid}; + renderHeaderCell: () => nlsHPCC.Status, + renderCell: (cell) => { + let statusMsg = ""; + if (cell.Suspended) { + statusMsg = nlsHPCC.SuspendedByUser; } + cell?.Clusters?.ClusterQueryState?.some(state => { + if (state.Errors || state.State !== "Available") { + statusMsg = nlsHPCC.SuspendedByCluster; + } else if (state.MixedNodeStates) { + statusMsg = nlsHPCC.MixedNodeStates; + } + }); + return {(statusMsg)}; }, - Dll: { label: nlsHPCC.Dll }, - PublishedBy: { label: nlsHPCC.PublishedBy }, - Status: { label: nlsHPCC.Status, sortable: false } - }; - }, []); + }), + ], []); + + const columnMap: ColumnMap = React.useMemo(() => { + const retVal: ColumnMap = {}; + columns.forEach((col, idx) => { + const columnId = col.columnId.toString(); + retVal[columnId] = { + id: `${columnId}_${idx}`, + field: columnId, + label: columnId + }; + }); + return retVal; + }, [columns]); - const copyButtons = useCopyButtons(columns, selection, "roxiequeries"); + const copyButtons = useCopyButtons(columnMap, selection, "roxiequeries"); const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({ title: nlsHPCC.Delete, @@ -289,7 +358,7 @@ export const Queries: React.FunctionComponent = ({ {({ size }) =>
- = ({ pageSize={pageSize} total={total} columns={columns} - height={`${size.height}px`} + sizingOptions={columnSizingOptions} + height={"calc(100vh - 176px)"} + onSelect={onSelectionChange} + selectedItems={selectedRows} setSelection={setSelection} setTotal={setTotal} refresh={refreshTable} - > + >
}
@@ -317,6 +389,6 @@ export const Queries: React.FunctionComponent = ({ setPageSize={setPageSize} total={total} >} - footerStyles={{}} + footerStyles={footerStyles} />; }; diff --git a/esp/src/src-react/components/Workunits.tsx b/esp/src/src-react/components/Workunits.tsx index b2047850ee7..f8584e41574 100644 --- a/esp/src/src-react/components/Workunits.tsx +++ b/esp/src/src-react/components/Workunits.tsx @@ -1,23 +1,28 @@ import * as React from "react"; -import { CommandBar, ContextualMenuItemType, DetailsRow, ICommandBarItemProps, IDetailsRowProps, Icon, Image, Link } from "@fluentui/react"; -import { hsl as d3Hsl } from "@hpcc-js/common"; +import { CommandBar, ContextualMenuItemType, /*DetailsRow,*/ ICommandBarItemProps, /*IDetailsRowProps,*/ Image, Link } from "@fluentui/react"; +import { TableCellLayout, TableColumnDefinition, TableRowId, createTableColumn, Tooltip } from "@fluentui/react-components"; +import { LockClosedFilled } from "@fluentui/react-icons"; +// import { hsl as d3Hsl } from "@hpcc-js/common"; +import { Workunit } from "@hpcc-js/comms"; import { SizeMe } from "react-sizeme"; -import { CreateWUQueryStore, defaultSort, emptyFilter, Get, WUQueryStore, formatQuery } from "src/ESPWorkunit"; -import * as WsWorkunits from "src/WsWorkunits"; +import { CreateWUQueryStore, defaultSort, emptyFilter, formatQuery, Get, WUQueryStore } from "src/ESPWorkunit"; +import { ColumnMap } from "src/Utility"; import { formatCost } from "src/Session"; +import * as WsWorkunits from "src/WsWorkunits"; +import { QuerySortItem } from "src/store/Store"; import nlsHPCC from "src/nlsHPCC"; import { useConfirm } from "../hooks/confirm"; +import { useUserTheme } from "../hooks/theme"; import { useMyAccount } from "../hooks/user"; import { useUserStore } from "../hooks/store"; -import { useLogicalClustersPalette } from "../hooks/platform"; -import { calcSearch, pushParams } from "../util/history"; -import { useHasFocus, useIsMounted } from "../hooks/util"; +// import { useLogicalClustersPalette } from "../hooks/platform"; import { HolyGrail } from "../layouts/HolyGrail"; -import { FluentPagedGrid, FluentPagedFooter, useCopyButtons, useFluentStoreState, FluentColumns } from "./controls/Grid"; +import { useHasFocus, useIsMounted } from "../hooks/util"; +import { pushParams } from "../util/history"; +import { FluentPagedDataGrid, FluentPagedFooter, useCopyButtons, useFluentStoreState } from "./controls/Grid"; import { Fields } from "./forms/Fields"; import { Filter } from "./forms/Filter"; import { ShortVerticalDivider } from "./Common"; -import { QuerySortItem } from "src/store/Store"; const FilterFields: Fields = { "Type": { type: "checkbox", label: nlsHPCC.ArchivedOnly }, @@ -59,6 +64,15 @@ export const Workunits: React.FunctionComponent = ({ store }) => { + const { themeV9 } = useUserTheme(); + const footerStyles = React.useMemo(() => { + return { + zIndex: 2, + background: themeV9.colorNeutralBackground1, + borderTop: `1px solid ${themeV9.colorNeutralStroke1}` + }; + }, [themeV9]); + const hasFilter = React.useMemo(() => Object.keys(filter).length > 0, [filter]); const [showFilter, setShowFilter] = React.useState(false); @@ -71,7 +85,13 @@ export const Workunits: React.FunctionComponent = ({ pageSize, setPageSize, total, setTotal, refreshTable } = useFluentStoreState({ page }); - const [, , palette] = useLogicalClustersPalette(); + // const [, , palette] = useLogicalClustersPalette(); + + const [selectedRows, setSelectedRows] = React.useState(new Set()); + const onSelectionChange = (items, rowIds) => { + setSelectedRows(rowIds); + setSelection(items); + }; // Refresh on focus --- const isMounted = useIsMounted(); @@ -92,71 +112,109 @@ export const Workunits: React.FunctionComponent = ({ return store ? store : CreateWUQueryStore(); }, [store]); - const columns = React.useMemo((): FluentColumns => { + const columnSizingOptions = React.useMemo(() => { return { - col1: { - width: 16, - selectorType: "checkbox" - }, - Protected: { - headerIcon: "LockSolid", - headerTooltip: nlsHPCC.Protected, - width: 16, - sortable: true, - formatter: (_protected) => { - if (_protected === true) { - return ; - } - return ""; - } - }, - Wuid: { - label: nlsHPCC.WUID, width: 120, - sortable: true, - formatter: (Wuid, row) => { - const wu = Get(Wuid); - const search = calcSearch(filter); - return <> - -   - {Wuid} - ; - } - }, - Owner: { label: nlsHPCC.Owner, width: 80 }, - Jobname: { label: nlsHPCC.JobName }, - Cluster: { label: nlsHPCC.Cluster }, - RoxieCluster: { label: nlsHPCC.RoxieCluster, sortable: false }, - State: { label: nlsHPCC.State, width: 60 }, - TotalClusterTime: { - label: nlsHPCC.TotalClusterTime, width: 120, - justify: "right", + Protected: { minWidth: 16, defaultWidth: 16 }, + Wuid: { minWidth: 160, defaultWidth: 160 }, + Cluster: { minWidth: 64, defaultWidth: 64 }, + RoxieCluster: { minWidth: 110, defaultWidth: 110 }, + TotalClusterTime: { minWidth: 120, defaultWidth: 120 }, + State: { minWidth: 64, defaultWidth: 64 }, + }; + }, []); + + const columns: TableColumnDefinition[] = React.useMemo(() => [ + createTableColumn({ + columnId: "Protected", + renderHeaderCell: () => , + renderCell: (cell) => cell.Protected ? : "", + }), + createTableColumn({ + columnId: "Wuid", + compare: (a, b) => a.Wuid.localeCompare(b.Wuid), + renderHeaderCell: () => nlsHPCC.WUID, + renderCell: (cell) => { + const wuid = cell.Wuid; + const wu = Get(wuid); + return +  {wuid} + ; }, - "Compile Cost": { - label: nlsHPCC.CompileCost, width: 100, - justify: "right", - formatter: (cost, row) => { - return `${formatCost(row.CompileCost)}`; - } + }), + createTableColumn({ + columnId: "Owner", + compare: (a, b) => a.Owner.localeCompare(b.Owner), + renderHeaderCell: () => nlsHPCC.Owner, + renderCell: (cell) => {cell.Owner}, + }), + createTableColumn({ + columnId: "Jobname", + compare: (a, b) => a.Jobname.localeCompare(b.Jobname), + renderHeaderCell: () => nlsHPCC.JobName, + renderCell: (cell) => {cell.Jobname}, + }), + createTableColumn({ + columnId: "Cluster", + compare: (a, b) => a.Cluster.localeCompare(b.Cluster), + renderHeaderCell: () => nlsHPCC.Cluster, + renderCell: (cell) => {cell.Cluster}, + }), + createTableColumn({ + columnId: "RoxieCluster", + compare: (a, b) => a.RoxieCluster.localeCompare(b.RoxieCluster), + renderHeaderCell: () => nlsHPCC.RoxieCluster, + renderCell: (cell) => {cell.RoxieCluster}, + }), + createTableColumn({ + columnId: "State", + compare: (a, b) => a.State.localeCompare(b.State), + renderHeaderCell: () => nlsHPCC.State, + renderCell: (cell) => {cell.State}, + }), + createTableColumn({ + columnId: "TotalClusterTime", + compare: (a, b) => { + const aNum = parseFloat(a.TotalClusterTime); + const bNum = parseFloat(b.TotalClusterTime); + return aNum - bNum; }, - "Execution Cost": { - label: nlsHPCC.ExecuteCost, width: 100, - justify: "right", - formatter: (cost, row) => { - return `${formatCost(row.ExecuteCost)}`; - } - }, - "File Access Cost": { - label: nlsHPCC.FileAccessCost, width: 100, - justify: "right", - formatter: (cost, row) => { - return `${formatCost(row.FileAccessCost)}`; - } - } - }; - }, [filter]); + renderHeaderCell: () => nlsHPCC.TotalClusterTime, + renderCell: (cell) => {cell.TotalClusterTime}, + }), + createTableColumn({ + columnId: "CompileCost", + compare: (a, b) => a.CompileCost - b.CompileCost, + renderHeaderCell: () => nlsHPCC.CompileCost, + renderCell: (cell) => {formatCost(cell.CompileCost)}, + }), + createTableColumn({ + columnId: "ExecutionCost", + compare: (a, b) => a.ExecuteCost - b.ExecuteCost, + renderHeaderCell: () => nlsHPCC.ExecuteCost, + renderCell: (cell) => {formatCost(cell.ExecuteCost)}, + }), + createTableColumn({ + columnId: "FileAccessCost", + compare: (a, b) => a.FileAccessCost - b.FileAccessCost, + renderHeaderCell: () => nlsHPCC.FileAccessCost, + renderCell: (cell) => {formatCost(cell.FileAccessCost)}, + }), + ], []); + + const columnMap: ColumnMap = React.useMemo(() => { + const retVal: ColumnMap = {}; + columns.forEach((col, idx) => { + const columnId = col.columnId.toString(); + retVal[columnId] = { + id: `${columnId}_${idx}`, + field: columnId, + label: columnId + }; + }); + return retVal; + }, [columns]); - const copyButtons = useCopyButtons(columns, selection, "workunits"); + const copyButtons = useCopyButtons(columnMap, selection, "workunits"); const doActionWithWorkunits = React.useCallback(async (action: "Delete" | "Abort") => { const unknownWUs = selection.filter(wu => wu.State === "unknown"); @@ -164,8 +222,10 @@ export const Workunits: React.FunctionComponent = ({ await WsWorkunits.WUAction(unknownWUs, "SetToFailed"); } await WsWorkunits.WUAction(selection, action); + setSelection([]); + setSelectedRows(null); refreshTable.call(true); - }, [refreshTable, selection]); + }, [refreshTable, selection, setSelection]); const [DeleteConfirm, setShowDeleteConfirm] = useConfirm({ title: nlsHPCC.Delete, @@ -287,6 +347,8 @@ export const Workunits: React.FunctionComponent = ({ setUIState(state); }, [selection]); + /* + // TODO: No such onRenderRow in Fluent v9 DataGrid const renderRowTimings = React.useCallback((props: IDetailsRowProps, size: { readonly width: number; readonly height: number; }) => { if (showTimeline && props) { const total = props.item.timings.page.end - props.item.timings.page.start; @@ -314,15 +376,16 @@ export const Workunits: React.FunctionComponent = ({ } return ; }, [palette, showTimeline]); + */ return } main={ <> {({ size }) => -
+
- = ({ pageSize={pageSize} total={total} columns={columns} - height={`${size.height}px`} + sizingOptions={columnSizingOptions} + height={"calc(100vh - 176px)"} + onSelect={onSelectionChange} + selectedItems={selectedRows} setSelection={setSelection} setTotal={setTotal} refresh={refreshTable} - onRenderRow={showTimeline ? props => renderRowTimings(props, size) : undefined} - > + >
} @@ -351,7 +416,7 @@ export const Workunits: React.FunctionComponent = ({ setPageNum={setPageNum} setPageSize={setPageSize} total={total} - >} - footerStyles={{}} + >} + footerStyles={footerStyles} />; }; diff --git a/esp/src/src-react/components/controls/DataGrid.tsx b/esp/src/src-react/components/controls/DataGrid.tsx new file mode 100644 index 00000000000..451d5df799d --- /dev/null +++ b/esp/src/src-react/components/controls/DataGrid.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { DataGridBody, DataGridProps, DataGridRow, DataGrid, DataGridHeader, DataGridHeaderCell, DataGridCell } from "@fluentui/react-components"; + +export type SortState = Parameters>[1]; + +interface DataGridV9Props { + items: any, + columns: any, + onSelect?: any, + onSortChange?: any, + selectedItems?: any, + sizingOptions?: any, + sortState?: any, +} + +export const DataGridV9: React.FunctionComponent = ({ + items, + columns, + selectedItems, + sortState, + onSelect, + onSortChange, + sizingOptions +}) => { + return { + console.log(data); + if (onSelect) onSelect(items.filter((el, idx) => data.selectedItems.has(idx)), data.selectedItems); + }} + focusMode="composite" + > + + + {({ renderHeaderCell }) => ( + {renderHeaderCell()} + )} + + + > + {({ item, rowId }) => ( + key={rowId} selectionCell={{ checkboxIndicator: { "aria-label": "Select row" }, }}> + {({ renderCell }) => ( + {renderCell(item)} + )} + + )} + + ; +}; \ No newline at end of file diff --git a/esp/src/src-react/components/controls/Grid.tsx b/esp/src/src-react/components/controls/Grid.tsx index fff30e920ca..6eff83d8699 100644 --- a/esp/src/src-react/components/controls/Grid.tsx +++ b/esp/src/src-react/components/controls/Grid.tsx @@ -1,14 +1,17 @@ import * as React from "react"; import { DetailsList, DetailsListLayoutMode, Dropdown, IColumn as _IColumn, ICommandBarItemProps, IDetailsHeaderProps, IDetailsListStyles, mergeStyleSets, Selection, Stack, TooltipHost, TooltipOverflowMode, IDetailsList, IRenderFunction, IDetailsRowProps } from "@fluentui/react"; +import { DataGridProps } from "@fluentui/react-components"; import { Pagination } from "@fluentui/react-experiments/lib/Pagination"; import { useConst, useId, useMount, useOnEvent } from "@fluentui/react-hooks"; import { BaseStore, Memory, QueryRequest, QuerySortItem } from "src/store/Memory"; import nlsHPCC from "src/nlsHPCC"; +import { ColumnMap } from "src/Utility"; import { createCopyDownloadSelection } from "../Common"; import { updatePage, updateSort } from "../../util/history"; import { useDeepCallback, useDeepEffect, useDeepMemo } from "../../hooks/deepHooks"; import { useUserStore, useNonReactiveEphemeralPageStore } from "../../hooks/store"; import { useUserTheme } from "../../hooks/theme"; +import { DataGridV9 } from "./DataGrid"; /* --- Debugging dependency changes --- * @@ -118,7 +121,7 @@ const gridStyles = (height: string): Partial => { }; }; -export function useCopyButtons(columns: FluentColumns, selection: any[], filename: string): ICommandBarItemProps[] { +export function useCopyButtons(columns: FluentColumns | ColumnMap, selection: any[], filename: string): ICommandBarItemProps[] { const memoizedColumns = useDeepMemo(() => columns, [], [columns]); @@ -328,6 +331,123 @@ const FluentStoreGrid: React.FunctionComponent = ({
; }; +interface FluentStoreDataGridProps { + store: any, + query?: QueryRequest, + sort?: QuerySortItem, + start: number, + count: number, + columns: any, + height: string, + refresh: RefreshTable, + onSelect: any, + selectedItems?: any, + setSelection: (selection: any[]) => void, + setTotal: (total: number) => void, + sizingOptions?: any +} + +const FluentStoreDataGrid: React.FunctionComponent = ({ + store, + query, + sort, + start, + count, + columns, + height, + refresh, + onSelect, + selectedItems, + setSelection, + setTotal, + sizingOptions +}) => { + const memoizedColumns = useDeepMemo(() => columns, [], [columns]); + const [items, setItems] = React.useState([]); + const [columnWidths] = useNonReactiveEphemeralPageStore("columnWidths"); + const [storeSort, setStoreSort] = React.useState(sort); + const [gridSort, setGridSort] = React.useState>[1]>({ sortColumn: sort?.attribute?.toString() ?? "", sortDirection: sort?.descending ? "descending" : "ascending" }); + + const selectionHandler = useConst(() => new Selection({ + onSelectionChanged: () => { + setSelection(selectionHandler.getSelection()); + } + })); + + const refreshTable = useDeepCallback((clearSelection = false) => { + if (isNaN(start) || isNaN(count)) return; + if (clearSelection) { + selectionHandler.setItems([], true); + } + const storeQuery = store.query({ ...query }, { start, count, sort: storeSort ? [storeSort] : undefined }); + storeQuery.total.then(total => { + setTotal(total); + }); + storeQuery.then(items => { + setItems(items); + setSelection(selectionHandler.getSelection()); + }); + }, [count, selectionHandler, start, store], [query, storeSort]); + + React.useEffect(() => { + // Dummy line to ensure its included in the dependency array --- + refresh.value; + refreshTable(refresh.clear); + }, [refresh.clear, refresh.value, refreshTable]); + + const fluentColumns: IColumn[] = React.useMemo(() => { + return columnsAdapter(memoizedColumns, columnWidths); + }, [columnWidths, memoizedColumns]); + + const datagridStyles = mergeStyleSets({ + wrapper: { + width: "auto", + height: "100%", + overflowY: "hidden", + ".fui-TableCellLayout": { + overflow: "hidden", + }, + ".fui-TableCellLayout__content": { + overflow: "hidden" + }, + ".fui-TableCellLayout__main": { + overflow: "hidden", + textOverflow: "ellipsis" + }, + ".fui-DataGridBody": { + maxHeight: "calc(100vh - 250px)", + overflowY: "auto" + } + } + }); + + React.useEffect(() => { + setStoreSort(sort); + }, [sort]); + + React.useEffect(() => { + updateColumnSorted(fluentColumns, sort?.attribute, sort?.descending); + }, [fluentColumns, sort]); + + const onSortChange: DataGridProps["onSortChange"] = React.useCallback((e, nextSortState) => { + updateSort(true, nextSortState.sortDirection !== "ascending", nextSortState.sortColumn.toString()); + setStoreSort({ attribute: nextSortState.sortColumn, descending: nextSortState.sortDirection === "descending" }); + setGridSort(nextSortState); + }, []); + + return
+ +
; +}; + interface FluentGridProps { data: any[], primaryID: string, @@ -418,6 +538,62 @@ export const FluentPagedGrid: React.FunctionComponent = ({ ; }; +interface FluentPagedDataGridProps { + store: BaseStore, + query?: QueryRequest, + sort?: QuerySortItem, + pageNum?: number, + pageSize: number, + total: number, + columns: any, + sizingOptions?: any, + height?: string, + onSelect?: any, + selectedItems?: any, + setSelection: (selection: any[]) => void, + setTotal: (total: number) => void, + refresh: RefreshTable +} + +export const FluentPagedDataGrid: React.FunctionComponent = ({ + store, + query, + sort, + pageNum = 1, + pageSize, + total, + columns, + sizingOptions, + height, + onSelect, + selectedItems, + setSelection, + setTotal, + refresh +}) => { + const [page, setPage] = React.useState(pageNum - 1); + const [sortBy, setSortBy] = React.useState(sort); + + React.useEffect(() => { + const maxPage = Math.ceil(total / pageSize) - 1; + if (maxPage >= 0 && page > maxPage) { // maxPage can be -1 if total is 0 + setPage(maxPage); + } + }, [page, pageSize, total]); + + React.useEffect(() => { + setSortBy(sort); + }, [sort]); + + React.useEffect(() => { + const _page = pageNum >= 1 ? pageNum - 1 : 0; + setPage(_page); + }, [pageNum]); + + return + ; +}; + interface FluentPagedFooterProps { persistID: string, pageNum?: number, @@ -452,7 +628,12 @@ export const FluentPagedFooter: React.FunctionComponent padding: "10px 12px 10px 6px", display: "grid", gridTemplateColumns: "9fr 1fr", - gridColumnGap: "10px" + gridColumnGap: "10px", + }, + footer: { + borderTop: `1px solid ${theme.palette.neutralLight}`, + zIndex: 2, + background: theme.palette.black }, pageControls: { ".ms-Pagination-container": {