Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>
SSHHelpHostSuffix: string;
SiteName: string;
IdleTimeout: string;
+ BannerUUID: string;
};
Login: {
LoginCluster: string;
SSHHelpHostSuffix: "",
SiteName: "",
IdleTimeout: "0s",
+ BannerUUID: "",
},
Login: {
LoginCluster: "",
--- /dev/null
+// 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
+};
--- /dev/null
+// 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,
+ }),
+ });
import { Config } from 'common/config';
import { pluginConfig } from 'plugins';
import { MiddlewareListReducer } from 'common/plugintypes';
+import { bannerReducer } from './banner/banner-reducer';
declare global {
interface Window {
const createRootReducer = (services: ServiceRepository) => combineReducers({
auth: authReducer(services),
+ banner: bannerReducer,
collectionPanel: collectionPanelReducer,
collectionPanelFiles: collectionPanelFilesReducer,
contextMenu: contextMenuReducer,
--- /dev/null
+// 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>');
+ });
+});
+
--- /dev/null
+// 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));
// 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);
import { createServices } from "services/services";
import 'jest-localstorage-mock';
+jest.mock('views-components/baner/banner', () => ({ Banner: () => 'Banner' }))
+
const history = createBrowserHistory();
it('renders without crashing', () => {
//
// 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";
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';
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}>