[arvados-workbench2] created: 2.4.0-373-g2cec626e

git repository hosting git at public.arvados.org
Tue Dec 20 13:48:18 UTC 2022


        at  2cec626e679d593d5b208a0807b391ab978e0e9d (commit)


commit 2cec626e679d593d5b208a0807b391ab978e0e9d
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Tue Dec 20 14:47:34 2022 +0100

    18368: Notification banner first implementation
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/src/common/config.ts b/src/common/config.ts
index 574445df..ff44e2ef 100644
--- a/src/common/config.ts
+++ b/src/common/config.ts
@@ -62,6 +62,7 @@ export interface ClusterConfigJSON {
         SSHHelpHostSuffix: string;
         SiteName: string;
         IdleTimeout: string;
+        BannerUUID: string;
     };
     Login: {
         LoginCluster: string;
@@ -249,6 +250,7 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
         SSHHelpHostSuffix: "",
         SiteName: "",
         IdleTimeout: "0s",
+        BannerUUID: "",
     },
     Login: {
         LoginCluster: "",
diff --git a/src/store/banner/banner-action.ts b/src/store/banner/banner-action.ts
new file mode 100644
index 00000000..808ca822
--- /dev/null
+++ b/src/store/banner/banner-action.ts
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from "store/store";
+import { unionize, UnionOf } from 'common/unionize';
+
+export const bannerReducerActions = unionize({
+    OPEN_BANNER: {},
+    CLOSE_BANNER: {},
+});
+
+export type BannerAction = UnionOf<typeof bannerReducerActions>;
+
+export const openBanner = () =>
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch(bannerReducerActions.OPEN_BANNER());
+    };
+
+export const closeBanner = () =>
+    async (dispatch: Dispatch<any>, getState: () => RootState) => {
+        dispatch(bannerReducerActions.CLOSE_BANNER());
+    };
+
+export default {
+    openBanner,
+    closeBanner
+};
diff --git a/src/store/banner/banner-reducer.ts b/src/store/banner/banner-reducer.ts
new file mode 100644
index 00000000..8009f4b2
--- /dev/null
+++ b/src/store/banner/banner-reducer.ts
@@ -0,0 +1,26 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { BannerAction, bannerReducerActions } from "./banner-action";
+
+export interface BannerState {
+    isOpen: boolean;
+}
+
+const initialState = {
+    isOpen: false,
+};
+
+export const bannerReducer = (state: BannerState = initialState, action: BannerAction) =>
+    bannerReducerActions.match(action, {
+        default: () => state,
+        OPEN_BANNER: () => ({
+             ...state,
+             isOpen: true,
+        }),
+        CLOSE_BANNER: () => ({
+            ...state,
+            isOpen: false,
+       }),
+    });
diff --git a/src/store/store.ts b/src/store/store.ts
index 94f110a0..899eb1cb 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -73,6 +73,7 @@ import { ALL_PROCESSES_PANEL_ID } from './all-processes-panel/all-processes-pane
 import { Config } from 'common/config';
 import { pluginConfig } from 'plugins';
 import { MiddlewareListReducer } from 'common/plugintypes';
+import { bannerReducer } from './banner/banner-reducer';
 
 declare global {
     interface Window {
@@ -187,6 +188,7 @@ export function configureStore(history: History, services: ServiceRepository, co
 
 const createRootReducer = (services: ServiceRepository) => combineReducers({
     auth: authReducer(services),
+    banner: bannerReducer,
     collectionPanel: collectionPanelReducer,
     collectionPanelFiles: collectionPanelFilesReducer,
     contextMenu: contextMenuReducer,
diff --git a/src/views-components/baner/banner.test.tsx b/src/views-components/baner/banner.test.tsx
new file mode 100644
index 00000000..1e820089
--- /dev/null
+++ b/src/views-components/baner/banner.test.tsx
@@ -0,0 +1,63 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { configure, shallow, mount } from "enzyme";
+import { BannerComponent } from './banner';
+import { Button } from "@material-ui/core";
+import Adapter from "enzyme-adapter-react-16";
+import servicesProvider from '../../common/service-provider';
+
+configure({ adapter: new Adapter() });
+
+jest.mock('../../common/service-provider', () => ({
+    getServices: jest.fn(),
+}));
+
+describe('<BannerComponent />', () => {
+
+    let props;
+
+    beforeEach(() => {
+        props = {
+            isOpen: false,
+            bannerUUID: undefined,
+            keepWebInlineServiceUrl: '',
+            openBanner: jest.fn(),
+            closeBanner: jest.fn(),
+            classes: {} as any,
+        }
+    });
+
+    it('renders without crashing', () => {
+        // when
+        const banner = shallow(<BannerComponent {...props} />);
+        
+        // then
+        expect(banner.find(Button)).toHaveLength(1);
+    });
+
+    it('calls collectionService', () => {
+        // given
+        props.isOpen = true;
+        props.bannerUUID = '123';
+        const mocks = {
+            collectionService: {
+                files: jest.fn(() => ({ then: (callback) => callback([{ name: 'banner.html' }]) })),
+                getFileContents: jest.fn(() => ({ then: (callback) => callback('<h1>Test</h1>') }))
+            }
+        };
+        (servicesProvider.getServices as any).mockImplementation(() => mocks);
+
+        // when
+        const banner = mount(<BannerComponent {...props} />);
+
+        // then
+        expect(servicesProvider.getServices).toHaveBeenCalled();
+        expect(mocks.collectionService.files).toHaveBeenCalled();
+        expect(mocks.collectionService.getFileContents).toHaveBeenCalled();
+        expect(banner.html()).toContain('<h1>Test</h1>');
+    });
+});
+
diff --git a/src/views-components/baner/banner.tsx b/src/views-components/baner/banner.tsx
new file mode 100644
index 00000000..9fae6381
--- /dev/null
+++ b/src/views-components/baner/banner.tsx
@@ -0,0 +1,111 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { useState, useCallback, useEffect } from 'react';
+import { Dialog, DialogContent, DialogActions, Button, StyleRulesCallback, withStyles, WithStyles } from "@material-ui/core";
+import { connect } from "react-redux";
+import { RootState } from "store/store";
+import bannerActions from "store/banner/banner-action";
+import { ArvadosTheme } from 'common/custom-theme';
+import servicesProvider from 'common/service-provider';
+import { Dispatch } from 'redux';
+
+type CssRules = 'dialogContent' | 'dialogContentIframe';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    dialogContent: {
+        minWidth: '550px',
+        minHeight: '500px',
+        display: 'block'
+    },
+    dialogContentIframe: {
+        minWidth: '550px',
+        minHeight: '500px'
+    }
+});
+
+interface BannerProps {
+    isOpen: boolean;
+    bannerUUID?: string;
+    keepWebInlineServiceUrl: string;
+};
+
+type BannerComponentProps = BannerProps & WithStyles<CssRules> & {
+    openBanner: Function,
+    closeBanner: Function,
+};
+
+const mapStateToProps = (state: RootState): BannerProps => ({
+    isOpen: state.banner.isOpen,
+    bannerUUID: state.auth.config.clusterConfig.Workbench.BannerUUID,
+    keepWebInlineServiceUrl: state.auth.config.keepWebInlineServiceUrl,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    openBanner: () => dispatch<any>(bannerActions.openBanner()),
+    closeBanner: () => dispatch<any>(bannerActions.closeBanner()),
+});
+
+export const BANNER_LOCAL_STORAGE_KEY = 'bannerFileData';
+
+export const BannerComponent = (props: BannerComponentProps) => {
+    const { 
+        isOpen,
+        openBanner,
+        closeBanner,
+        bannerUUID,
+        keepWebInlineServiceUrl
+    } = props;
+    const [bannerContents, setBannerContents] = useState(`<h1>Loading ...</h1>`)
+
+    const onConfirm = useCallback(() => {
+        closeBanner();
+    }, [closeBanner])
+
+    useEffect(() => {
+        if (!!bannerUUID && bannerUUID !== "") {
+            servicesProvider.getServices().collectionService.files(bannerUUID)
+                .then(results => {
+                    const bannerFileData = results.find(({name}) => name === 'banner.html');
+                    const result = localStorage.getItem(BANNER_LOCAL_STORAGE_KEY);
+
+                    if (result && result === JSON.stringify(bannerFileData) && !isOpen) {
+                        return;
+                    }
+
+                    if (bannerFileData) {
+                        servicesProvider.getServices()
+                            .collectionService.getFileContents(bannerFileData)
+                            .then(data => {
+                                setBannerContents(data);
+                                openBanner();
+                                localStorage.setItem(BANNER_LOCAL_STORAGE_KEY, JSON.stringify(bannerFileData));
+                            });
+                    }
+                });
+        }
+    }, [bannerUUID, keepWebInlineServiceUrl, openBanner, isOpen]);
+
+    return (
+        <Dialog open={isOpen}>
+            <div data-cy='confirmation-dialog'>
+                <DialogContent className={props.classes.dialogContent}>
+                    <div dangerouslySetInnerHTML={{ __html: bannerContents }}></div>
+                </DialogContent>
+                <DialogActions style={{ margin: '0px 24px 24px' }}>
+                    <Button
+                        data-cy='confirmation-dialog-ok-btn'
+                        variant='contained'
+                        color='primary'
+                        type='submit'
+                        onClick={onConfirm}>
+                        Close
+                    </Button>
+                </DialogActions>
+            </div>
+        </Dialog>
+    );
+}
+
+export const Banner = withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(BannerComponent));
diff --git a/src/views-components/main-app-bar/notifications-menu.tsx b/src/views-components/main-app-bar/notifications-menu.tsx
index e27bdad5..30a5756f 100644
--- a/src/views-components/main-app-bar/notifications-menu.tsx
+++ b/src/views-components/main-app-bar/notifications-menu.tsx
@@ -3,21 +3,59 @@
 // 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";
 
+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;
+    const result = localStorage.getItem(BANNER_LOCAL_STORAGE_KEY);
+    const menuItems: any[] = [];
+
+    if (!isOpen && result) {
+        menuItems.push(<MenuItem><span onClick={openBanner}>Restore Banner</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 => item)
+        }
+    </DropdownMenu>);
+}
+
+export const NotificationsMenu = connect(mapStateToProps, mapDispatchToProps)(NotificationsMenuComponent);
diff --git a/src/views/workbench/workbench.test.tsx b/src/views/workbench/workbench.test.tsx
index 471ecc40..fe5dff8a 100644
--- a/src/views/workbench/workbench.test.tsx
+++ b/src/views/workbench/workbench.test.tsx
@@ -14,6 +14,8 @@ import { CustomTheme } from 'common/custom-theme';
 import { createServices } from "services/services";
 import 'jest-localstorage-mock';
 
+jest.mock('views-components/baner/banner', () => ({ Banner: () => 'Banner' }))
+
 const history = createBrowserHistory();
 
 it('renders without crashing', () => {
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index ae8a8f84..b6ce07ae 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React, { useState, useCallback } from 'react';
+import React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { Route, Switch } from "react-router";
 import { ProjectPanel } from "views/project-panel/project-panel";
@@ -99,6 +99,7 @@ import { RestoreCollectionVersionDialog } from 'views-components/collections-dia
 import { WebDavS3InfoDialog } from 'views-components/webdav-s3-dialog/webdav-s3-dialog';
 import { pluginConfig } from 'plugins';
 import { ElementListReducer } from 'common/plugintypes';
+import { Banner } from 'views-components/baner/banner';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -185,18 +186,6 @@ const reduceRoutesFn: (a: React.ReactElement[],
 
 routes = React.createElement(React.Fragment, null, pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
 
-const Banner = () => {
-    const [visible, setVisible] = useState(true);
-    const hideBanner = useCallback(() => setVisible(false), []);
-
-    return visible ? 
-        <div id="banner" onClick={hideBanner} className="app-banner">
-            <span>
-            This is important message
-            </span>
-        </div> : null;
-}
-
 export const WorkbenchPanel =
     withStyles(styles)((props: WorkbenchPanelProps) =>
         <Grid container item xs className={props.classes.root}>

commit 297ccba9513f02ebfa6e13ab8831eb0b195b6dc3
Merge: d3cf4aea 50ec3349
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Tue Dec 13 17:59:39 2022 +0100

    Merge branch 'main' into 18368-notification-banner
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>


commit d3cf4aea6553f1065fc9d2312e246604653cabd8
Merge: 9fdaa583 06fdcfdd
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Sun Dec 4 10:37:29 2022 +0100

    Merge remote-tracking branch 'origin/main' into 18368-notification-banner
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>


commit 9fdaa5832115269584ff98a5b245e96b5a649d54
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Thu Oct 27 23:35:11 2022 +0200

    18368: initial impl
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/src/index.css b/src/index.css
index 0172d68b..51f07761 100644
--- a/src/index.css
+++ b/src/index.css
@@ -5,3 +5,25 @@ body {
     width: 100vw;
     height: 100vh;
 }
+
+.app-banner {
+    width: calc(100% - 2rem);
+    height: 150px;
+    z-index: 11111;
+    position: fixed;
+    top: 0px;
+    background-color: #00bfa5;
+    border: 1px solid #01685a;
+    color: #ffffff;
+    margin: 1rem;
+    box-sizing: border-box;
+    cursor: pointer;
+}
+
+.app-banner span {
+    font-size: 2rem;
+    text-align: center;
+    display: block;
+    margin: auto;
+    padding: 2rem;
+}
\ No newline at end of file
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index a6c49e34..ae8a8f84 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React from 'react';
+import React, { useState, useCallback } from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { Route, Switch } from "react-router";
 import { ProjectPanel } from "views/project-panel/project-panel";
@@ -185,6 +185,18 @@ const reduceRoutesFn: (a: React.ReactElement[],
 
 routes = React.createElement(React.Fragment, null, pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
 
+const Banner = () => {
+    const [visible, setVisible] = useState(true);
+    const hideBanner = useCallback(() => setVisible(false), []);
+
+    return visible ? 
+        <div id="banner" onClick={hideBanner} className="app-banner">
+            <span>
+            This is important message
+            </span>
+        </div> : null;
+}
+
 export const WorkbenchPanel =
     withStyles(styles)((props: WorkbenchPanelProps) =>
         <Grid container item xs className={props.classes.root}>
@@ -270,6 +282,7 @@ export const WorkbenchPanel =
             <VirtualMachineAttributesDialog />
             <FedLogin />
             <WebDavS3InfoDialog />
+            <Banner />
             {React.createElement(React.Fragment, null, pluginConfig.dialogs)}
         </Grid>
     );

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list