[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