Explorar el Código

HPCC-26026 Initial favorite support

Signed-off-by: Gordon Smith <GordonJSmith@gmail.com>
Gordon Smith hace 4 años
padre
commit
8aaaab9097

+ 19 - 4
esp/src/src-react/components/Menu.tsx

@@ -1,10 +1,15 @@
 import * as React from "react";
-import { INavLinkGroup, INavStyles, Nav } from "@fluentui/react";
+import { INavLink, INavLinkGroup, INavStyles, Nav } from "@fluentui/react";
 import { SizeMe } from "react-sizeme";
 import nlsHPCC from "src/nlsHPCC";
+import { useFavorites } from "../hooks/favorite";
 
 const navLinkGroups: INavLinkGroup[] = [
     {
+        name: "Favorites",
+        links: []
+    },
+    {
         name: "Home",
         links: [
             { url: "#/activities", name: nlsHPCC.Activities },
@@ -78,16 +83,26 @@ interface DevMenuProps {
     location: string
 }
 
+const FIXED_WIDTH = 240;
+
 export const DevMenu: React.FunctionComponent<DevMenuProps> = ({
     location
 }) => {
 
-    const fixedWidth = 240;
+    const [favorites] = useFavorites();
+    const [menu, setMenu] = React.useState<INavLinkGroup[]>([...navLinkGroups]);
+
+    React.useEffect(() => {
+        navLinkGroups[0].links = Object.keys(favorites).map((key): INavLink => {
+            return { url: key, name: key };
+        });
+        setMenu([...navLinkGroups]);
+    }, [favorites]);
 
     return <SizeMe monitorHeight>{({ size }) =>
-        <div style={{ width: `${fixedWidth}px`, height: "100%", position: "relative" }}>
+        <div style={{ width: `${FIXED_WIDTH}px`, height: "100%", position: "relative" }}>
             <div style={{ position: "absolute" }}>
-                <Nav groups={navLinkGroups} selectedKey={location} styles={navStyles(fixedWidth, size.height)} />
+                <Nav groups={menu} selectedKey={location} styles={navStyles(FIXED_WIDTH, size.height)} />
             </div>
         </div>
     }

+ 15 - 2
esp/src/src-react/components/Result.tsx

@@ -7,9 +7,10 @@ import { WUResult } from "@hpcc-js/eclwatch";
 import nlsHPCC from "src/nlsHPCC";
 import { ESPBase } from "src/ESPBase";
 import { csvEncode } from "src/Utility";
+import { useFavorite } from "../hooks/favorite";
 import { HolyGrail } from "../layouts/HolyGrail";
-import { pushParams } from "../util/history";
 import { AutosizeHpccJSComponent } from "../layouts/HpccJSAdapter";
+import { pushParams } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
 import { Fields } from "./forms/Fields";
 import { Filter } from "./forms/Filter";
@@ -231,6 +232,7 @@ export const Result: React.FunctionComponent<ResultProps> = ({
     const [result] = React.useState<CommsResult>(resultTable.calcResult());
     const [FilterFields, setFilterFields] = React.useState<Fields>({});
     const [showFilter, setShowFilter] = React.useState(false);
+    const [isFavorite, addFavorite, removeFavorite] = useFavorite(window.location.hash);
 
     React.useEffect(() => {
         result?.fetchXMLSchema().then(() => {
@@ -290,7 +292,18 @@ export const Result: React.FunctionComponent<ResultProps> = ({
                     { key: "csv", text: nlsHPCC.CSV, onClick: () => doDownload("csv", wuid, result.Sequence) },
                 ]
             }
-        }
+        },
+        {
+            key: "star", iconProps: { iconName: isFavorite ? "FavoriteStarFill" : "FavoriteStar" },
+            onClick: () => {
+                if (isFavorite) {
+                    removeFavorite();
+                } else {
+                    addFavorite();
+                }
+            }
+        },
+
     ];
 
     return <HolyGrail

+ 17 - 1
esp/src/src-react/components/WorkunitDetails.tsx

@@ -7,6 +7,7 @@ import { getImageURL } from "src/Utility";
 import { getStateIconClass } from "src/ESPWorkunit";
 import { WUStatus } from "src/react/index";
 import { useWorkunit } from "../hooks/Workunit";
+import { useFavorite } from "../hooks/favorite";
 import { DojoAdapter } from "../layouts/DojoAdapter";
 import { pushUrl } from "../util/history";
 import { ShortVerticalDivider } from "./Common";
@@ -75,6 +76,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
     const [jobname, setJobname] = React.useState("");
     const [description, setDescription] = React.useState("");
     const [_protected, setProtected] = React.useState(false);
+    const [isFavorite, addFavorite, removeFavorite] = useFavorite(window.location.hash);
 
     React.useEffect(() => {
         setJobname(jobname || workunit?.Jobname);
@@ -119,6 +121,20 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
 
     const protectedImage = getImageURL(workunit?.Protected ? "locked.png" : "unlocked.png");
     const stateIconClass = getStateIconClass(workunit?.StateID, workunit?.isComplete(), workunit?.Archived);
+
+    const rightButtons: ICommandBarItemProps[] = [
+        {
+            key: "star", iconProps: { iconName: isFavorite ? "FavoriteStarFill" : "FavoriteStar" },
+            onClick: () => {
+                if (isFavorite) {
+                    removeFavorite();
+                } else {
+                    addFavorite();
+                }
+            }
+        }
+    ];
+
     const serviceNames = workunit?.ServiceNames?.Item?.join("\n") || "";
     const resourceCount = workunit?.ResourceURLCount > 1 ? workunit?.ResourceURLCount - 1 : undefined;
 
@@ -131,7 +147,7 @@ export const WorkunitDetails: React.FunctionComponent<WorkunitDetailsProps> = ({
                             <div className="pane-content">
                                 <ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
                                     <Sticky stickyPosition={StickyPositionType.Header}>
-                                        <CommandBar items={buttons} />
+                                        <CommandBar items={buttons} farItems={rightButtons} />
                                     </Sticky>
                                     <Sticky stickyPosition={StickyPositionType.Header}>
                                         <div style={{ display: "inline-block" }}>

+ 0 - 1
esp/src/src-react/hooks/Grid.ts

@@ -7,5 +7,4 @@ export function useGrid(store, filter, sort, columns) {
         sort,
         columns
     };
-
 } 

+ 110 - 0
esp/src/src-react/hooks/favorite.ts

@@ -0,0 +1,110 @@
+import * as React from "react";
+import { CallbackFunction, Observable } from "@hpcc-js/util";
+import { userKeyValStore } from "src/KeyValStore";
+
+const STORE_ID = "favorites";
+const STORE_CACHE_TIMEOUT = 10000;
+
+interface Payload {
+    //  TODO:  Will be used for labels and extra info...
+}
+type UrlMap = { [url: string]: Payload };
+
+class Favorites {
+
+    private _store = userKeyValStore();
+    private _observable = new Observable("cleared", "added", "removed");
+
+    constructor() {
+    }
+
+    private _prevPull: Promise<UrlMap>;
+    private async pull(): Promise<UrlMap> {
+        if (!this._prevPull) {
+            this._prevPull = this._store.get(STORE_ID).then((str: string): UrlMap => {
+                if (typeof str === "string") {
+                    try {
+                        const retVal = JSON.parse(str);
+                        if (retVal.constructor === Object) {
+                            return retVal;
+                        }
+                    } catch (e) {
+                        return {};
+                    }
+                }
+                return {};
+            });
+            setTimeout(() => delete this._prevPull, STORE_CACHE_TIMEOUT);
+        }
+        return this._prevPull;
+    }
+
+    private async push(favs: UrlMap): Promise<void> {
+        this._prevPull = Promise.resolve(favs);
+        return this._store.set(STORE_ID, JSON.stringify(favs));
+    }
+
+    async clear(): Promise<void> {
+        this.push({});
+        this._observable.dispatchEvent("cleared");
+    }
+
+    async has(url: string): Promise<boolean> {
+        const favs = await this.pull();
+        return favs[url] !== undefined;
+    }
+
+    async add(url: string, payload: Payload = {}): Promise<void> {
+        const favs = await this.pull();
+        favs[url] = payload;
+        this.push(favs);
+        this._observable.dispatchEvent("added", url);
+    }
+
+    async remove(url: string): Promise<void> {
+        const favs = await this.pull();
+        delete favs[url];
+        this.push(favs);
+        this._observable.dispatchEvent("removed", url);
+    }
+
+    async all(): Promise<UrlMap> {
+        return await this.pull();
+    }
+
+    listen(callback: CallbackFunction): () => void {
+        const added = this._observable.addObserver("added", val => callback("added", val));
+        const removed = this._observable.addObserver("removed", val => callback("removed", val));
+        return () => {
+            added.release();
+            removed.release();
+        };
+    }
+}
+const favorites = new Favorites();
+
+export function useFavorite(hash: string): [boolean, () => void, () => void] {
+    const [favorite, setFavorite] = React.useState(false);
+
+    React.useEffect(() => {
+        favorites.has(hash).then(setFavorite);
+        return favorites?.listen(() => {
+            favorites.has(hash).then(setFavorite);
+        });
+    }, [hash]);
+
+    return [favorite, () => favorites.add(hash), () => favorites.remove(hash)];
+}
+
+export function useFavorites(): [UrlMap] {
+    const [all, setAll] = React.useState<UrlMap>({});
+
+    React.useEffect(() => {
+        favorites.all().then(all => setAll({ ...all }));
+        return favorites?.listen(async () => {
+            favorites.all().then(all => setAll({ ...all }));
+        });
+    }, []);
+
+    return [all];
+}

+ 5 - 1
esp/src/src/WUScopeController.ts

@@ -1044,7 +1044,11 @@ export class WUScopeController8 extends WUScopeControllerBase<ISubgraph, IVertex
         const e = this.createEdge(edge);
         if (e) {
             const attrs = edge._.rawAttrs();
-            const numSlaves = edge.parent._.hasAttr("NumSlaves") ? parseInt(edge.parent._.attr("NumSlaves").RawValue) : Number.MAX_SAFE_INTEGER;
+            const numSlaves = edge._.hasAttr("NumSlaves") ?
+                parseInt(edge._.attr("NumSlaves").RawValue) :
+                edge.parent._.hasAttr("NumSlaves") ?
+                    parseInt(edge.parent._.attr("NumSlaves").RawValue) :
+                    Number.MAX_SAFE_INTEGER;
             const numStarts = parseInt(attrs["NumStarts"]);
             const numStops = parseInt(attrs["NumStops"]);
             if (!isNaN(numSlaves) && !isNaN(numStarts) && !isNaN(numStops)) {

+ 1 - 1
esp/src/src/react/hooks/useWsStore.ts

@@ -8,6 +8,6 @@ export const useGet = (key: string, filter?: object) => {
         getRecentFilters(key).then(response => {
             setResponseState({ data: response, loading: false });
         });
-    }, [filter]);
+    }, [key, filter]);
     return responseState;
 };