Skip to content

Commit

Permalink
HPCC-31940 Add a visual gantt overlay onto the WUs page
Browse files Browse the repository at this point in the history
Signed-off-by: Gordon Smith <[email protected]>
  • Loading branch information
GordonSmith committed May 24, 2024
1 parent 8b0cc2c commit 8ebf785
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 14 deletions.
50 changes: 46 additions & 4 deletions esp/src/src-react/components/Workunits.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as React from "react";
import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Icon, Image, Link } from "@fluentui/react";
import { CommandBar, ContextualMenuItemType, DetailsRow, ICommandBarItemProps, IDetailsRowProps, Icon, Image, Link } from "@fluentui/react";
import { hsl as d3Hsl } from "@hpcc-js/common";
import { SizeMe } from "react-sizeme";
import { CreateWUQueryStore, defaultSort, emptyFilter, Get, WUQueryStore, formatQuery } from "src/ESPWorkunit";
import * as WsWorkunits from "src/WsWorkunits";
import { formatCost } from "src/Session";
import nlsHPCC from "src/nlsHPCC";
import { useConfirm } from "../hooks/confirm";
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 { HolyGrail } from "../layouts/HolyGrail";
Expand Down Expand Up @@ -61,12 +64,14 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
const [showFilter, setShowFilter] = React.useState(false);
const { currentUser } = useMyAccount();
const [uiState, setUIState] = React.useState({ ...defaultUIState });
const [showTimeline, setShowTimeline] = useUserStore<boolean>("workunits_showTimeline", true);
const {
selection, setSelection,
pageNum, setPageNum,
pageSize, setPageSize,
total, setTotal,
refreshTable } = useFluentStoreState({ page });
const [, , palette] = useLogicalClustersPalette();

// Refresh on focus ---
const isMounted = useIsMounted();
Expand Down Expand Up @@ -243,7 +248,15 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
pushParams(filter);
}
},
], [currentUser, filter, hasFilter, refreshTable, selection, setShowAbortConfirm, setShowDeleteConfirm, store, total, uiState.hasNotCompleted, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);
{ key: "divider_5", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
{
key: "timeline", text: nlsHPCC.Timeline, canCheck: true, checked: showTimeline, iconProps: { iconName: "TimelineProgress" },
onClick: () => {
setShowTimeline(!showTimeline);
refreshTable.call();
}
},
], [currentUser.username, filter, hasFilter, refreshTable, selection, setShowAbortConfirm, setShowDeleteConfirm, showTimeline, store, total, uiState.hasNotCompleted, uiState.hasNotProtected, uiState.hasProtected, uiState.hasSelection]);

Check warning on line 259 in esp/src/src-react/components/Workunits.tsx

View workflow job for this annotation

GitHub Actions / Check eclwatch and npm (20)

React Hook React.useMemo has a missing dependency: 'setShowTimeline'. Either include it or remove the dependency array

Check warning on line 259 in esp/src/src-react/components/Workunits.tsx

View workflow job for this annotation

GitHub Actions / Check eclwatch and npm (16)

React Hook React.useMemo has a missing dependency: 'setShowTimeline'. Either include it or remove the dependency array

Check warning on line 259 in esp/src/src-react/components/Workunits.tsx

View workflow job for this annotation

GitHub Actions / Check eclwatch and npm (18)

React Hook React.useMemo has a missing dependency: 'setShowTimeline'. Either include it or remove the dependency array

// Selection ---
React.useEffect(() => {
Expand Down Expand Up @@ -274,6 +287,34 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
setUIState(state);
}, [selection]);

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;
const startPct = 100 - (props.item.timings.start - props.item.timings.page.start) / total * 100;
const endPct = 100 - (props.item.timings.end - props.item.timings.page.start) / total * 100;
const backgroundColor = palette(props.item.Cluster);
const borderColor = d3Hsl(backgroundColor).darker().toString();

return <div style={{ position: "relative", width: `${size.width - 4}px` }}>
<DetailsRow {...props} />
<div style={{
position: "absolute",
top: 4,
bottom: 4,
left: `${endPct}%`,
width: `${startPct - endPct}%`,
backgroundColor,
borderColor,
borderWidth: 1,
borderStyle: "solid",
opacity: .33,
pointerEvents: "none"
}} />
</div>;
}
return <DetailsRow {...props} />;
}, [showTimeline]);

Check warning on line 316 in esp/src/src-react/components/Workunits.tsx

View workflow job for this annotation

GitHub Actions / Check eclwatch and npm (20)

React Hook React.useCallback has a missing dependency: 'palette'. Either include it or remove the dependency array

Check warning on line 316 in esp/src/src-react/components/Workunits.tsx

View workflow job for this annotation

GitHub Actions / Check eclwatch and npm (16)

React Hook React.useCallback has a missing dependency: 'palette'. Either include it or remove the dependency array

Check warning on line 316 in esp/src/src-react/components/Workunits.tsx

View workflow job for this annotation

GitHub Actions / Check eclwatch and npm (18)

React Hook React.useCallback has a missing dependency: 'palette'. Either include it or remove the dependency array

return <HolyGrail
header={<CommandBar items={buttons} farItems={copyButtons} />}
main={
Expand All @@ -293,6 +334,7 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
setSelection={setSelection}
setTotal={setTotal}
refresh={refreshTable}
onRenderRow={showTimeline ? props => renderRowTimings(props, size) : undefined}
></FluentPagedGrid>
</div>
</div>
Expand All @@ -302,14 +344,14 @@ export const Workunits: React.FunctionComponent<WorkunitsProps> = ({
<AbortConfirm />
</>
}
footer={<FluentPagedFooter
footer={< FluentPagedFooter
persistID={"workunits"}
pageNum={pageNum}
selectionCount={selection.length}
setPageNum={setPageNum}
setPageSize={setPageSize}
total={total}
></FluentPagedFooter>}
></FluentPagedFooter >}
footerStyles={{}}
/>;
};
21 changes: 14 additions & 7 deletions esp/src/src-react/components/controls/Grid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { DetailsList, DetailsListLayoutMode, Dropdown, IColumn as _IColumn, ICommandBarItemProps, IDetailsHeaderProps, IDetailsListStyles, mergeStyleSets, Selection, Stack, TooltipHost, TooltipOverflowMode, IDetailsList } from "@fluentui/react";
import { DetailsList, DetailsListLayoutMode, Dropdown, IColumn as _IColumn, ICommandBarItemProps, IDetailsHeaderProps, IDetailsListStyles, mergeStyleSets, Selection, Stack, TooltipHost, TooltipOverflowMode, IDetailsList, IRenderFunction, IDetailsRowProps } from "@fluentui/react";
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";
Expand Down Expand Up @@ -193,6 +193,7 @@ interface FluentStoreGridProps {
refresh: RefreshTable,
setSelection: (selection: any[]) => void,
setTotal: (total: number) => void,
onRenderRow?: IRenderFunction<IDetailsRowProps>
}

const FluentStoreGrid: React.FunctionComponent<FluentStoreGridProps> = ({
Expand All @@ -206,6 +207,7 @@ const FluentStoreGrid: React.FunctionComponent<FluentStoreGridProps> = ({
refresh,
setSelection,
setTotal,
onRenderRow
}) => {
const memoizedColumns = useDeepMemo(() => columns, [], [columns]);
const [sorted, setSorted] = React.useState<QuerySortItem>(sort);
Expand Down Expand Up @@ -320,6 +322,7 @@ const FluentStoreGrid: React.FunctionComponent<FluentStoreGridProps> = ({
onColumnHeaderClick={onColumnClick}
onRenderDetailsHeader={renderDetailsHeader}
onColumnResize={columnResize}
onRenderRow={onRenderRow}
styles={gridStyles(height)}
/>
</div>;
Expand All @@ -334,7 +337,8 @@ interface FluentGridProps {
height?: string,
setSelection: (selection: any[]) => void,
setTotal: (total: number) => void,
refresh: RefreshTable
refresh: RefreshTable,
onRenderRow?: IRenderFunction<IDetailsRowProps>
}

export const FluentGrid: React.FunctionComponent<FluentGridProps> = ({
Expand All @@ -346,7 +350,8 @@ export const FluentGrid: React.FunctionComponent<FluentGridProps> = ({
height,
setSelection,
setTotal,
refresh
refresh,
onRenderRow
}) => {

const constStore = useConst(() => new Memory(primaryID, alphaNumColumns));
Expand All @@ -357,7 +362,7 @@ export const FluentGrid: React.FunctionComponent<FluentGridProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [constStore, data, /*refresh*/]);

return <FluentStoreGrid store={constStore} columns={columns} sort={sort} start={0} count={data.length} height={height} setSelection={setSelection} setTotal={setTotal} refresh={refresh}>
return <FluentStoreGrid store={constStore} columns={columns} sort={sort} start={0} count={data.length} height={height} setSelection={setSelection} setTotal={setTotal} refresh={refresh} onRenderRow={onRenderRow}>
</FluentStoreGrid>;
};

Expand All @@ -372,7 +377,8 @@ interface FluentPagedGridProps {
height?: string,
setSelection: (selection: any[]) => void,
setTotal: (total: number) => void,
refresh: RefreshTable
refresh: RefreshTable,
onRenderRow?: IRenderFunction<IDetailsRowProps>
}

export const FluentPagedGrid: React.FunctionComponent<FluentPagedGridProps> = ({
Expand All @@ -386,7 +392,8 @@ export const FluentPagedGrid: React.FunctionComponent<FluentPagedGridProps> = ({
height,
setSelection,
setTotal,
refresh
refresh,
onRenderRow
}) => {
const [page, setPage] = React.useState(pageNum - 1);
const [sortBy, setSortBy] = React.useState(sort);
Expand All @@ -407,7 +414,7 @@ export const FluentPagedGrid: React.FunctionComponent<FluentPagedGridProps> = ({
setPage(_page);
}, [pageNum]);

return <FluentStoreGrid store={store} query={query} columns={columns} sort={sortBy} start={page * pageSize} count={pageSize} height={height} setSelection={setSelection} setTotal={setTotal} refresh={refresh}>
return <FluentStoreGrid store={store} query={query} columns={columns} sort={sortBy} start={page * pageSize} count={pageSize} height={height} setSelection={setSelection} setTotal={setTotal} refresh={refresh} onRenderRow={onRenderRow}>
</FluentStoreGrid>;
};

Expand Down
25 changes: 23 additions & 2 deletions esp/src/src-react/hooks/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Topology, WsTopology, WorkunitsServiceEx } from "@hpcc-js/comms";
import { getBuildInfo, BuildInfo, fetchModernMode } from "src/Session";
import { cmake_build_type, containerized, ModernMode } from "src/BuildInfo";
import { sessionKeyValStore, userKeyValStore } from "src/KeyValStore";
import { Palette } from "@hpcc-js/common";

const logger = scopedLogger("src-react/hooks/platform.ts");

Expand Down Expand Up @@ -36,14 +37,18 @@ export function useBuildInfo(): [BuildInfo, { isContainer: boolean, currencyCode
return [buildInfo, { isContainer, currencyCode, opsCategory }];
}

let g_targetCluster: Promise<WsTopology.TpLogicalCluster[]>;
export function useLogicalClusters(): [WsTopology.TpLogicalCluster[] | undefined, WsTopology.TpLogicalCluster | undefined] {
const [targetClusters, setTargetClusters] = React.useState<WsTopology.TpLogicalCluster[]>();
const [defaultCluster, setDefaultCluster] = React.useState<WsTopology.TpLogicalCluster>();

React.useEffect(() => {
const topology = Topology.attach({ baseUrl: "" });
if (!g_targetCluster) {
const topology = Topology.attach({ baseUrl: "" });
g_targetCluster = topology.fetchLogicalClusters();
}
let active = true;
topology.fetchLogicalClusters().then(response => {
g_targetCluster.then(response => {
if (active) {
setTargetClusters(response);
let firstRow: WsTopology.TpLogicalCluster;
Expand All @@ -70,6 +75,22 @@ export function useLogicalClusters(): [WsTopology.TpLogicalCluster[] | undefined
return [targetClusters, defaultCluster];
}

export function useLogicalClustersPalette(): [WsTopology.TpLogicalCluster[] | undefined, WsTopology.TpLogicalCluster | undefined, Palette.OrdinalPaletteFunc] {
const [targetClusters, defaultCluster] = useLogicalClusters();

const palette = useConst(() => Palette.ordinal("workunits", ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]));

React.useEffect(() => {
if (targetClusters) {
targetClusters.forEach(cluster => {
palette(cluster.Name);
});
}
}, [palette, targetClusters]);

return [targetClusters, defaultCluster, palette];
}

let wuCheckFeaturesPromise;
export const fetchCheckFeatures = () => {
if (!wuCheckFeaturesPromise) {
Expand Down
44 changes: 43 additions & 1 deletion esp/src/src/ESPWorkunit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1085,8 +1085,50 @@ export function CreateWUQueryStore(): BaseStore<WsWorkunitsNS.WUQuery, typeof Wo
request.Sortby = "ClusterTime";
}
return service.WUQuery(request).then(response => {
const page = {
start: undefined,
end: undefined
};
const data = response.Workunits.ECLWorkunit.map(wu => {
const start = Utility.wuidToDateTime(wu.Wuid);
if (!page.start || page.start > start) {
page.start = start;
}
let timePartsSection = 0;
const end = new Date(start);
const timeParts = wu.TotalClusterTime.split(":");
while (timeParts.length) {
const timePart = timeParts.pop();
switch (timePartsSection) {
case 0:
end.setSeconds(end.getSeconds() + +timePart);
break;
case 1:
end.setMinutes(end.getMinutes() + +timePart);
break;
case 2:
end.setHours(end.getHours() + +timePart);
break;
case 3:
end.setDate(end.getDate() + +timePart);
break;
}
++timePartsSection;
}
if (!page.end || page.end < end) {
page.end = end;
}
return {
...Get(wu.Wuid, wu),
timings: {
start,
end,
page
}
};
});
return {
data: response.Workunits.ECLWorkunit.map(wu => Get(wu.Wuid, wu)),
data,
total: response.NumWUs
};
});
Expand Down
4 changes: 4 additions & 0 deletions esp/src/src/Utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1303,4 +1303,8 @@ export function wuidToDate(wuid: string): string {

export function wuidToTime(wuid: string): string {
return `${wuid.substring(10, 12)}:${wuid.substring(12, 14)}:${wuid.substring(14, 16)}`;
}

export function wuidToDateTime(wuid: string): Date {
return new Date(`${wuidToDate(wuid)}T${wuidToTime(wuid)}Z`);
}

0 comments on commit 8ebf785

Please sign in to comment.