Skip to content

Commit

Permalink
HPCC-30964 Add overflow support to metrics breadcrumbs
Browse files Browse the repository at this point in the history
Remove activity and child from breadcrumbs

Signed-off-by: Gordon Smith <[email protected]>
  • Loading branch information
GordonSmith committed Dec 6, 2023
1 parent 82ba015 commit 235b331
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 52 deletions.
2 changes: 1 addition & 1 deletion esp/src/src-react/components/FileDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const FileDetails: React.FunctionComponent<FileDetailsProps> = ({

return <SizeMe monitorHeight>{({ size }) =>
<div style={{ height: "100%" }}>
<OverflowTabList tabs={tabs} selectedTab={tab} onTabSelect={onTabSelect} size="medium" />
<OverflowTabList tabs={tabs} selected={tab} onTabSelect={onTabSelect} size="medium" />
<DelayLoadedPanel visible={tab === "summary"} size={size}>
{file?.ContentType === "key"
? <IndexFileSummary cluster={cluster} logicalFile={logicalFile} />
Expand Down
53 changes: 32 additions & 21 deletions esp/src/src-react/components/Metrics.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from "react";
import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, IIconProps, SearchBox } from "@fluentui/react";
import { Breadcrumb, BreadcrumbButton, BreadcrumbDivider, BreadcrumbItem, Spinner } from "@fluentui/react-components";
import { Label, Spinner } from "@fluentui/react-components";
import { typographyStyles } from "@fluentui/react-theme";
import { useConst } from "@fluentui/react-hooks";
import { bundleIcon, Folder20Filled, Folder20Regular, FolderOpen20Filled, FolderOpen20Regular, } from "@fluentui/react-icons";
import { WorkunitsServiceEx } from "@hpcc-js/comms";
Expand All @@ -13,12 +14,13 @@ import { FetchStatus, useMetricsOptions, useWorkunitMetrics } from "../hooks/met
import { HolyGrail } from "../layouts/HolyGrail";
import { AutosizeComponent, AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter";
import { DockPanel, DockPanelItems, ReactWidget, ResetableDockPanel } from "../layouts/DockPanel";
import { IScope, MetricGraph, MetricGraphWidget, isGraphvizWorkerResponse, layoutCache } from "../util/metricGraph";
import { IScope, LayoutStatus, MetricGraph, MetricGraphWidget, isGraphvizWorkerResponse, layoutCache } from "../util/metricGraph";
import { pushUrl } from "../util/history";
import { debounce } from "../util/throttle";
import { ErrorBoundary } from "../util/errorBoundary";
import { ShortVerticalDivider } from "./Common";
import { MetricsOptions } from "./MetricsOptions";
import { BreadcrumbInfo, OverflowBreadcrumb } from "./controls/OverflowBreadcrumb";

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

Expand Down Expand Up @@ -303,7 +305,7 @@ export const Metrics: React.FunctionComponent<MetricsProps> = ({
for (let i = 0; i < minLen; ++i) {
const item = lineages[0][i];
if (lineages.every(lineage => lineage[i] === item)) {
if (metricGraph.isSubgraph(item) && item.id && !metricGraph.isVertex(item)) {
if (item.id && item.type !== "child" && metricGraph.isSubgraph(item) && !metricGraph.isVertex(item)) {
newLineage.push(item);
}
} else {
Expand All @@ -321,7 +323,8 @@ export const Metrics: React.FunctionComponent<MetricsProps> = ({
const updateMetricGraph = React.useCallback((svg: string, selection: IScope[]) => {
let cancelled = false;
if (metricGraphWidget?.renderCount() > 0) {
setIsRenderComplete(false);
const sameSVG = metricGraphWidget.svg() === svg;
setIsRenderComplete(sameSVG);
metricGraphWidget
.svg(svg)
.visible(false)
Expand All @@ -335,7 +338,11 @@ export const Metrics: React.FunctionComponent<MetricsProps> = ({
;
if (trackSelection && selectedMetricsSource !== "metricGraphWidget") {
if (newSel.length) {
metricGraphWidget.zoomToSelection(0);
if (sameSVG) {
metricGraphWidget.centerOnSelection();
} else {
metricGraphWidget.zoomToSelection(0);
}
} else {
metricGraphWidget.zoomToFit(0);
}
Expand Down Expand Up @@ -410,38 +417,42 @@ export const Metrics: React.FunctionComponent<MetricsProps> = ({
} else if (!isLayoutComplete) {
return `${nlsHPCC.PerformingLayout} (${dot.split("\n").length})`;
} else if (!isRenderComplete) {
return `${nlsHPCC.RenderSVG}`;
return nlsHPCC.RenderSVG;
}
return "";
}, [fetchStatus, isLayoutComplete, isRenderComplete, dot]);

const breadcrumbs = React.useMemo<BreadcrumbInfo[]>(() => {
return lineage.map(item => {
return {
id: item.id,
label: item.id,
props: {
icon: selectedLineage === item ? <SelectedLineageIcon /> : <LineageIcon />
}
};
});
}, [lineage, selectedLineage]);

const graphComponent = React.useMemo(() => {
return <HolyGrail
header={<>
<CommandBar items={graphButtons} farItems={graphRightButtons} />
<Breadcrumb>{
lineage.map((item, idx) => {
return <>
<BreadcrumbItem key={idx} >
<BreadcrumbButton current={selectedLineage === item} icon={selectedLineage === item ? <SelectedLineageIcon /> : <LineageIcon />} onClick={() => setSelectedLineage(item)}>
{item.id}
</BreadcrumbButton>
</BreadcrumbItem>
{idx < lineage.length - 1 && <BreadcrumbDivider />}
</>;
})
}</Breadcrumb>
<OverflowBreadcrumb breadcrumbs={breadcrumbs} selected={selectedLineage?.id} onSelect={item => setSelectedLineage(lineage.find(l => l.id === item.id))} />
</>}
main={<>
<AutosizeComponent hidden={!spinnerLabel}>
<Spinner size="extra-large" label={spinnerLabel} labelPosition="below"></Spinner>
<Spinner size="extra-large" label={spinnerLabel} labelPosition="below" ></Spinner>
</AutosizeComponent>
<AutosizeComponent hidden={!!spinnerLabel || selectedMetrics.length > 0}>
<Label style={{ ...typographyStyles.subtitle2 }}>{nlsHPCC.NoContentPleaseSelectItem}</Label>
</AutosizeComponent>
<AutosizeHpccJSComponent widget={metricGraphWidget}>
</AutosizeHpccJSComponent>
</>
}
/>;
}, [graphButtons, graphRightButtons, lineage, spinnerLabel, metricGraphWidget, selectedLineage]);
}, [graphButtons, graphRightButtons, breadcrumbs, selectedLineage?.id, spinnerLabel, selectedMetrics.length, metricGraphWidget, lineage]);

// Props Table ---
const propsTable = useConst(() => new Table()
Expand Down Expand Up @@ -518,7 +529,7 @@ export const Metrics: React.FunctionComponent<MetricsProps> = ({
React.useEffect(() => {
let cancelled = false;
if (metricGraphWidget?.renderCount() > 0) {
setIsLayoutComplete(false);
setIsLayoutComplete(layoutCache.status(dot) === LayoutStatus.COMPLETED);
layoutCache.calcSVG(dot).then(response => {
if (!cancelled) {
if (isGraphvizWorkerResponse(response)) {
Expand Down
2 changes: 1 addition & 1 deletion esp/src/src-react/components/QueryDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const QueryDetails: React.FunctionComponent<QueryDetailsProps> = ({

return <SizeMe monitorHeight>{({ size }) =>
<div style={{ height: "100%" }}>
<OverflowTabList tabs={tabs} selectedTab={tab} onTabSelect={onTabSelect} size="medium" />
<OverflowTabList tabs={tabs} selected={tab} onTabSelect={onTabSelect} size="medium" />
<DelayLoadedPanel visible={tab === "summary"} size={size}>
<QuerySummary queryId={queryId} querySet={querySet} />
</DelayLoadedPanel>
Expand Down
2 changes: 1 addition & 1 deletion esp/src/src-react/components/WorkunitDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({

return <SizeMe monitorHeight>{({ size }) =>
<div style={{ height: "100%" }}>
<OverflowTabList tabs={tabs} selectedTab={tab} onTabSelect={onTabSelect} size="medium" />
<OverflowTabList tabs={tabs} selected={tab} onTabSelect={onTabSelect} size="medium" />
<DelayLoadedPanel visible={tab === "summary"} size={size}>
<WorkunitSummary wuid={wuid} />
</DelayLoadedPanel>
Expand Down
62 changes: 62 additions & 0 deletions esp/src/src-react/components/controls/OverflowBreadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as React from "react";
import { Overflow, Breadcrumb, OverflowItem, BreadcrumbItem, BreadcrumbButton, BreadcrumbButtonProps, BreadcrumbDivider, OverflowDivider } from "@fluentui/react-components";
import { bundleIcon, Folder20Filled, Folder20Regular, FolderOpen20Filled, FolderOpen20Regular, } from "@fluentui/react-icons";
import { OverflowMenu } from "./OverflowMenu";

const LineageIcon = bundleIcon(Folder20Filled, Folder20Regular);
const SelectedLineageIcon = bundleIcon(FolderOpen20Filled, FolderOpen20Regular);

export interface BreadcrumbInfo {
id: string;
label: string;
props?: BreadcrumbButtonProps
}

interface OverflowGroupDividerProps {
groupId: string;
}

const OverflowGroupDivider: React.FunctionComponent<OverflowGroupDividerProps> = ({
groupId,
}) => {
return <OverflowDivider groupId={groupId}>
<BreadcrumbDivider data-group={groupId} />
</OverflowDivider>;
};

function icon(breadcrumb: BreadcrumbInfo, selected: string) {
return breadcrumb.id === selected ? <SelectedLineageIcon /> : <LineageIcon />;
}

export interface OverflowBreadcrumbProps {
breadcrumbs: BreadcrumbInfo[];
selected: string;
onSelect: (tab: BreadcrumbInfo) => void;
}

export const OverflowBreadcrumb: React.FunctionComponent<OverflowBreadcrumbProps> = ({
breadcrumbs,
selected,
onSelect
}) => {

const overflowItems = React.useMemo(() => {
return breadcrumbs.map((breadcrumb, idx) => <>
<OverflowItem id={breadcrumb.id} groupId={breadcrumb.id} key={`button-items-${breadcrumb.id}`} priority={breadcrumb.id === selected ? 1 : 1}>
<BreadcrumbItem>
<BreadcrumbButton {...breadcrumb.props} current={breadcrumb.id === selected} icon={icon(breadcrumb, selected)} onClick={() => onSelect(breadcrumb)}>
{breadcrumb.label}
</BreadcrumbButton>
</BreadcrumbItem>
</OverflowItem>
{idx < breadcrumbs.length - 1 && <OverflowGroupDivider groupId={breadcrumb.id} />}
</>);
}, [breadcrumbs, onSelect, selected]);

return <Overflow>
<Breadcrumb>
{...overflowItems}
<OverflowMenu menuItems={breadcrumbs.map(breadcrumb => ({ ...breadcrumb, icon: icon(breadcrumb, selected) }))} onMenuSelect={onSelect} />
</Breadcrumb>
</Overflow>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,35 @@ import * as React from "react";
import { makeStyles, tokens, Button, Menu, MenuList, MenuPopover, MenuTrigger, useOverflowMenu, useIsOverflowItemVisible, MenuItem, } from "@fluentui/react-components";
import { MoreHorizontalRegular, MoreHorizontalFilled, bundleIcon, } from "@fluentui/react-icons";
import type { ARIAButtonElement } from "@fluentui/react-aria";
import { TabInfo } from "./TabInfo";
import { Count } from "./Count";
import { Count } from "./TabbedPanes/Count";

const MoreHorizontal = bundleIcon(MoreHorizontalFilled, MoreHorizontalRegular);

export interface MenuItem {
id: string;
icon?: React.ReactElement;
label: string;
count?: string | number;
disabled?: boolean;
}

type OverflowMenuItemProps = {
tab: TabInfo;
item: MenuItem;
onClick: React.MouseEventHandler<ARIAButtonElement<"div">>;
};

const OverflowMenuItem = (props: OverflowMenuItemProps) => {
const { tab, onClick } = props;
const isVisible = useIsOverflowItemVisible(tab.id);
const OverflowMenuItem: React.FunctionComponent<OverflowMenuItemProps> = ({
item,
onClick
}) => {
const isVisible = useIsOverflowItemVisible(item.id);

if (isVisible) {
return <></>;
}

return <MenuItem key={tab.id} icon={tab.icon} disabled={tab.disabled} onClick={onClick}>
<div>{tab.label}<Count value={tab.count} /></div>
return <MenuItem key={item.id} icon={item.icon} disabled={item.disabled} onClick={onClick}>
<div>{item.label}<Count value={item.count} /></div>
</MenuItem>;
};

Expand All @@ -34,13 +43,13 @@ const useOverflowMenuStyles = makeStyles({
},
});

export type OverflowMenuProps = {
tabs: TabInfo[];
onMenuSelect: (tab: TabInfo) => void;
};
export interface OverflowMenuProps {
menuItems: readonly MenuItem[];
onMenuSelect: (menuItem: MenuItem) => void;
}

export const OverflowMenu: React.FunctionComponent<OverflowMenuProps> = ({
tabs,
menuItems,
onMenuSelect
}) => {
const { ref, isOverflowing, overflowCount } = useOverflowMenu<HTMLButtonElement>();
Expand All @@ -58,17 +67,17 @@ export const OverflowMenu: React.FunctionComponent<OverflowMenuProps> = ({
className={styles.menuButton}
ref={ref}
icon={<MoreHorizontal />}
aria-label={`${overflowCount} more tabs`}
role="tab"
aria-label={`${overflowCount} more menu items`}
role="menuItem"
/>
</MenuTrigger>
<MenuPopover>
<MenuList className={styles.menu}>
{tabs.map((tab) => (
{menuItems.map((menuItem) => (
<OverflowMenuItem
key={tab.id}
tab={tab}
onClick={() => onMenuSelect(tab)}
key={menuItem.id}
item={menuItem}
onClick={() => onMenuSelect(menuItem)}
/>
))}
</MenuList>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import * as React from "react";
import { Overflow, OverflowItem, SelectTabData, SelectTabEvent, Tab, TabList } from "@fluentui/react-components";
import { Count } from "./Count";
import { TabInfo } from "./TabInfo";
import { OverflowMenu } from "./OverflowMenu";
import { OverflowMenu } from "../OverflowMenu";

export interface OverflowTabListProps {
tabs: TabInfo[];
selectedTab: string;
selected: string;
onTabSelect: (tab: TabInfo) => void;
size?: "small" | "medium" | "large";
}

export const OverflowTabList: React.FunctionComponent<OverflowTabListProps> = ({
tabs,
selectedTab,
selected,
onTabSelect,
size = "medium"
}) => {
Expand All @@ -24,23 +24,23 @@ export const OverflowTabList: React.FunctionComponent<OverflowTabListProps> = ({
const tabsIndex = {};
return [tabs.map(tab => {
tabsIndex[tab.id] = tab;
if (tab.id === selectedTab) {
if (tab.id === selected) {
tab.__state = state;
}
return <OverflowItem key={tab.id} id={tab.id} priority={tab.id === selectedTab ? 2 : 1}>
return <OverflowItem key={tab.id} id={tab.id} priority={tab.id === selected ? 2 : 1}>
<Tab value={tab.id} icon={tab.icon} disabled={tab.disabled}>{tab.label}<Count value={tab.count} /></Tab>
</OverflowItem>;
}), tabsIndex];
}, [selectedTab, state, tabs]);
}, [selected, state, tabs]);

const localTabSelect = React.useCallback((evt: SelectTabEvent, data: SelectTabData) => {
onTabSelect(tabsIndex[data.value as string]);
}, [onTabSelect, tabsIndex]);

return <Overflow minimumVisible={2}>
<TabList selectedValue={selectedTab} onTabSelect={localTabSelect} size={size}>
<TabList selectedValue={selected} onTabSelect={localTabSelect} size={size}>
{...overflowItems}
<OverflowMenu onMenuSelect={onTabSelect} tabs={tabs} />
<OverflowMenu onMenuSelect={onTabSelect} menuItems={tabs} />
</TabList>
</Overflow>;
};
5 changes: 5 additions & 0 deletions esp/src/src-react/util/metricGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,11 @@ export class MetricGraphWidget extends SVGZoomWidget {
return this;
}

centerOnSelection(transitionDuration?: number) {
this.centerOnBBox(this.selectionBBox(), transitionDuration);
return this;
}

zoomToItem(scopeID: string) {
this.zoomToBBox(this.itemBBox(scopeID));
return this;
Expand Down
3 changes: 2 additions & 1 deletion esp/src/src/nls/hpcc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ export = {
NewPassword: "New Password",
NextSelection: "Next Selection",
NoContent: "(No content)",
NoContentPleaseSelectItem: "No content - please select an item",
NoCommon: "No Common",
noDataMessage: "...Zero Rows...",
Node: "Node",
Expand Down Expand Up @@ -688,9 +689,9 @@ export = {
PleaseSelectADynamicESDLService: "Please select a dynamic ESDL service",
PleaseSelectAServiceToBind: "Please select a service to bind",
PleaseSelectATopologyItem: "Please select a target, service or machine.",
Plugins: "Plugins",
PleaseEnterANumber: "Please enter a number 1 - ",
PleaseLogin: "Please log in using your username and password",
Plugins: "Plugins",
Pods: "Pods",
PodsAccessError: "Cannot retrieve list of pods",
Port: "Port",
Expand Down

0 comments on commit 235b331

Please sign in to comment.