Merge branch '18368-notification-banner' into 19836-new-tooltip-impl
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 10 Feb 2023 15:06:04 +0000 (16:06 +0100)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 10 Feb 2023 15:06:04 +0000 (16:06 +0100)
Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

1  2 
src/common/config.ts
src/store/store.ts
src/store/tooltips/tooltips-middleware.ts
src/views-components/main-app-bar/notifications-menu.tsx
src/views/workbench/workbench.tsx

index 9319736784b81676c6c15fe931e5136b806d985a,ff44e2efc8ef8cf67b6662f835bcb4576e4a786d..2c7995f582f397b49590c93d90814b4ea10ff794
@@@ -28,82 -26,83 +28,94 @@@ export interface ClusterConfigJSON 
              Scheme: string
          }
      };
 -    Mail?: {
 -        SupportEmailAddress: string;
 +  Mail?: {
 +    SupportEmailAddress: string;
 +  };
 +  Services: {
 +    Controller: {
 +      ExternalURL: string;
      };
 -    Services: {
 -        Controller: {
 -            ExternalURL: string
 -        }
 -        Workbench1: {
 -            ExternalURL: string
 -        }
 -        Workbench2: {
 -            ExternalURL: string
 -        }
 -        Websocket: {
 -            ExternalURL: string
 -        }
 -        WebDAV: {
 -            ExternalURL: string
 -        },
 -        WebDAVDownload: {
 -            ExternalURL: string
 -        },
 -        WebShell: {
 -            ExternalURL: string
 -        }
 +    Workbench1: {
 +      ExternalURL: string;
 +    };
 +    Workbench2: {
 +      ExternalURL: string;
      };
 -        BannerUUID: string;
+     Workbench: {
+         DisableSharingURLsUI: boolean;
+         ArvadosDocsite: string;
+         FileViewersConfigURL: string;
+         WelcomePageHTML: string;
+         InactivePageHTML: string;
+         SSHHelpPageHTML: string;
+         SSHHelpHostSuffix: string;
+         SiteName: string;
+         IdleTimeout: string;
 -    Login: {
 -        LoginCluster: string;
 -        Google: {
 -            Enable: boolean;
 -        }
 -        LDAP: {
 -            Enable: boolean;
 -        }
 -        OpenIDConnect: {
 -            Enable: boolean;
 -        }
 -        PAM: {
 -            Enable: boolean;
 -        }
 -        SSO: {
 -            Enable: boolean;
 -        }
 -        Test: {
 -            Enable: boolean;
 -        }
+     };
 +    Websocket: {
 +      ExternalURL: string;
      };
 -    Collections: {
 -        ForwardSlashNameSubstitution: string;
 -        ManagedProperties?: {
 -            [key: string]: {
 -                Function: string,
 -                Value: string,
 -                Protected?: boolean,
 -            }
 -        },
 -        TrustAllContent: boolean
 +    WebDAV: {
 +      ExternalURL: string;
      };
 -    Volumes: {
 -        [key: string]: {
 -            StorageClasses: {
 -                [key: string]: boolean;
 -            }
 -        }
 +    WebDAVDownload: {
 +      ExternalURL: string;
 +    };
 +    WebShell: {
 +      ExternalURL: string;
 +    };
 +  };
 +  Workbench: {
 +    DisableSharingURLsUI: boolean;
 +    ArvadosDocsite: string;
 +    FileViewersConfigURL: string;
 +    WelcomePageHTML: string;
 +    InactivePageHTML: string;
 +    SSHHelpPageHTML: string;
 +    SSHHelpHostSuffix: string;
 +    SiteName: string;
 +    IdleTimeout: string;
++    BannerUUID: string;
 +  };
 +  Login: {
 +    LoginCluster: string;
 +    Google: {
 +      Enable: boolean;
 +    };
 +    LDAP: {
 +      Enable: boolean;
 +    };
 +    OpenIDConnect: {
 +      Enable: boolean;
 +    };
 +    PAM: {
 +      Enable: boolean;
 +    };
 +    SSO: {
 +      Enable: boolean;
      };
 +    Test: {
 +      Enable: boolean;
 +    };
 +  };
 +  Collections: {
 +    ForwardSlashNameSubstitution: string;
 +    ManagedProperties?: {
 +      [key: string]: {
 +        Function: string;
 +        Value: string;
 +        Protected?: boolean;
 +      };
 +    };
 +    TrustAllContent: boolean;
 +  };
 +  Volumes: {
 +    [key: string]: {
 +      StorageClasses: {
 +        [key: string]: boolean;
 +      };
 +    };
 +  };
  }
  
  export class Config {
@@@ -240,73 -219,66 +252,85 @@@ remove the entire ${varName} entry fro
  };
  
  // Maps remote cluster hosts and removes the default RemoteCluster entry
 -export const mapRemoteHosts = (clusterConfigJSON: ClusterConfigJSON, config: Config) => {
 -    config.remoteHosts = {};
 -    Object.keys(clusterConfigJSON.RemoteClusters).forEach(k => { config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host; });
 -    delete config.remoteHosts["*"];
 +export const mapRemoteHosts = (
 +  clusterConfigJSON: ClusterConfigJSON,
 +  config: Config
 +) => {
 +  config.remoteHosts = {};
 +  Object.keys(clusterConfigJSON.RemoteClusters).forEach((k) => {
 +    config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host;
 +  });
 +  delete config.remoteHosts['*'];
  };
  
 -export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): ClusterConfigJSON => ({
 -    API: {
 -        UnfreezeProjectRequiresAdmin: false,
 +export const mockClusterConfigJSON = (
 +  config: Partial<ClusterConfigJSON>
 +): ClusterConfigJSON => ({
 +  API: {
 +    UnfreezeProjectRequiresAdmin: false,
 +    MaxItemsPerResponse: 1000,
 +  },
 +  ClusterID: '',
 +  RemoteClusters: {},
 +  Services: {
 +    Controller: { ExternalURL: '' },
 +    Workbench1: { ExternalURL: '' },
 +    Workbench2: { ExternalURL: '' },
 +    Websocket: { ExternalURL: '' },
 +    WebDAV: { ExternalURL: '' },
 +    WebDAVDownload: { ExternalURL: '' },
 +    WebShell: { ExternalURL: '' },
++    Workbench: {
++      DisableSharingURLsUI: false,
++      ArvadosDocsite: "",
++      FileViewersConfigURL: "",
++      WelcomePageHTML: "",
++      InactivePageHTML: "",
++      SSHHelpPageHTML: "",
++      SSHHelpHostSuffix: "",
++      SiteName: "",
++      IdleTimeout: "0s",
+     },
 -    ClusterID: "",
 -    RemoteClusters: {},
 -    Services: {
 -        Controller: { ExternalURL: "" },
 -        Workbench1: { ExternalURL: "" },
 -        Workbench2: { ExternalURL: "" },
 -        Websocket: { ExternalURL: "" },
 -        WebDAV: { ExternalURL: "" },
 -        WebDAVDownload: { ExternalURL: "" },
 -        WebShell: { ExternalURL: "" },
 +  },
 +  Workbench: {
 +    DisableSharingURLsUI: false,
 +    ArvadosDocsite: '',
 +    FileViewersConfigURL: '',
 +    WelcomePageHTML: '',
 +    InactivePageHTML: '',
 +    SSHHelpPageHTML: '',
 +    SSHHelpHostSuffix: '',
 +    SiteName: '',
 +    IdleTimeout: '0s',
++    BannerUUID: "",
 +  },
 +  Login: {
 +    LoginCluster: '',
 +    Google: {
 +      Enable: false,
      },
 -    Workbench: {
 -        DisableSharingURLsUI: false,
 -        ArvadosDocsite: "",
 -        FileViewersConfigURL: "",
 -        WelcomePageHTML: "",
 -        InactivePageHTML: "",
 -        SSHHelpPageHTML: "",
 -        SSHHelpHostSuffix: "",
 -        SiteName: "",
 -        IdleTimeout: "0s",
 -        BannerUUID: "",
 +    LDAP: {
 +      Enable: false,
 +    },
 +    OpenIDConnect: {
 +      Enable: false,
      },
 -    Login: {
 -        LoginCluster: "",
 -        Google: {
 -            Enable: false,
 -        },
 -        LDAP: {
 -            Enable: false,
 -        },
 -        OpenIDConnect: {
 -            Enable: false,
 -        },
 -        PAM: {
 -            Enable: false,
 -        },
 -        SSO: {
 -            Enable: false,
 -        },
 -        Test: {
 -            Enable: false,
 -        },
 +    PAM: {
 +      Enable: false,
      },
 -    Collections: {
 -        ForwardSlashNameSubstitution: "",
 -        TrustAllContent: false,
 +    SSO: {
 +      Enable: false,
      },
 -    Volumes: {},
 -    ...config
 +    Test: {
 +      Enable: false,
 +    },
 +  },
 +  Collections: {
 +    ForwardSlashNameSubstitution: '',
 +    TrustAllContent: false,
 +  },
 +  Volumes: {},
 +  ...config,
  });
  
  export const mockConfig = (config: Partial<Config>): Config => ({
index 4213143a676787a04df866950d52a21a71870370,899eb1cbaadd6d3da44fb752210e78f8256e36d0..52bb73338431546dcb6012e5ccf2dfac7f0a2c06
@@@ -74,12 -73,11 +74,14 @@@ import { ALL_PROCESSES_PANEL_ID } from 
  import { Config } from 'common/config';
  import { pluginConfig } from 'plugins';
  import { MiddlewareListReducer } from 'common/plugintypes';
- import { sidePanelReducer } from './side-panel/side-panel-reducer'
++import { sidePanelReducer } from './side-panel/side-panel-reducer';
 +import { tooltipsMiddleware } from './tooltips/tooltips-middleware';
+ import { bannerReducer } from './banner/banner-reducer';
 +
  declare global {
      interface Window {
 -      __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
 +        __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
      }
  }
  
index 982348b48d2015e0c8884c29a14d234d8fb755d6,0000000000000000000000000000000000000000..901374d2fd07babfa26c83fcbcd7d5e93f7141b6
mode 100644,000000..100644
--- /dev/null
@@@ -1,79 -1,0 +1,83 @@@
- const TOOLTIP_LOCAL_STORAGE_KEY = "TOOLTIP_LOCAL_STORAGE_KEY";
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import { CollectionDirectory, CollectionFile } from "models/collection-file";
 +import { Middleware, Store } from "redux";
 +import { ServiceRepository } from "services/services";
 +import { RootState } from "store/store";
 +import tippy, { createSingleton } from 'tippy.js';
 +import 'tippy.js/dist/tippy.css';
 +
 +let running = false;
 +let tooltipsContents = null;
 +let tooltipsFetchFailed = false;
-     const result = localStorage.getItem(TOOLTIP_LOCAL_STORAGE_KEY);
-     const { BannerURL } = (state.auth.config.clusterConfig.Workbench as any);
++export const TOOLTIP_LOCAL_STORAGE_KEY = "TOOLTIP_LOCAL_STORAGE_KEY";
++
++const tippySingleton = createSingleton([], {delay: 10});
 +
 +export const tooltipsMiddleware = (services: ServiceRepository): Middleware => (store: Store) => next => action => {
 +    const state: RootState = store.getState();
-     let bannerUUID = !!BannerURL ? BannerURL : 'tordo-4zz18-1buneu6sb8zxiti';
++    const hideTooltip = localStorage.getItem(TOOLTIP_LOCAL_STORAGE_KEY);
++    const { BannerUUID } = (state.auth.config.clusterConfig.Workbench as any);
 +
-     if (bannerUUID && !tooltipsContents && !result && !tooltipsFetchFailed && !running) {
++    const bannerUUID = BannerUUID || 'tordo-4zz18-1buneu6sb8zxiti';
 +
-     } else if (tooltipsContents && !result && !tooltipsFetchFailed) {
++    if (bannerUUID && !tooltipsContents && !hideTooltip && !tooltipsFetchFailed && !running) {
 +        running = true;
 +        fetchTooltips(services, bannerUUID);
-         createSingleton(tippyInstances, {delay: 10});
++    } else if (tooltipsContents && !hideTooltip && !tooltipsFetchFailed) {
 +        applyTooltips();
 +    }
 +
 +    return next(action);
 +};
 +
 +const fetchTooltips = (services, bannerUUID) => {
 +    services.collectionService.files(bannerUUID)
 +        .then(results => {
 +            const tooltipsFile: CollectionDirectory | CollectionFile | undefined = results.find(({ name }) => name === 'tooltips.json');
 +
 +            if (tooltipsFile) {
 +                running = true;
 +                services.collectionService.getFileContents(tooltipsFile as CollectionFile)
 +                    .then(data => {
 +                        tooltipsContents = JSON.parse(data);
 +                        applyTooltips();
 +                    })
 +                    .catch(() => {})
 +                    .finally(() => {
 +                        running = false;
 +                    });
 +            }  else {
 +                tooltipsFetchFailed = true;
 +            }
 +        })
 +        .catch(() => {})
 +        .finally(() => {
 +            running = false;
 +        });
 +};
 +
 +const applyTooltips = () => {
 +    const tippyInstances: any[] = Object.keys(tooltipsContents as any)
 +        .map((key) => {
 +            const content = (tooltipsContents as any)[key]
 +            const element = document.querySelector(key);
 +
 +            if (element) {
 +                const hasTippyAttatched = !!(element as any)._tippy;
 +
 +                if (!hasTippyAttatched && tooltipsContents) {
 +                    return tippy(element as any, { content });
 +                }
 +            }
 +
 +            return null;
 +        })
 +        .filter(data => !!data);
 +
++    if (tippyInstances.length > 0) {
++        tippySingleton.setInstances(tippyInstances);
++    }
 +};
index e27bdad552f7c51c34610c3e3bba38d2ed87a279,30a5756f2a00998466a1ecb975409cd9a0850497..ca97a612bb11875460c17eeed6487266b9c46cf3
@@@ -3,21 -3,59 +3,77 @@@
  // SPDX-License-Identifier: AGPL-3.0
  
  import React from "react";
- import { Badge, MenuItem } from '@material-ui/core';
+ import { Dispatch } from "redux";
+ import { connect } from "react-redux";
+ import { Badge, MenuItem } from "@material-ui/core";
  import { DropdownMenu } from "components/dropdown-menu/dropdown-menu";
- import { NotificationIcon } from 'components/icon/icon';
- export const NotificationsMenu = 
-     () =>
-         <DropdownMenu
-             icon={
-                 <Badge
-                     badgeContent={0}
-                     color="primary">
-                     <NotificationIcon />
-                 </Badge>}
-             id="account-menu"
-             title="Notifications">
-             <MenuItem>You are up to date</MenuItem>
-         </DropdownMenu>;
+ import { NotificationIcon } from "components/icon/icon";
+ import bannerActions from "store/banner/banner-action";
+ import { BANNER_LOCAL_STORAGE_KEY } from "views-components/baner/banner";
+ import { RootState } from "store/store";
++import { TOOLTIP_LOCAL_STORAGE_KEY } from "store/tooltips/tooltips-middleware";
++import { useCallback } from "react";
  
 -    const result = localStorage.getItem(BANNER_LOCAL_STORAGE_KEY);
+ const mapStateToProps = (state: RootState): NotificationsMenuProps => ({
+     isOpen: state.banner.isOpen,
+     bannerUUID: state.auth.config.clusterConfig.Workbench.BannerUUID,
+ });
+ const mapDispatchToProps = (dispatch: Dispatch) => ({
+     openBanner: () => dispatch<any>(bannerActions.openBanner()),
+ });
+ type NotificationsMenuProps = {
+     isOpen: boolean;
+     bannerUUID?: string;
+ }
+ type NotificationsMenuComponentProps = NotificationsMenuProps & {
+     openBanner: any;
+ }
+ export const NotificationsMenuComponent = (props: NotificationsMenuComponentProps) => {
+     const { isOpen, openBanner } = props;
 -    if (!isOpen && result) {
++    const bannerResult = localStorage.getItem(BANNER_LOCAL_STORAGE_KEY);
++    const tooltipResult = localStorage.getItem(TOOLTIP_LOCAL_STORAGE_KEY);
+     const menuItems: any[] = [];
 -            menuItems.map(item => item)
++    if (!isOpen && bannerResult) {
+         menuItems.push(<MenuItem><span onClick={openBanner}>Restore Banner</span></MenuItem>);
+     }
++    const toggleTooltips = useCallback(() => {
++        if (tooltipResult) {
++            localStorage.removeItem(TOOLTIP_LOCAL_STORAGE_KEY);
++        } else {
++            localStorage.setItem(TOOLTIP_LOCAL_STORAGE_KEY, 'true');
++        }
++        window.location.reload();
++    }, [tooltipResult]);
++
++    if (tooltipResult) {
++        menuItems.push(<MenuItem><span onClick={toggleTooltips}>Enable tooltips</span></MenuItem>);
++    } else {
++        menuItems.push(<MenuItem><span onClick={toggleTooltips}>Disable tooltips</span></MenuItem>);
++    }
++
+     if (menuItems.length === 0) {
+         menuItems.push(<MenuItem>You are up to date</MenuItem>);
+     }
+     return (<DropdownMenu
+         icon={
+             <Badge
+                 badgeContent={0}
+                 color="primary">
+                 <NotificationIcon />
+             </Badge>}
+         id="account-menu"
+         title="Notifications">
+         {
++            menuItems.map((item, i) => <div key={i}>{item}</div>)
+         }
+     </DropdownMenu>);
+ }
+ export const NotificationsMenu = connect(mapStateToProps, mapDispatchToProps)(NotificationsMenuComponent);
index 87f004b335693b7df95a32102415f2b7232b75be,b6ce07aebaf961c0c84f90051afef3432adf44bf..7103efd132a1b8e1ac149229d1fbcda0e7620a7f
@@@ -99,7 -99,7 +99,8 @@@ import { RestoreCollectionVersionDialo
  import { WebDavS3InfoDialog } from 'views-components/webdav-s3-dialog/webdav-s3-dialog';
  import { pluginConfig } from 'plugins';
  import { ElementListReducer } from 'common/plugintypes';
 +import { COLLAPSE_ICON_SIZE } from 'views-components/side-panel-toggle/side-panel-toggle'
+ import { Banner } from 'views-components/baner/banner';
  
  type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
  
@@@ -293,6 -271,7 +294,7 @@@ export const WorkbenchPanel 
              <VirtualMachineAttributesDialog />
              <FedLogin />
              <WebDavS3InfoDialog />
+             <Banner />
              {React.createElement(React.Fragment, null, pluginConfig.dialogs)}
 -        </Grid>
 +        </Grid>}
      );