Skip to content

Commit

Permalink
HPCC-31074 Refactor ECL Archive Widget to React
Browse files Browse the repository at this point in the history
Fixes resize issue with ECL Archive Widget.  Also is first step for direct
link to lines of ECL Code https://track.hpccsystems.com/browse/HPCC-30997

Signed-off-by: Gordon Smith <[email protected]>
  • Loading branch information
GordonSmith committed Jan 25, 2024
1 parent 628672e commit d1cd17e
Show file tree
Hide file tree
Showing 9 changed files with 582 additions and 38 deletions.
119 changes: 119 additions & 0 deletions esp/src/src-react/components/ECLArchive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as React from "react";
import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react";
import { WUDetails, IScope } from "@hpcc-js/comms";
import nlsHPCC from "src/nlsHPCC";
import { useWorkunitArchive } from "../hooks/workunit";
import { useWorkunitMetrics } from "../hooks/metrics";
import { HolyGrail } from "../layouts/HolyGrail";
import { DockPanel, DockPanelItem, ResetableDockPanel } from "../layouts/DockPanel";
import { pushUrl } from "../util/history";
import { ShortVerticalDivider } from "./Common";
import { ECLArchiveTree } from "./ECLArchiveTree";
import { ECLArchiveEditor } from "./ECLArchiveEditor";
import { MetricsPropertiesTables } from "./MetricsPropertiesTables";

const scopeFilterDefault: WUDetails.RequestNS.ScopeFilter = {
MaxDepth: 999999,
ScopeTypes: []
};

const nestedFilterDefault: WUDetails.RequestNS.NestedFilter = {
Depth: 999999,
ScopeTypes: ["activity"]
};

interface ECLArchiveProps {
wuid: string;
parentUrl?: string;
selection?: string;
}

export const ECLArchive: React.FunctionComponent<ECLArchiveProps> = ({
wuid,
parentUrl = `/workunits/${wuid}/eclsummary`,
selection
}) => {
const [fullscreen, setFullscreen] = React.useState<boolean>(false);
const [dockpanel, setDockpanel] = React.useState<ResetableDockPanel>();
const [_archiveXmlStr, _workunit2, _state2, archive, refreshArchive] = useWorkunitArchive(wuid);
const [metrics, _columns, _activities, _properties, _measures, _scopeTypes, _fetchStatus, refreshMetrics] = useWorkunitMetrics(wuid, scopeFilterDefault, nestedFilterDefault);
const [markers, setMarkers] = React.useState<{ lineNum: number, label: string }[]>([]);
const [selectionText, setSelectionText] = React.useState<string>("");
const [selectedMetrics, setSelectedMetrics] = React.useState<IScope[]>([]);

selection = selection ?? archive?.queryId();

React.useEffect(() => {
if (archive) {
archive?.updateMetrics(metrics);
}
}, [archive, metrics]);

React.useEffect(() => {
if (metrics.length) {
setSelectionText(archive?.content(selection) ?? "");
setMarkers(archive?.markers(selection) ?? []);
setSelectedMetrics(archive?.metrics(selection) ?? []);
}
}, [archive, metrics.length, selection]);

const setSelectedItem = React.useCallback((selId: string) => {
pushUrl(`${parentUrl}/${selId}`);
}, [parentUrl]);

React.useEffect(() => {
if (dockpanel) {
// Should only happen once on startup ---
const layout: any = dockpanel.layout();
if (Array.isArray(layout?.main?.sizes) && layout.main.sizes.length === 2) {
layout.main.sizes = [0.3, 0.7];
dockpanel.layout(layout).lazyRender();
}
}
}, [dockpanel]);

// Command Bar ---
const buttons = React.useMemo((): ICommandBarItemProps[] => [
{
key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
onClick: () => {
refreshArchive();
refreshMetrics();
pushUrl(`${parentUrl}`);
}
},
{ key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
], [parentUrl, refreshArchive, refreshMetrics]);

const rightButtons = React.useMemo((): ICommandBarItemProps[] => [
{
key: "copy", text: nlsHPCC.CopyToClipboard, disabled: !navigator?.clipboard?.writeText, iconOnly: true, iconProps: { iconName: "Copy" },
onClick: () => {
navigator?.clipboard?.writeText(selectionText);
}
}, {
key: "fullscreen", title: nlsHPCC.MaximizeRestore, iconProps: { iconName: fullscreen ? "ChromeRestore" : "FullScreen" },
onClick: () => setFullscreen(!fullscreen)
}
], [selectionText, fullscreen]);

return <HolyGrail fullscreen={fullscreen}
header={<CommandBar items={buttons} farItems={rightButtons} />}
main={
<DockPanel hideSingleTabs onDockPanelCreate={setDockpanel}>
<DockPanelItem key="scopesTable" title="Files" >
{ // Only render after archive is loaded (to ensure it "defaults to open") ---
archive?.modAttrs.length &&
<ECLArchiveTree archive={archive} selectedAttrIDs={[selection]} setSelectedItem={setSelectedItem} />
}
</DockPanelItem>
<DockPanelItem key="eclEditor" title="ECL" padding={4} location="split-right" relativeTo="scopesTable">
<ECLArchiveEditor ecl={selectionText} markers={markers}></ECLArchiveEditor>
</DockPanelItem>
<DockPanelItem key="properties" title="Properties" location="split-bottom" relativeTo="scopesTable" >
<MetricsPropertiesTables scopes={selectedMetrics}></MetricsPropertiesTables>
</DockPanelItem>
</DockPanel>
}
/>;
};
54 changes: 54 additions & 0 deletions esp/src/src-react/components/ECLArchiveEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from "react";
import { useConst } from "@fluentui/react-hooks";
import { Palette } from "@hpcc-js/common";
import { ECLEditor } from "@hpcc-js/codemirror";
import { useUserTheme } from "../hooks/theme";
import { AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter";

const palette = Palette.rainbow("YlOrRd");

interface ECLArchiveProps {
ecl?: string;
readonly?: boolean;
markers?: { lineNum: number, label: string }[];
}

export const ECLArchiveEditor: React.FunctionComponent<ECLArchiveProps> = ({
ecl = "",
readonly = true,
markers = []
}) => {
const { isDark } = useUserTheme();

const editor = useConst(() =>
new ECLEditor()
.readOnly(true)
);

React.useEffect(() => {
editor
?.text(ecl)
?.readOnly(readonly)
?.option("theme", isDark ? "darcula" : "default")
?.lazyRender()
;
}, [ecl, editor, isDark, readonly]);

React.useEffect(() => {
const fontFamily = "Verdana";
const fontSize = 12;
const maxLabelWidth = Math.max(
...markers.map(marker => {
const color = palette(+marker.label, 0, 100);
editor?.addGutterMarker(+marker.lineNum - 1, marker.label, color, fontFamily, `${fontSize}px`);
return editor?.textSize(marker.label, fontFamily, fontSize)?.width ?? 0;
})
);
editor
?.gutterMarkerWidth(maxLabelWidth + 6)
?.lazyRender()
;
}, [editor, markers]);

return <AutosizeHpccJSComponent widget={editor}></AutosizeHpccJSComponent>;
};
104 changes: 104 additions & 0 deletions esp/src/src-react/components/ECLArchiveTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as React from "react";
import { FlatTree, useHeadlessFlatTree_unstable, HeadlessFlatTreeItemProps, TreeItem, TreeItemLayout, CounterBadge } from "@fluentui/react-components";
import { FluentIconsProps, FolderOpen20Regular, Folder20Regular, FolderOpen20Filled, Folder20Filled, Document20Regular, Document20Filled, Important16Regular } from "@fluentui/react-icons";
import { Archive, isAttribute } from "../util/metricArchive";

type FlatItem = HeadlessFlatTreeItemProps & { fileTimePct?: number, content: string };

const iconStyleProps: FluentIconsProps = {
primaryFill: "red",
};

const AsideContent = ({
isImportant,
messageCount,
}: {
isImportant?: boolean;
messageCount?: number;
}) => {
const color = messageCount < 50 ? "brand" :
messageCount < 70 ? "informative" :
messageCount < 90 ? "important" :
"danger";
return <>
{isImportant && <Important16Regular {...iconStyleProps} />}
{!isNaN(messageCount) && messageCount > 0 && (
<CounterBadge count={messageCount} color={color} size="small" />
)}
</>;
};

interface ECLArchiveTreeProps {
archive?: Archive;
selectedAttrIDs: string[];
setSelectedItem: (eclId: string, scopeID: string[]) => void;
}

export const ECLArchiveTree: React.FunctionComponent<ECLArchiveTreeProps> = ({
archive,
selectedAttrIDs = [],
setSelectedItem
}) => {

const defaultOpenItems = React.useMemo(() => {
return (archive?.modAttrs.filter(modAttr => modAttr.type === "Module") ?? []).map(modAttr => modAttr.id) ?? [];
}, [archive?.modAttrs]);

const [flatTreeItems, setFlatTreeItems] = React.useState<FlatItem[]>([]);
const flatTree = useHeadlessFlatTree_unstable(flatTreeItems, { defaultOpenItems });

React.useEffect(() => {
const flatTreeItems: FlatItem[] = [];
archive?.modAttrs.forEach(modAttr => {
flatTreeItems.push({
value: modAttr.id,
parentValue: modAttr.parentId ? modAttr.parentId : undefined,
content: modAttr.name,
fileTimePct: isAttribute(modAttr) && Math.round((archive?.sourcePathTime(modAttr.sourcePath) / archive?.timeTotalExecute) * 100),
});
});
setFlatTreeItems(flatTreeItems);
}, [archive, archive?.modAttrs, archive?.timeTotalExecute]);

const onClick = React.useCallback(evt => {
const attrId = evt.currentTarget?.dataset?.fuiTreeItemValue;
const modAttr = archive?.modAttrs.find(modAttr => modAttr.id === attrId);
if (modAttr?.type === "Attribute") {
setSelectedItem(attrId, archive.metricIDs(attrId));
}
}, [archive, setSelectedItem]);

const { ...treeProps } = flatTree.getTreeProps();
return <FlatTree {...treeProps} size="small">
{
Array.from(flatTree.items(), flatTreeItem => {
console.log(flatTreeItem.getTreeItemProps());
const { fileTimePct, content, ...treeItemProps } = flatTreeItem.getTreeItemProps();
return <TreeItem {...treeItemProps} onClick={onClick}>
<TreeItemLayout
// expandIcon={
// flatTreeItem.itemType === "branch" ?
// (treeProps.openItems.has(flatTreeItem.value) ?
// selectedItem?.startsWith(content) ? <FolderOpen20Filled /> : <FolderOpen20Regular /> :
// selectedItem?.startsWith(content) ? <Folder20Filled /> : <Folder20Regular />)
// :
// undefined
// }
iconBefore={
flatTreeItem.itemType === "branch" ?
(treeProps.openItems.has(flatTreeItem.value) ?
selectedAttrIDs.some(attrId => attrId.startsWith(content)) ? <FolderOpen20Filled /> : <FolderOpen20Regular /> :
selectedAttrIDs.some(attrId => attrId.startsWith(content)) ? <Folder20Filled /> : <Folder20Regular />) :
selectedAttrIDs.some(attrId => attrId === flatTreeItem.value) ?
<Document20Filled /> :
<Document20Regular />
}
aside={<AsideContent isImportant={false} messageCount={fileTimePct} />}
>
{content}
</TreeItemLayout>
</TreeItem>;
})
}
</FlatTree >;
};
26 changes: 2 additions & 24 deletions esp/src/src-react/components/Metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ErrorBoundary } from "../util/errorBoundary";
import { ShortVerticalDivider } from "./Common";
import { MetricsOptions } from "./MetricsOptions";
import { BreadcrumbInfo, OverflowBreadcrumb } from "./controls/OverflowBreadcrumb";
import { MetricsPropertiesTables } from "./MetricsPropertiesTables";

const logger = scopedLogger("src-react/components/Metrics.tsx");

Expand Down Expand Up @@ -360,28 +361,6 @@ export const Metrics: React.FunctionComponent<MetricsProps> = ({
}, [lineage, selectedLineage]);

// Props Table ---
const propsTable = useConst(() => new Table()
.columns([nlsHPCC.Property, nlsHPCC.Value, "Avg", "Min", "Max", "Delta", "StdDev", "SkewMin", "SkewMax", "NodeMin", "NodeMax"])
.columnWidth("auto")
);

const updatePropsTable = React.useCallback((scopes: IScope[]) => {
const props = [];
scopes.forEach((item, idx) => {
for (const key in item.__groupedProps) {
const row = item.__groupedProps[key];
props.push([row.Key, row.Value, row.Avg, row.Min, row.Max, row.Delta, row.StdDev, row.SkewMin, row.SkewMax, row.NodeMin, row.NodeMax]);
}
if (idx < scopes.length - 1) {
props.push(["------------------------------", "------------------------------"]);
}
});
propsTable
?.data(props)
?.lazyRender()
;
}, [propsTable]);

const propsTable2 = useConst(() => new Table()
.columns([nlsHPCC.Property, nlsHPCC.Value])
.columnWidth("auto")
Expand Down Expand Up @@ -437,7 +416,6 @@ export const Metrics: React.FunctionComponent<MetricsProps> = ({
React.useEffect(() => {
if (selectedMetrics) {
updateScopesTable(selectedMetrics);
updatePropsTable(selectedMetrics);
updatePropsTable2(selectedMetrics);
updateLineage(selectedMetrics);
}
Expand Down Expand Up @@ -569,7 +547,7 @@ export const Metrics: React.FunctionComponent<MetricsProps> = ({
/>
</DockPanelItem>
<DockPanelItem key="propsTable" title={nlsHPCC.Properties} location="split-bottom" relativeTo="scopesTable" >
<AutosizeHpccJSComponent widget={propsTable}></AutosizeHpccJSComponent>
<MetricsPropertiesTables scopes={selectedMetrics}></MetricsPropertiesTables>
</DockPanelItem>
<DockPanelItem key="propsTable2" title={nlsHPCC.CrossTab} location="tab-after" relativeTo="propsTable" >
<AutosizeHpccJSComponent widget={propsTable2}></AutosizeHpccJSComponent>
Expand Down
40 changes: 40 additions & 0 deletions esp/src/src-react/components/MetricsPropertiesTables.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from "react";
import { useConst } from "@fluentui/react-hooks";
import { IScope } from "@hpcc-js/comms";
import { Table } from "@hpcc-js/dgrid";
import nlsHPCC from "src/nlsHPCC";
import { AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter";

interface MetricsPropertiesTablesProps {
scopes?: IScope[];
}

export const MetricsPropertiesTables: React.FunctionComponent<MetricsPropertiesTablesProps> = ({
scopes = []
}) => {

// Props Table ---
const propsTable = useConst(() => new Table()
.columns([nlsHPCC.Property, nlsHPCC.Value, "Avg", "Min", "Max", "Delta", "StdDev", "SkewMin", "SkewMax", "NodeMin", "NodeMax"])
.columnWidth("auto")
);

React.useEffect(() => {
const props = [];
scopes.forEach((item, idx) => {
for (const key in item.__groupedProps) {
const row = item.__groupedProps[key];
props.push([row.Key, row.Value, row.Avg, row.Min, row.Max, row.Delta, row.StdDev, row.SkewMin, row.SkewMax, row.NodeMin, row.NodeMax]);
}
if (idx < scopes.length - 1) {
props.push(["------------------------------", "------------------------------"]);
}
});
propsTable
?.data(props)
?.lazyRender()
;
}, [propsTable, scopes]);

return <AutosizeHpccJSComponent widget={propsTable}></AutosizeHpccJSComponent>;
};
Loading

0 comments on commit d1cd17e

Please sign in to comment.