diff --git a/esp/src/src-react/components/FileDetails.tsx b/esp/src/src-react/components/FileDetails.tsx index 4111fe870f6..599339bc7da 100644 --- a/esp/src/src-react/components/FileDetails.tsx +++ b/esp/src/src-react/components/FileDetails.tsx @@ -105,7 +105,7 @@ export const FileDetails: React.FunctionComponent = ({ return {({ size }) =>
- + {file?.ContentType === "key" ? diff --git a/esp/src/src-react/components/Metrics.tsx b/esp/src/src-react/components/Metrics.tsx index ed3d5c14ae2..f31cf7a4f1f 100644 --- a/esp/src/src-react/components/Metrics.tsx +++ b/esp/src/src-react/components/Metrics.tsx @@ -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"; @@ -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"); @@ -303,7 +305,7 @@ export const Metrics: React.FunctionComponent = ({ 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 { @@ -321,7 +323,8 @@ export const Metrics: React.FunctionComponent = ({ 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) @@ -335,7 +338,11 @@ export const Metrics: React.FunctionComponent = ({ ; if (trackSelection && selectedMetricsSource !== "metricGraphWidget") { if (newSel.length) { - metricGraphWidget.zoomToSelection(0); + if (sameSVG) { + metricGraphWidget.centerOnSelection(); + } else { + metricGraphWidget.zoomToSelection(0); + } } else { metricGraphWidget.zoomToFit(0); } @@ -410,38 +417,42 @@ export const Metrics: React.FunctionComponent = ({ } 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(() => { + return lineage.map(item => { + return { + id: item.id, + label: item.id, + props: { + icon: selectedLineage === item ? : + } + }; + }); + }, [lineage, selectedLineage]); + const graphComponent = React.useMemo(() => { return - { - lineage.map((item, idx) => { - return <> - - : } onClick={() => setSelectedLineage(item)}> - {item.id} - - - {idx < lineage.length - 1 && } - ; - }) - } + setSelectedLineage(lineage.find(l => l.id === item.id))} /> } main={<> + } />; - }, [graphButtons, graphRightButtons, lineage, spinnerLabel, metricGraphWidget, selectedLineage]); + }, [graphButtons, graphRightButtons, breadcrumbs, selectedLineage?.id, spinnerLabel, selectedMetrics.length, metricGraphWidget, lineage]); // Props Table --- const propsTable = useConst(() => new Table() @@ -518,7 +529,7 @@ export const Metrics: React.FunctionComponent = ({ 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)) { diff --git a/esp/src/src-react/components/QueryDetails.tsx b/esp/src/src-react/components/QueryDetails.tsx index 92a58a014c6..40b7b97061d 100644 --- a/esp/src/src-react/components/QueryDetails.tsx +++ b/esp/src/src-react/components/QueryDetails.tsx @@ -99,7 +99,7 @@ export const QueryDetails: React.FunctionComponent = ({ return {({ size }) =>
- + diff --git a/esp/src/src-react/components/WorkunitDetails.tsx b/esp/src/src-react/components/WorkunitDetails.tsx index 66d016c479b..2373e601687 100644 --- a/esp/src/src-react/components/WorkunitDetails.tsx +++ b/esp/src/src-react/components/WorkunitDetails.tsx @@ -112,7 +112,7 @@ export const WorkunitDetails: React.FunctionComponent = ({ return {({ size }) =>
- + diff --git a/esp/src/src-react/components/controls/OverflowBreadcrumb.tsx b/esp/src/src-react/components/controls/OverflowBreadcrumb.tsx new file mode 100644 index 00000000000..227759b73f9 --- /dev/null +++ b/esp/src/src-react/components/controls/OverflowBreadcrumb.tsx @@ -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 = ({ + groupId, +}) => { + return + + ; +}; + +function icon(breadcrumb: BreadcrumbInfo, selected: string) { + return breadcrumb.id === selected ? : +} + +export interface OverflowBreadcrumbProps { + breadcrumbs: BreadcrumbInfo[]; + selected: string; + onSelect: (tab: BreadcrumbInfo) => void; +} + +export const OverflowBreadcrumb: React.FunctionComponent = ({ + breadcrumbs, + selected, + onSelect +}) => { + + const overflowItems = React.useMemo(() => { + return breadcrumbs.map((breadcrumb, idx) => <> + + + onSelect(breadcrumb)}> + {breadcrumb.label} + + + + {idx < breadcrumbs.length - 1 && } + ); + }, [breadcrumbs, onSelect, selected]); + + return + + {...overflowItems} + ({ ...breadcrumb, icon: icon(breadcrumb, selected) }))} onMenuSelect={onSelect} /> + + ; +}; diff --git a/esp/src/src-react/components/controls/TabbedPanes/OverflowMenu.tsx b/esp/src/src-react/components/controls/OverflowMenu.tsx similarity index 62% rename from esp/src/src-react/components/controls/TabbedPanes/OverflowMenu.tsx rename to esp/src/src-react/components/controls/OverflowMenu.tsx index 018b0e75ce6..fa41b4903ac 100644 --- a/esp/src/src-react/components/controls/TabbedPanes/OverflowMenu.tsx +++ b/esp/src/src-react/components/controls/OverflowMenu.tsx @@ -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>; }; -const OverflowMenuItem = (props: OverflowMenuItemProps) => { - const { tab, onClick } = props; - const isVisible = useIsOverflowItemVisible(tab.id); +const OverflowMenuItem: React.FunctionComponent = ({ + item, + onClick +}) => { + const isVisible = useIsOverflowItemVisible(item.id); if (isVisible) { return <>; } - return -
{tab.label}
+ return +
{item.label}
; }; @@ -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 = ({ - tabs, + menuItems, onMenuSelect }) => { const { ref, isOverflowing, overflowCount } = useOverflowMenu(); @@ -58,17 +67,17 @@ export const OverflowMenu: React.FunctionComponent = ({ className={styles.menuButton} ref={ref} icon={} - aria-label={`${overflowCount} more tabs`} - role="tab" + aria-label={`${overflowCount} more menu items`} + role="menuItem" /> - {tabs.map((tab) => ( + {menuItems.map((menuItem) => ( onMenuSelect(tab)} + key={menuItem.id} + item={menuItem} + onClick={() => onMenuSelect(menuItem)} /> ))} diff --git a/esp/src/src-react/components/controls/TabbedPanes/OverflowTabList.tsx b/esp/src/src-react/components/controls/TabbedPanes/OverflowTabList.tsx index 585d74c080b..e9cf3520ccf 100644 --- a/esp/src/src-react/components/controls/TabbedPanes/OverflowTabList.tsx +++ b/esp/src/src-react/components/controls/TabbedPanes/OverflowTabList.tsx @@ -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 = ({ tabs, - selectedTab, + selected, onTabSelect, size = "medium" }) => { @@ -24,23 +24,23 @@ export const OverflowTabList: React.FunctionComponent = ({ const tabsIndex = {}; return [tabs.map(tab => { tabsIndex[tab.id] = tab; - if (tab.id === selectedTab) { + if (tab.id === selected) { tab.__state = state; } - return + return {tab.label} ; }), tabsIndex]; - }, [selectedTab, state, tabs]); + }, [selected, state, tabs]); const localTabSelect = React.useCallback((evt: SelectTabEvent, data: SelectTabData) => { onTabSelect(tabsIndex[data.value as string]); }, [onTabSelect, tabsIndex]); return - + {...overflowItems} - + ; }; diff --git a/esp/src/src-react/util/metricGraph.ts b/esp/src/src-react/util/metricGraph.ts index b44d91217fb..ba799585eee 100644 --- a/esp/src/src-react/util/metricGraph.ts +++ b/esp/src/src-react/util/metricGraph.ts @@ -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; diff --git a/esp/src/src/nls/hpcc.ts b/esp/src/src/nls/hpcc.ts index 64f66489db3..3062e77c01f 100644 --- a/esp/src/src/nls/hpcc.ts +++ b/esp/src/src/nls/hpcc.ts @@ -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", @@ -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",