Skip to content

Commit

Permalink
Merge pull request #18199 from GordonSmith/HPCC-31074-ECL_ARCHIVE_REACT
Browse files Browse the repository at this point in the history
HPCC-31074 Refactor ECL Archive Widget to React
  • Loading branch information
GordonSmith authored Feb 6, 2024
2 parents 9f1a7c5 + 840ae72 commit dbf320f
Show file tree
Hide file tree
Showing 9 changed files with 572 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: ["graph"]
};

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>;
};
96 changes: 96 additions & 0 deletions esp/src/src-react/components/ECLArchiveTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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
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 dbf320f

Please sign in to comment.