Ver código fonte

Merge pull request #15203 from GordonSmith/HPCC-26260-NavMenu

HPCC-26260 React Navigation Menu

Reviewed-By: Jaman Brundage <jaman.brundage@lexisnexisrisk.com>
Reviewed-By: Richard Chapman <rchapman@hpccsystems.com>
Richard Chapman 3 anos atrás
pai
commit
6ce5194140

+ 4 - 0
esp/src/eclwatch/HPCCPlatformWidget.js

@@ -432,6 +432,10 @@ define([
             this.stackContainer.selectChild(this.widget._Config);
         },
 
+        _onOpenModernECLWatch: function (evt) {
+            window.open("/esp/files/index.html");
+        },
+
         _onOpenErrWarn: function (evt) {
             this.stackContainer.selectChild(this.errWarnPage);
         },

+ 2 - 0
esp/src/eclwatch/templates/HPCCPlatformWidget.html

@@ -21,6 +21,8 @@
                 <div id="${id}More" class="left glow" data-dojo-props="iconClass:'iconAdvanced', showLabel:false" data-dojo-type="dijit.form.DropDownButton">
                     <span>${i18n.Advanced}</span>
                     <div data-dojo-type="dijit.DropDownMenu">
+                        <div id="${id}ModernECLWatch" data-dojo-attach-event="onClick:_onOpenModernECLWatch" data-dojo-type="dijit.MenuItem">${i18n.OpenModernECLWatch}</div>
+                        <span data-dojo-type="dijit.MenuSeparator"></span>
                         <div id="${id}ErrWarn" data-dojo-attach-event="onClick:_onOpenErrWarn" data-dojo-type="dijit.MenuItem">${i18n.ErrorWarnings}</div>
                         <span data-dojo-type="dijit.MenuSeparator"></span>
                         <div id="${id}SetBanner" data-dojo-attach-event="onClick:_onSetBanner" data-dojo-type="dijit.MenuItem">${i18n.SetBanner}</div>

+ 12 - 2
esp/src/src-react/components/Activities.tsx

@@ -10,6 +10,8 @@ import { createCopyDownloadSelection, ShortVerticalDivider } from "./Common";
 import { DojoGrid, selector, tree } from "./DojoGrid";
 import { Summary } from "./DiskUsage";
 
+declare const dojoConfig;
+
 class DelayedRefresh {
     _promises: Promise<any>[] = [];
 
@@ -399,8 +401,16 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
         setUIState(state);
     }, [activity, selection]);
 
+    if (dojoConfig.isContainer) {
+        return <HolyGrail
+            header={<CommandBar items={buttons} overflowButtonProps={{}} farItems={rightButtons} />}
+            main={
+                <DojoGrid type="Sel" store={gridParams.store} query={gridParams.query} columns={gridParams.columns} setGrid={setGrid} setSelection={setSelection} />
+            }
+        />;
+    }
     return <ReflexContainer orientation="horizontal">
-        <ReflexElement minSize={100} size={100}>
+        <ReflexElement minSize={100} style={{ overflow: "hidden" }}>
             <Summary />
         </ReflexElement>
         <ReflexSplitter style={styles.reflexSplitter}>
@@ -414,5 +424,5 @@ export const Activities: React.FunctionComponent<ActivitiesProps> = ({
                 }
             />
         </ReflexElement>
-    </ReflexContainer >;
+    </ReflexContainer>;
 };

+ 32 - 0
esp/src/src-react/components/Breadcrumbs.tsx

@@ -0,0 +1,32 @@
+import { Breadcrumb, FontSizes, IBreadcrumbItem, IBreadcrumbStyleProps, IBreadcrumbStyles, IStyleFunctionOrObject } from "@fluentui/react";
+import * as React from "react";
+
+const breadCrumbStyles: IStyleFunctionOrObject<IBreadcrumbStyleProps, IBreadcrumbStyles> = {
+    root: { margin: 0 },
+    itemLink: { fontSize: FontSizes.size10, lineHeight: 20, paddingLeft: 2, paddingRight: 2 },
+};
+
+interface BreadcrumbsProps {
+    hashPath: string;
+    ignoreN?: number;
+}
+
+export const Breadcrumbs: React.FunctionComponent<BreadcrumbsProps> = ({
+    hashPath,
+    ignoreN = 0
+}) => {
+
+    const crumbs = React.useMemo(() => {
+        const paths = decodeURI(hashPath).split("/");
+
+        let fullPath = "#";
+        return [{ text: "", key: "home", href: "#/" },
+        ...paths.filter(path => !!path).map((path, idx) => {
+            const retVal: IBreadcrumbItem = { text: path.toUpperCase(), key: "" + idx, href: `${fullPath}/${path}` };
+            fullPath = `${fullPath}/${path}`;
+            return retVal;
+        }).filter((row, idx) => idx >= ignoreN)];
+    }, [hashPath, ignoreN]);
+
+    return <Breadcrumb items={crumbs} styles={breadCrumbStyles} />;
+};

+ 16 - 10
esp/src/src-react/components/Frame.tsx

@@ -1,28 +1,27 @@
 import * as React from "react";
 import { ThemeProvider } from "@fluentui/react";
+import { select as d3Select } from "@hpcc-js/common";
 import { HolyGrail } from "../layouts/HolyGrail";
 import { hashHistory } from "../util/history";
 import { router } from "../routes";
 import { darkTheme, lightTheme } from "../themes";
 import { DevTitle } from "./Title";
-import { DevMenu } from "./Menu";
+import { MainNavigation, SubNavigation } from "./Menu";
 
 interface DevFrameProps {
 }
 
 export const DevFrame: React.FunctionComponent<DevFrameProps> = () => {
 
-    const [location, setLocation] = React.useState<string>("");
-    const [paths, setPaths] = React.useState<string[]>([]);
+    const [location, setLocation] = React.useState<string>(window.location.hash.split("#").join(""));
     const [body, setBody] = React.useState(<h1>...loading...</h1>);
-    const [showMenu] = React.useState(true);
     const [useDarkMode, setUseDarkMode] = React.useState(false);
 
     React.useEffect(() => {
 
         const unlisten = hashHistory.listen(async (location, action) => {
+            console.log(location.pathname);
             setLocation(location.pathname);
-            setPaths(location.pathname.split("/"));
             document.title = `ECL Watch${location.pathname.split("/").join(" | ")}`;
             setBody(await router.resolve(location));
         });
@@ -32,13 +31,20 @@ export const DevFrame: React.FunctionComponent<DevFrameProps> = () => {
         return () => unlisten();
     }, []);
 
+    React.useEffect(() => {
+        d3Select("body")
+            .classed("flat-dark", useDarkMode)
+            ;
+    }, [useDarkMode]);
+
     return <ThemeProvider theme={useDarkMode ? darkTheme : lightTheme} style={{ height: "100%" }}>
         <HolyGrail
-            header={<DevTitle paths={paths} useDarkMode={useDarkMode} setUseDarkMode={setUseDarkMode} />}
-            left={showMenu ? <DevMenu location={location} /> : undefined}
-            main={<>
-                {body}
-            </>}
+            header={<DevTitle useDarkMode={useDarkMode} setUseDarkMode={setUseDarkMode} />}
+            left={<MainNavigation hashPath={location} useDarkMode={useDarkMode} setUseDarkMode={setUseDarkMode} />}
+            main={<HolyGrail
+                header={<SubNavigation hashPath={location} />}
+                main={body}
+            />}
         />
     </ThemeProvider >;
 };

+ 217 - 101
esp/src/src-react/components/Menu.tsx

@@ -1,125 +1,241 @@
 import * as React from "react";
-import { INavLink, INavLinkGroup, INavStyles, Nav } from "@fluentui/react";
-import { SizeMe } from "react-sizeme";
+import { IconButton, IContextualMenuItem, INavLinkGroup, Nav, Pivot, PivotItem, Stack, useTheme } from "@fluentui/react";
+import { useConst } from "@fluentui/react-hooks";
 import nlsHPCC from "src/nlsHPCC";
-import { useFavorites } from "../hooks/favorite";
-import { hashHistory } from "../util/history";
+import { MainNav, routes } from "../routes";
+import { pushUrl } from "../util/history";
+import { Breadcrumbs } from "./Breadcrumbs";
+import { useFavorites, useHistory } from "src-react/hooks/favorite";
 
+//  Top Level Nav  ---
 const navLinkGroups: INavLinkGroup[] = [
     {
-        name: "Favorites",
-        links: []
-    },
-    {
-        name: "History",
-        links: []
-    },
-    {
-        name: "Home",
-        links: [
-            { url: "#/activities", name: nlsHPCC.Activities },
-            { url: "#/activities/legacy", name: `${nlsHPCC.Activities} (L)` },
-            { url: "#/clusters", name: nlsHPCC.TargetClusters },
-            { url: "#/events", name: nlsHPCC.EventScheduler }
-        ]
-    },
-    {
-        name: "ECL",
-        links: [
-            { url: "#/workunits", name: nlsHPCC.Workunits },
-            { url: "#/workunits/dashboard", name: `${nlsHPCC.Workunits} (D)` },
-            { url: "#/workunits/legacy", name: `${nlsHPCC.Workunits} (L)` },
-            { url: "#/play", name: nlsHPCC.Playground },
-            { url: "#/play/legacy", name: `${nlsHPCC.Playground} (L)` },
-        ]
-    },
-    {
-        name: "Files",
         links: [
-            { url: "#/files", name: nlsHPCC.LogicalFiles },
-            { url: "#/files/legacy", name: `${nlsHPCC.LogicalFiles} (L)` },
-            { url: "#/landingzone", name: nlsHPCC.LandingZones },
-            { url: "#/dfuworkunits", name: nlsHPCC.Workunits },
-            { url: "#/dfuworkunits/legacy", name: `${nlsHPCC.Workunits} (L)` },
-            { url: "#/xref", name: nlsHPCC.XRef },
-        ]
-    },
-    {
-        name: "Published Queries",
-        links: [
-            { url: "#/queries", name: nlsHPCC.Queries },
-            { url: "#/queries/legacy", name: `${nlsHPCC.Queries} (L)` },
-            { url: "#/packagemaps", name: nlsHPCC.PackageMaps },
-        ]
-    },
-    {
-        name: "Operations",
-        links: [
-            { url: "#/topology", name: nlsHPCC.Topology },
-            { url: "#/diskusage", name: nlsHPCC.DiskUsage },
-            { url: "#/clusters2", name: nlsHPCC.TargetClusters },
-            { url: "#/processes", name: nlsHPCC.ClusterProcesses },
-            { url: "#/servers", name: nlsHPCC.SystemServers },
-            { url: "#/security", name: nlsHPCC.Security },
-            { url: "#/monitoring", name: nlsHPCC.Monitoring },
-            { url: "#/esdl", name: nlsHPCC.DESDL },
-            { url: "#/elk", name: nlsHPCC.LogVisualization },
+            {
+                name: nlsHPCC.Activities,
+                url: "#/activities",
+                icon: "Home",
+                key: "activities"
+            },
+            {
+                name: nlsHPCC.ECL,
+                url: "#/workunits",
+                icon: "SetAction",
+                key: "workunits"
+            },
+            {
+                name: nlsHPCC.Files,
+                url: "#/files",
+                icon: "PageData",
+                key: "files"
+            },
+            {
+                name: nlsHPCC.PublishedQueries,
+                url: "#/queries",
+                icon: "Globe",
+                key: "queries"
+            },
+            {
+                name: nlsHPCC.Operations,
+                url: "#/topology",
+                icon: "Admin",
+                key: "topology"
+            }
         ]
     }
 ];
-navLinkGroups.forEach(group => {
-    group.links.forEach(link => {
-        link.key = link.url.substr(1);
+
+const navIdx: { [id: string]: MainNav[] } = {};
+
+function append(route, path) {
+    if (!navIdx[path]) {
+        navIdx[path] = [];
+    }
+    route.mainNav?.forEach(item => {
+        navIdx[path].push(item);
     });
+}
+
+routes.forEach((route: any) => {
+    if (Array.isArray(route.path)) {
+        route.path.forEach(path => {
+            append(route, path);
+        });
+    } else {
+        append(route, route.path);
+    }
 });
 
-const navStyles = (width: number, height: number): Partial<INavStyles> => {
-    return {
-        root: {
-            width,
-            height,
-            boxSizing: "border-box",
-            border: "1px solid #eee",
-            overflow: "auto",
-        }
-    };
+function navSelectedKey(hashPath) {
+    const rootPath = navIdx[`/${hashPath?.split("/")[1]}`];
+    if (rootPath?.length) {
+        return rootPath[0];
+    }
+    return null;
+}
+
+const FIXED_WIDTH = 38;
+
+interface MainNavigationProps {
+    hashPath: string;
+    useDarkMode: boolean;
+    setUseDarkMode: (_: boolean) => void;
+}
+
+export const MainNavigation: React.FunctionComponent<MainNavigationProps> = ({
+    hashPath,
+    useDarkMode,
+    setUseDarkMode
+}) => {
+
+    const theme = useTheme();
+
+    const menu = useConst([...navLinkGroups]);
+
+    const selKey = React.useMemo(() => {
+        return navSelectedKey(hashPath);
+    }, [hashPath]);
+
+    return <Stack verticalAlign="space-between" styles={{ root: { width: `${FIXED_WIDTH}px`, height: "100%", position: "relative", backgroundColor: theme.palette.themeLighterAlt } }}>
+        <Stack.Item>
+            <Nav selectedKey={selKey} groups={menu} />
+        </Stack.Item>
+        <Stack.Item>
+            <IconButton iconProps={{ iconName: useDarkMode ? "Sunny" : "ClearNight" }} onClick={() => setUseDarkMode(!useDarkMode)} />
+            <IconButton iconProps={{ iconName: "Settings" }} onClick={() => { }} />
+        </Stack.Item>
+    </Stack>;
+};
+
+//  Second Level Nav  ---
+interface SubMenu {
+    headerText: string;
+    itemKey: string;
+}
+
+type SubMenuItems = { [nav: string]: SubMenu[] };
+
+const subMenuItems: SubMenuItems = {
+    "activities": [
+        { headerText: nlsHPCC.Activities, itemKey: "/activities" },
+        { headerText: nlsHPCC.Activities + " (L)", itemKey: "/activities/legacy" },
+        { headerText: nlsHPCC.TargetClusters, itemKey: "/clusters" },
+        { headerText: nlsHPCC.EventScheduler + " (L)", itemKey: "/events" }
+    ],
+    "workunits": [
+        { headerText: nlsHPCC.Workunits, itemKey: "/workunits" },
+        { headerText: nlsHPCC.Dashboard, itemKey: "/workunits/dashboard" },
+        { headerText: nlsHPCC.Workunits + " (L)", itemKey: "/workunits/legacy" },
+        { headerText: nlsHPCC.Playground, itemKey: "/play" },
+        { headerText: nlsHPCC.Playground + " (L)", itemKey: "/play/legacy" },
+    ],
+    "files": [
+        { headerText: nlsHPCC.LogicalFiles, itemKey: "/files" },
+        { headerText: nlsHPCC.LogicalFiles + " (L)", itemKey: "/files/legacy" },
+        { headerText: nlsHPCC.LandingZones, itemKey: "/landingzone" },
+        { headerText: nlsHPCC.Workunits, itemKey: "/dfuworkunits" },
+        { headerText: nlsHPCC.Workunits + " (L)", itemKey: "/dfuworkunits/legacy" },
+        { headerText: nlsHPCC.XRef, itemKey: "/xref" },
+    ],
+    "queries": [
+        { headerText: nlsHPCC.Queries, itemKey: "/queries" },
+        { headerText: nlsHPCC.Queries + " (L)", itemKey: "/queries/legacy" },
+        { headerText: nlsHPCC.PackageMaps, itemKey: "/packagemaps" }
+    ],
+    "topology": [
+        { headerText: nlsHPCC.Topology, itemKey: "/topology" },
+        { headerText: nlsHPCC.DiskUsage, itemKey: "/diskusage" },
+        { headerText: nlsHPCC.TargetClusters, itemKey: "/clusters2" },
+        { headerText: nlsHPCC.ClusterProcesses, itemKey: "/processes" },
+        { headerText: nlsHPCC.SystemServers, itemKey: "/servers" },
+        { headerText: nlsHPCC.Security, itemKey: "/security" },
+        { headerText: nlsHPCC.Monitoring, itemKey: "/monitoring" },
+        { headerText: nlsHPCC.DESDL, itemKey: "/esdl" },
+        { headerText: nlsHPCC.LogVisualization, itemKey: "/elk" },
+    ],
 };
 
-interface DevMenuProps {
-    location: string
+const subNavIdx: { [id: string]: string[] } = {};
+
+for (const key in subMenuItems) {
+    const subNav = subMenuItems[key];
+    subNav.forEach(item => {
+        if (!subNavIdx[item.itemKey]) {
+            subNavIdx[item.itemKey] = [];
+        }
+        subNavIdx[item.itemKey].push(key);
+    });
+}
+
+function subNavSelectedKey(hashPath) {
+    return !!subNavIdx[hashPath] ? hashPath : null;
 }
 
-const FIXED_WIDTH = 240;
+const handleLinkClick = (item?: PivotItem) => {
+    if (item?.props?.itemKey) {
+        pushUrl(item.props.itemKey);
+    }
+};
+
+interface SubNavigationProps {
+    hashPath: string;
+}
 
-export const DevMenu: React.FunctionComponent<DevMenuProps> = ({
-    location
+export const SubNavigation: React.FunctionComponent<SubNavigationProps> = ({
+    hashPath,
 }) => {
 
+    const theme = useTheme();
+
     const [favorites] = useFavorites();
-    const [menu, setMenu] = React.useState<INavLinkGroup[]>([...navLinkGroups]);
+    const [history] = useHistory();
 
-    React.useEffect(() => {
-        navLinkGroups[0].links = Object.keys(favorites).map((key): INavLink => {
-            return { url: key, name: key };
-        });
-        setMenu([...navLinkGroups]);
-    }, [favorites]);
+    const mainNav = React.useMemo(() => {
+        return navSelectedKey(hashPath);
+    }, [hashPath]);
 
-    React.useEffect(() => {
-        return hashHistory.listen((location, action) => {
-            navLinkGroups[1].links = hashHistory.recent().map((row): INavLink => {
-                return { url: `#${row.pathname + row.search}`, name: row.pathname };
+    const subNav = React.useMemo(() => {
+        return subNavSelectedKey(hashPath);
+    }, [hashPath]);
+
+    const altSubNav = React.useMemo(() => {
+        const parts = hashPath.split("/");
+        parts.shift();
+        return parts.shift();
+    }, [hashPath]);
+
+    const favoriteMenu: IContextualMenuItem[] = React.useMemo(() => {
+        const retVal: IContextualMenuItem[] = [];
+        for (const key in favorites) {
+            retVal.push({
+                name: decodeURI(key),
+                href: key,
+                key,
             });
-            setMenu([...navLinkGroups]);
-        });
-    }, []);
-
-    return <SizeMe monitorHeight>{({ size }) =>
-        <div style={{ width: `${FIXED_WIDTH}px`, height: "100%", position: "relative" }}>
-            <div style={{ position: "absolute" }}>
-                <Nav groups={menu} selectedKey={location} styles={navStyles(FIXED_WIDTH, size.height)} />
-            </div>
-        </div>
-    }
-    </SizeMe>;
+        }
+        return retVal;
+    }, [favorites]);
+
+    return <div style={{ backgroundColor: theme.palette.themeLighter }}>
+        <Stack horizontal horizontalAlign="space-between">
+            <Stack.Item align="center" grow={1}>
+                <Stack horizontal >
+                    <Stack.Item grow={0} >
+                        <Pivot selectedKey={subNav || altSubNav} onLinkClick={handleLinkClick} headersOnly={true} linkFormat="tabs" styles={{ root: { marginLeft: 4 }, text: { lineHeight: 20 }, link: { maxHeight: 20, marginRight: 4 }, linkContent: { maxHeight: 20 } }} >
+                            {subMenuItems[mainNav]?.map(row => <PivotItem headerText={row.headerText} itemKey={row.itemKey} />)}
+                        </Pivot>
+                    </Stack.Item>
+                    {!subNav &&
+                        <Stack.Item grow={1}>
+                            <Breadcrumbs hashPath={hashPath} ignoreN={1} />
+                        </Stack.Item>
+                    }
+                </Stack>
+            </Stack.Item>
+            <Stack.Item align="center" grow={0}>
+                <IconButton title={nlsHPCC.Advanced} iconProps={{ iconName: "History" }} menuProps={{ items: history }} />
+                <IconButton title={nlsHPCC.Advanced} iconProps={{ iconName: favoriteMenu.length === 0 ? "FavoriteStar" : "FavoriteStarFill" }} menuProps={{ items: favoriteMenu }} />
+            </Stack.Item>
+        </Stack>
+    </div>;
 };

+ 84 - 75
esp/src/src-react/components/Title.tsx

@@ -1,105 +1,114 @@
 import * as React from "react";
-import { Breadcrumb, ContextualMenuItemType, DefaultPalette, FontSizes, IBreadcrumbItem, IBreadcrumbStyleProps, IBreadcrumbStyles, IconButton, IContextualMenuProps, IIconProps, Image, IStyleFunctionOrObject, Link, SearchBox, Stack, Toggle } from "@fluentui/react";
+import { ContextualMenuItemType, DefaultButton, IconButton, IContextualMenuProps, IIconProps, Image, IPanelProps, IPersonaSharedProps, IRenderFunction, Panel, PanelType, Persona, PersonaSize, SearchBox, Stack, Text, useTheme } from "@fluentui/react";
 import { About } from "./About";
+import { useBoolean } from "@fluentui/react-hooks";
 
 import nlsHPCC from "src/nlsHPCC";
 
-const breadCrumbStyles: IStyleFunctionOrObject<IBreadcrumbStyleProps, IBreadcrumbStyles> = {
-    itemLink: { fontSize: FontSizes.size10, lineHeight: 14, paddingLeft: 2, paddingRight: 2 },
-};
-
 const collapseMenuIcon: IIconProps = { iconName: "CollapseMenu" };
 
+const advMenuProps = (setShowAbout): IContextualMenuProps => ({
+    items: [
+        { key: "legacy", text: nlsHPCC.OpenLegacyECLWatch, href: "/esp/files/stub.htm" },
+        { key: "divider_0", itemType: ContextualMenuItemType.Divider },
+        { key: "errors", href: "#/errors", text: nlsHPCC.ErrorWarnings, },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider },
+        { key: "banner", text: nlsHPCC.SetBanner },
+        { key: "toolbar", text: nlsHPCC.SetToolbar },
+        { key: "divider_2", itemType: ContextualMenuItemType.Divider },
+        { key: "docs", href: "https://hpccsystems.com/training/documentation/", text: nlsHPCC.Documentation, target: "_blank" },
+        { key: "downloads", href: "https://hpccsystems.com/download", text: nlsHPCC.Downloads, target: "_blank" },
+        { key: "releaseNotes", href: "https://hpccsystems.com/download/release-notes", text: nlsHPCC.ReleaseNotes, target: "_blank" },
+        {
+            key: "additionalResources", text: nlsHPCC.AdditionalResources, subMenuProps: {
+                items: [
+                    { key: "redBook", href: "https://wiki.hpccsystems.com/display/hpcc/HPCC+Systems+Red+Book", text: nlsHPCC.RedBook, target: "_blank" },
+                    { key: "forums", href: "https://hpccsystems.com/bb/", text: nlsHPCC.Forums, target: "_blank" },
+                    { key: "issues", href: "https://track.hpccsystems.com/issues/", text: nlsHPCC.IssueReporting, target: "_blank" },
+                ]
+            }
+        },
+        { key: "divider_3", itemType: ContextualMenuItemType.Divider },
+        { key: "lock", text: nlsHPCC.Lock },
+        { key: "logout", text: nlsHPCC.Logout },
+        { key: "divider_4", itemType: ContextualMenuItemType.Divider },
+        { key: "config", href: "#/config", text: nlsHPCC.Configuration },
+        { key: "about", text: nlsHPCC.About, onClick: () => setShowAbout(true) }
+    ],
+    directionalHintFixed: true
+});
+
+const waffleIcon: IIconProps = { iconName: "WaffleOffice365" };
+const searchboxStyles = { margin: "5px", height: "auto", width: "100%" };
+
+const examplePersona: IPersonaSharedProps = {
+    secondaryText: "Designer",
+    tertiaryText: "In a meeting",
+    optionalText: "Available at 4:00pm",
+};
+
 interface DevTitleProps {
-    paths: string[],
     useDarkMode: boolean,
     setUseDarkMode: (_: boolean) => void;
 }
 
 export const DevTitle: React.FunctionComponent<DevTitleProps> = ({
-    paths,
-    useDarkMode,
-    setUseDarkMode
 }) => {
 
     const [showAbout, setShowAbout] = React.useState(false);
+    const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
 
-    let fullPath = "#";
-    const itemsWithHref = [{ text: "HOME", key: "home", href: "#/" },
-    ...paths.filter(path => !!path).map((path, idx) => {
-        const retVal: IBreadcrumbItem = { text: path.toUpperCase(), key: "" + idx, href: `${fullPath}/${path}` };
-        fullPath = `${fullPath}/${path}`;
-        return retVal;
-    })];
+    const onRenderNavigationContent: IRenderFunction<IPanelProps> = React.useCallback(
+        (props, defaultRender) => (
+            <>
+                <IconButton iconProps={waffleIcon} onClick={dismissPanel} style={{ width: 48, height: 48 }} />
+                <span style={searchboxStyles} />
+                {defaultRender!(props)}
+            </>
+        ),
+        [dismissPanel],
+    );
 
-    const advMenuProps: IContextualMenuProps = {
-        items: [
-            { key: "errors", href: "#/errors", text: nlsHPCC.ErrorWarnings, },
-            { key: "divider_1", itemType: ContextualMenuItemType.Divider },
-            { key: "banner", text: nlsHPCC.SetBanner },
-            { key: "toolbar", text: nlsHPCC.SetToolbar },
-            { key: "divider_2", itemType: ContextualMenuItemType.Divider },
-            { key: "docs", href: "https://hpccsystems.com/training/documentation/", text: nlsHPCC.Documentation, target: "_blank" },
-            { key: "downloads", href: "https://hpccsystems.com/download", text: nlsHPCC.Downloads, target: "_blank" },
-            { key: "releaseNotes", href: "https://hpccsystems.com/download/release-notes", text: nlsHPCC.ReleaseNotes, target: "_blank" },
-            {
-                key: "additionalResources", text: nlsHPCC.AdditionalResources, subMenuProps: {
-                    items: [
-                        { key: "redBook", href: "https://wiki.hpccsystems.com/display/hpcc/HPCC+Systems+Red+Book", text: nlsHPCC.RedBook, target: "_blank" },
-                        { key: "forums", href: "https://hpccsystems.com/bb/", text: nlsHPCC.Forums, target: "_blank" },
-                        { key: "issues", href: "https://track.hpccsystems.com/issues/", text: nlsHPCC.IssueReporting, target: "_blank" },
-                    ]
-                }
-            },
-            { key: "divider_3", itemType: ContextualMenuItemType.Divider },
-            { key: "lock", text: nlsHPCC.Lock },
-            { key: "logout", text: nlsHPCC.Logout },
-            { key: "divider_4", itemType: ContextualMenuItemType.Divider },
-            { key: "legacy", text: nlsHPCC.OpenLegacyECLWatch, href: "/esp/files/stub.htm" },
-            { key: "divider_5", itemType: ContextualMenuItemType.Divider },
-            { key: "config", href: "#/config", text: nlsHPCC.Configuration },
-            { key: "about", text: nlsHPCC.About, onClick: () => setShowAbout(true) }
-        ],
-        directionalHintFixed: true
-    };
+    const theme = useTheme();
 
-    return <>
-        <Stack tokens={{ padding: 9, childrenGap: 9 }} >
-            <Stack horizontal disableShrink horizontalAlign="space-between">
-                <Stack horizontal tokens={{ childrenGap: 18 }} >
-                    <Stack.Item align="center">
-                        <Link href="#/activities"><Image src="/esp/files/eclwatch/img/hpccsystems.png" /></Link>
-                    </Stack.Item>
-                    <Stack.Item align="center" styles={{ root: { minWidth: 240 } }}>
-                        <Breadcrumb items={itemsWithHref} styles={breadCrumbStyles} />
+    return <div style={{ backgroundColor: theme.palette.themeLight }}>
+        <Stack horizontal verticalAlign="center" horizontalAlign="space-between">
+            <Stack.Item align="center">
+                <Stack horizontal>
+                    <Stack.Item>
+                        <IconButton iconProps={waffleIcon} onClick={openPanel} style={{ width: 48, height: 48, color: theme.palette.themeDarker }} />
                     </Stack.Item>
                     <Stack.Item align="center">
-                        <SearchBox onSearch={newValue => { window.location.href = `#/search/${newValue.trim()}`; }} placeholder={nlsHPCC.PlaceholderFindText} styles={{ root: { minWidth: 320 } }} />
+                        <Text variant="large" nowrap block ><b style={{ color: theme.palette.themeDarker }}>ECL Watch</b></Text>
                     </Stack.Item>
                 </Stack>
-                <Stack horizontal tokens={{ childrenGap: 18 }} >
-                    <Stack.Item align="center">
-                        <Toggle
-                            label="Change themes"
-                            onText="Dark Mode"
-                            offText="Light Mode"
-                            onChange={() => {
-                                setUseDarkMode(!useDarkMode);
-                                const themeChangeEvent = new CustomEvent("eclwatch-theme-toggle", {
-                                    detail: { dark: !useDarkMode }
-                                });
-                                document.dispatchEvent(themeChangeEvent);
-                            }}
-                        />
+            </Stack.Item>
+            <Stack.Item align="center">
+                <SearchBox onSearch={newValue => { window.location.href = `#/search/${newValue.trim()}`; }} placeholder={nlsHPCC.PlaceholderFindText} styles={{ root: { minWidth: 320 } }} />
+            </Stack.Item>
+            <Stack.Item align="center" >
+                <Stack horizontal>
+                    <Stack.Item>
+                        <Persona {...examplePersona} text="Jane Doe" size={PersonaSize.size32} />
                     </Stack.Item>
                     <Stack.Item align="center">
-                        <IconButton title={nlsHPCC.Advanced} iconProps={collapseMenuIcon} menuProps={advMenuProps} />
+                        <IconButton title={nlsHPCC.Advanced} iconProps={collapseMenuIcon} menuProps={advMenuProps(setShowAbout)} />
                     </Stack.Item>
                 </Stack>
-            </Stack>
-        </Stack>
-        <Stack horizontal styles={{ root: { background: DefaultPalette.themeLighter } }} >
+            </Stack.Item>
         </Stack>
+        <Panel type={PanelType.smallFixedNear}
+            onRenderNavigationContent={onRenderNavigationContent}
+            headerText={nlsHPCC.Apps}
+            isLightDismiss
+            isOpen={isOpen}
+            onDismiss={dismissPanel}
+            hasCloseButton={false}
+        >
+            <DefaultButton text="Kibana" href="https://www.elastic.co/kibana/" target="_blank" onRenderIcon={() => <Image src="https://www.google.com/s2/favicons?domain=www.elastic.co" />} />
+            <DefaultButton text="K8s Dashboard" href="https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/" target="_blank" onRenderIcon={() => <Image src="https://www.google.com/s2/favicons?domain=kubernetes.io" />} />
+        </Panel>
         <About show={showAbout} onClose={() => setShowAbout(false)} ></About>
-    </>;
+    </div>;
 };
+

+ 25 - 3
esp/src/src-react/hooks/favorite.ts

@@ -1,8 +1,10 @@
 import * as React from "react";
 import { CallbackFunction, Observable } from "@hpcc-js/util";
 import { userKeyValStore } from "src/KeyValStore";
+import { IContextualMenuItem } from "@fluentui/react";
+import { hashHistory } from "src-react/util/history";
 
-const STORE_ID = "favorites";
+const STORE_FAVORITES_ID = "favorites";
 const STORE_CACHE_TIMEOUT = 10000;
 
 interface Payload {
@@ -21,7 +23,7 @@ class Favorites {
     private _prevPull: Promise<UrlMap>;
     private async pull(): Promise<UrlMap> {
         if (!this._prevPull) {
-            this._prevPull = this._store.get(STORE_ID).then((str: string): UrlMap => {
+            this._prevPull = this._store.get(STORE_FAVORITES_ID).then((str: string): UrlMap => {
                 if (typeof str === "string") {
                     try {
                         const retVal = JSON.parse(str);
@@ -41,7 +43,7 @@ class Favorites {
 
     private async push(favs: UrlMap): Promise<void> {
         this._prevPull = Promise.resolve(favs);
-        return this._store.set(STORE_ID, JSON.stringify(favs));
+        return this._store.set(STORE_FAVORITES_ID, JSON.stringify(favs));
     }
 
     async clear(): Promise<void> {
@@ -108,3 +110,23 @@ export function useFavorites(): [UrlMap] {
 
     return [all];
 }
+
+export function useHistory(): [IContextualMenuItem[]] {
+
+    const [history, setHistory] = React.useState<IContextualMenuItem[]>([]);
+
+    React.useEffect(() => {
+        return hashHistory.listen((location, action) => {
+            setHistory(hashHistory.recent().map((row): IContextualMenuItem => {
+                const url = `#${row.pathname + row.search}`;
+                return {
+                    name: decodeURI(row.pathname),
+                    href: url,
+                    key: url
+                };
+            }));
+        });
+    }, []);
+
+    return [history];
+}

+ 33 - 0
esp/src/src-react/index.css

@@ -0,0 +1,33 @@
+@import "./themes.css";
+
+.flat .dojo-component a:link {
+    color:var(--themePrimary)  
+}
+
+.flat .dojo-component a:visited {
+    color:var(--themePrimary)  
+}
+
+.flat .dojo-component a:hover {
+    color:var(--themeDarker)  
+}
+
+.flat .dojo-component a:active {
+    color:var(--themeDarker)  
+}
+
+.flat-dark .dojo-component a:link {
+    color:var(--dark-themePrimary)  
+}
+
+.flat-dark .dojo-component a:visited {
+    color:var(--dark-themePrimary)  
+}
+
+.flat-dark .dojo-component a:hover {
+    color:var(--dark-themeDarker)  
+}
+
+.flat-dark .dojo-component a:active {
+    color:var(--dark-themeDarker)  
+}

+ 1 - 0
esp/src/src-react/index.tsx

@@ -6,6 +6,7 @@ import { initSession } from "src/Session";
 import "css!dojo-themes/flat/flat.css";
 import "css!hpcc/css/ecl.css";
 import "css!hpcc/css/hpcc.css";
+import "src-react-css/index.css";
 
 initializeIcons();
 

Diferenças do arquivo suprimidas por serem muito extensas
+ 89 - 20
esp/src/src-react/routes.tsx


+ 56 - 0
esp/src/src-react/themes.css

@@ -0,0 +1,56 @@
+/*
+ *  https://fabricweb.z5.web.core.windows.net/pr-deploy-site/refs/heads/7.0/theming-designer/index.html
+ *
+ *  Keep in sync with themes.ts
+ */
+
+:root {
+    --themePrimary: "#259ad6";
+    --themeLighterAlt: "#f5fbfd";
+    --themeLighter: "#d7edf8";
+    --themeLight: "#b6dff3";
+    --themeTertiary: "#74c0e7";
+    --themeSecondary: "#3ba6db";
+    --themeDarkAlt: "#218bc1";
+    --themeDark: "#1c76a3";
+    --themeDarker: "#145778";
+    --neutralLighterAlt: "#faf9f8";
+    --neutralLighter: "#f3f2f1";
+    --neutralLight: "#edebe9";
+    --neutralQuaternaryAlt: "#e1dfdd";
+    --neutralQuaternary: "#d0d0d0";
+    --neutralTertiaryAlt: "#c8c6c4";
+    --neutralTertiary: "#d1d1d1";
+    --neutralSecondary: "#a3a3a3";
+    --neutralPrimaryAlt: "#787878";
+    --neutralPrimary: "#666666";
+    --neutralDark: "#4e4e4e";
+    --black: "#393939";
+    --white: "#ffffff";
+
+    --dark-themePrimary: "#ff8600";
+    --dark-themeLighterAlt: "#0a0500";
+    --dark-themeLighter: "#291600";
+    --dark-themeLight: "#4d2900";
+    --dark-themeTertiary: "#995200";
+    --dark-themeSecondary: "#e07800";
+    --dark-themeDarkAlt: "#ff9419";
+    --dark-themeDark: "#ffa53d";
+    --dark-themeDarker: "#ffbc70";
+    --dark-neutralLighterAlt: "#323232";
+    --dark-neutralLighter: "#313131";
+    --dark-neutralLight: "#2f2f2f";
+    --dark-neutralQuaternaryAlt: "#2c2c2c";
+    --dark-neutralQuaternary: "#2a2a2a";
+    --dark-neutralTertiaryAlt: "#282828";
+    --dark-neutralTertiary: "#c8c8c8";
+    --dark-neutralSecondary: "#d0d0d0";
+    --dark-neutralPrimaryAlt: "#dadada";
+    --dark-neutralPrimary: "#ffffff";
+    --dark-neutralDark: "#f4f4f4";
+    --dark-black: "#f8f8f8";
+    --dark-white: "#333333";
+
+
+
+}

+ 5 - 1
esp/src/src-react/themes.ts

@@ -1,4 +1,8 @@
-//  https://fabricweb.z5.web.core.windows.net/pr-deploy-site/refs/heads/7.0/theming-designer/index.html
+/*
+ *  https://fabricweb.z5.web.core.windows.net/pr-deploy-site/refs/heads/7.0/theming-designer/index.html
+ *
+ *  Keep in sync with themes.css
+ */
 
 export const lightTheme = {
     palette: {

+ 21 - 2
esp/src/src-react/util/history.ts

@@ -1,6 +1,7 @@
 import UniversalRouter, { ResolveContext } from "universal-router";
 import { parse, ParsedQuery, stringify } from "query-string";
 import { hashSum } from "@hpcc-js/util";
+import { userKeyValStore } from "src/KeyValStore";
 
 let g_router: UniversalRouter;
 
@@ -48,6 +49,8 @@ export type ListenerCallback<S extends object = object> = (location: HistoryLoca
 
 const globalHistory = globalThis.history;
 
+const STORE_HISTORY_ID = "history";
+
 class History<S extends object = object> {
 
     location: HistoryLocation = {
@@ -56,6 +59,7 @@ class History<S extends object = object> {
         id: hashSum("#/")
     };
     state: S = {} as S;
+    _store = userKeyValStore();
 
     constructor() {
         this.location = parseHash(document.location.hash);
@@ -73,6 +77,18 @@ class History<S extends object = object> {
             console.log("popstate: " + document.location + ", state: " + JSON.stringify(ev.state));
             this.state = ev.state;
         });
+
+        this._store.get(STORE_HISTORY_ID).then((str: string) => {
+            if (typeof str === "string") {
+                const retVal: HistoryLocation[] = JSON.parse(str);
+                if (Array.isArray(retVal)) {
+                    this._recent = retVal;
+                }
+            }
+        }).catch(e => {
+        }).finally(() => {
+            this._recent = this._recent === undefined ? [] : this._recent;
+        });
     }
 
     push(to: { pathname?: string, search?: string }, state?: S) {
@@ -99,17 +115,20 @@ class History<S extends object = object> {
         };
     }
 
-    protected _recent: HistoryLocation[] = [];
+    protected _recent;
     recent() {
-        return this._recent;
+        return this._recent === undefined ? [] : this._recent;
     }
 
     updateRecent() {
+        if (this._recent !== undefined) {
         this._recent = this._recent?.filter(row => row.id !== this.location.id) || [];
         this._recent.unshift(this.location);
         if (this._recent.length > 10) {
             this._recent.length = 10;
         }
+            this._store.set(STORE_HISTORY_ID, JSON.stringify(this._recent));
+        }
     }
 
     broadcast(action: string) {

+ 3 - 0
esp/src/src/nls/hpcc.ts

@@ -46,6 +46,7 @@ export = {
         AllowWrite: "<center>Allow<br>Write</center>",
         AllQueuedItemsCleared: "All Queued items have been cleared. The current running job will continue to execute.",
         Analyze: "Analyze",
+        Apps: "Apps",
         ANY: "ANY",
         AnyAdditionalProcessesToFilter: "Any Addtional Processes To Filter",
         Append: "Append",
@@ -151,6 +152,7 @@ export = {
         CSV: "CSV",
         Dali: "Dali",
         DaliIP: "DaliIP",
+        Dashboard: "Dashboard",
         DataPatterns: "Data Patterns",
         DataPatternsNotStarted: "Analysis not found.  To start, press Analyze button above.",
         DataPatternsStarted: "Analyzing.  Once complete report will display here.",
@@ -515,6 +517,7 @@ export = {
         OpenInNewPageNoFrame: "Open in New Page (No Frame)",
         OpenLegacyECLWatch: "Open Legacy ECL Watch",
         OpenLegacyMode: "Open (legacy)",
+        OpenModernECLWatch: "Open Modern ECL Watch",
         OpenNativeMode: "Open (native)",
         OpenSource: "Open Source",
         Operation: "Operation",