Merge remote-tracking branch 'origin/main' into 17579-Clear-table-filter-when-changin...
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Wed, 8 Dec 2021 22:01:38 +0000 (23:01 +0100)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Wed, 8 Dec 2021 22:01:55 +0000 (23:01 +0100)
Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

28 files changed:
src/components/collection-panel-files/collection-panel-files.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.tsx
src/components/icon/icon.tsx
src/components/multi-panel-view/multi-panel-view.test.tsx [new file with mode: 0644]
src/components/multi-panel-view/multi-panel-view.tsx [new file with mode: 0644]
src/components/panel-default-view/panel-default-view.tsx [deleted file]
src/store/process-logs-panel/process-logs-panel.ts
src/views-components/details-panel/process-details.tsx
src/views/all-processes-panel/all-processes-panel.tsx
src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx
src/views/api-client-authorization-panel/api-client-authorization-panel.tsx
src/views/collection-content-address-panel/collection-content-address-panel.tsx
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/groups-panel/groups-panel.tsx
src/views/link-panel/link-panel-root.tsx
src/views/process-panel/process-details-attributes.tsx [new file with mode: 0644]
src/views/process-panel/process-details-card.tsx [new file with mode: 0644]
src/views/process-panel/process-information-card.tsx
src/views/process-panel/process-panel-root.tsx
src/views/project-panel/project-panel.tsx
src/views/public-favorites-panel/public-favorites-panel.tsx
src/views/shared-with-me-panel/shared-with-me-panel.tsx
src/views/subprocess-panel/subprocess-panel-root.tsx
src/views/trash-panel/trash-panel.tsx
src/views/user-panel/user-panel.tsx
src/views/workbench/workbench.tsx

index 97ec3bf9e1e4a72a9a5d251fbf8e8cd55363613d..1ef6b5c94cdf117ea52a932c47a085957ce6c2d2 100644 (file)
@@ -48,7 +48,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     wrapper: {
         display: 'flex',
         minHeight: '600px',
-        marginBottom: '1rem',
         color: 'rgba(0, 0, 0, 0.87)',
         fontSize: '0.875rem',
         fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
@@ -490,7 +489,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                                                         data-parent-path={name}
                                                         className={classNames(classes.row, getActiveClass(name))}
                                                         key={id}>
-                                                            {getItemIcon(type, getActiveClass(name))} 
+                                                            {getItemIcon(type, getActiveClass(name))}
                                                             <div className={classes.rowName}>
                                                                 {name}
                                                             </div>
index 78aae35051a078e1f80aa9e580af5c79a24f8422..05125f12c7311b8a4fe700a7dd61923ce6e682c8 100644 (file)
@@ -11,17 +11,19 @@ import { SearchInput } from 'components/search-input/search-input';
 import { ArvadosTheme } from "common/custom-theme";
 import { createTree } from 'models/tree';
 import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
-import { MoreOptionsIcon } from 'components/icon/icon';
+import { CloseIcon, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
 import { PaperProps } from '@material-ui/core/Paper';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 
-type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title';
+type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     searchBox: {
         paddingBottom: theme.spacing.unit * 2
     },
     toolbar: {
-        paddingTop: theme.spacing.unit * 2
+        paddingTop: theme.spacing.unit,
+        paddingRight: theme.spacing.unit * 2,
     },
     toolbarUnderTitle: {
         paddingTop: 0
@@ -30,7 +32,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         overflow: 'auto'
     },
     root: {
-        height: '100%'
+        height: '100%',
     },
     moreOptionsButton: {
         padding: 0
@@ -39,7 +41,14 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         paddingLeft: theme.spacing.unit * 3,
         paddingTop: theme.spacing.unit * 3,
         fontSize: '18px'
-    }
+    },
+    dataTable: {
+        height: '100%',
+        overflow: 'auto',
+    },
+    container: {
+        height: '100%',
+    },
 });
 
 interface DataExplorerDataProps<T> {
@@ -79,7 +88,8 @@ interface DataExplorerActionProps<T> {
     extractKey?: (item: T) => React.Key;
 }
 
-type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules>;
+type DataExplorerProps<T> = DataExplorerDataProps<T> &
+    DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
 
 export const DataExplorer = withStyles(styles)(
     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
@@ -96,12 +106,14 @@ export const DataExplorer = withStyles(styles)(
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
-                paperKey, fetchMode, currentItemUuid, title
+                paperKey, fetchMode, currentItemUuid, title,
+                doHidePanel, doMaximizePanel, panelName, panelMaximized
             } = this.props;
 
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
-                {title && <div className={classes.title}>{title}</div>}
-                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
+                <Grid container direction="column" wrap="nowrap" className={classes.container}>
+                {title && <Grid item xs className={classes.title}>{title}</Grid>}
+                {(!hideColumnSelector || !hideSearchInput) && <Grid item xs><Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                         <div className={classes.searchBox}>
                             {!hideSearchInput && <SearchInput
@@ -115,8 +127,16 @@ export const DataExplorer = withStyles(styles)(
                             columns={columns}
                             onColumnToggle={onColumnToggle} />}
                     </Grid>
-                </Toolbar>}
-                <DataTable
+                    { doMaximizePanel && !panelMaximized &&
+                        <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
+                            <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
+                        </Tooltip> }
+                    { doHidePanel &&
+                        <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+                            <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+                        </Tooltip> }
+                </Toolbar></Grid>}
+                <Grid item xs="auto" className={classes.dataTable}><DataTable
                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
                     items={items}
                     onRowClick={(_, item: T) => onRowClick(item)}
@@ -128,8 +148,8 @@ export const DataExplorer = withStyles(styles)(
                     working={working}
                     defaultView={dataTableDefaultView}
                     currentItemUuid={currentItemUuid}
-                    currentRoute={paperKey} />
-                <Toolbar className={classes.footer}>
+                    currentRoute={paperKey} /></Grid>
+                <Grid item xs><Toolbar className={classes.footer}>
                     <Grid container justify="flex-end">
                         {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
                             count={itemsAvailable}
@@ -146,7 +166,8 @@ export const DataExplorer = withStyles(styles)(
                                 onClick={this.loadMore}
                             >Load more</Button>}
                     </Grid>
-                </Toolbar>
+                </Toolbar></Grid>
+                </Grid>
             </Paper>;
         }
 
index 0c84f642fd1cc2733b9a6d0cec03ab72b21ca669..de52d365030dfdc7a48a47bb2f6f52cd31fade51 100644 (file)
@@ -39,13 +39,11 @@ type CssRules = "tableBody" | "root" | "content" | "noItemsInfo" | 'tableCell' |
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     root: {
-        overflowX: 'auto',
-        overflowY: 'auto',
-        height: 'calc(100vh - 280px)',
+        width: '100%',
     },
     content: {
         display: 'inline-block',
-        width: '100%'
+        width: '100%',
     },
     tableBody: {
         background: theme.palette.background.paper
index 26ce4feae7c117c589ac336a211a4329365a65c7..523eefbd10c7b75bb0e6904f59a1abf62555f563 100644 (file)
@@ -59,12 +59,15 @@ import SettingsEthernet from '@material-ui/icons/SettingsEthernet';
 import Star from '@material-ui/icons/Star';
 import StarBorder from '@material-ui/icons/StarBorder';
 import Warning from '@material-ui/icons/Warning';
+import Visibility from '@material-ui/icons/Visibility';
+import VisibilityOff from '@material-ui/icons/VisibilityOff';
 import VpnKey from '@material-ui/icons/VpnKey';
 import LinkOutlined from '@material-ui/icons/LinkOutlined';
 
 // Import FontAwesome icons
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faPencilAlt, faSlash } from '@fortawesome/free-solid-svg-icons';
+import { CropFreeSharp } from '@material-ui/icons';
 library.add(
     faPencilAlt,
     faSlash,
@@ -112,10 +115,12 @@ export const FileIcon: IconType = (props) => <DescriptionIcon {...props} />;
 export const HelpIcon: IconType = (props) => <Help {...props} />;
 export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
 export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
+export const InfoIcon: IconType = (props) => <Info {...props} />;
 export const InputIcon: IconType = (props) => <InsertDriveFile {...props} />;
 export const KeyIcon: IconType = (props) => <VpnKey {...props} />;
 export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
 export const MailIcon: IconType = (props) => <Mail {...props} />;
+export const MaximizeIcon: IconType = (props) => <CropFreeSharp {...props} />;
 export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
 export const MoveToIcon: IconType = (props) => <Input {...props} />;
 export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
@@ -144,6 +149,8 @@ export const SidePanelRightArrowIcon: IconType = (props) => <PlayArrow {...props
 export const TrashIcon: IconType = (props) => <Delete {...props} />;
 export const UserPanelIcon: IconType = (props) => <Person {...props} />;
 export const UsedByIcon: IconType = (props) => <Folder {...props} />;
+export const VisibleIcon: IconType = (props) => <Visibility {...props} />;
+export const InvisibleIcon: IconType = (props) => <VisibilityOff {...props} />;
 export const WorkflowIcon: IconType = (props) => <Code {...props} />;
 export const WarningIcon: IconType = (props) => <Warning style={{ color: '#fbc02d', height: '30px', width: '30px' }} {...props} />;
 export const Link: IconType = (props) => <LinkOutlined {...props} />;
diff --git a/src/components/multi-panel-view/multi-panel-view.test.tsx b/src/components/multi-panel-view/multi-panel-view.test.tsx
new file mode 100644 (file)
index 0000000..6cf13d7
--- /dev/null
@@ -0,0 +1,87 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { configure, mount } from "enzyme";
+import Adapter from "enzyme-adapter-react-16";
+import { MPVContainer } from './multi-panel-view';
+import { Button } from "@material-ui/core";
+
+configure({ adapter: new Adapter() });
+
+const PanelMock = ({panelName, panelMaximized, doHidePanel, doMaximizePanel, children, ...rest}) =>
+    <div {...rest}>{children}</div>;
+
+describe('<MPVContainer />', () => {
+    let props;
+
+    beforeEach(() => {
+        props = {
+            classes: {},
+        };
+    });
+
+    it('should show default panel buttons for every child', () => {
+        const childs = [
+            <PanelMock key={1}>This is one panel</PanelMock>,
+            <PanelMock key={2}>This is another panel</PanelMock>,
+        ];
+        const wrapper = mount(<MPVContainer {...props}>{[...childs]}</MPVContainer>);
+        expect(wrapper.find(Button).first().html()).toContain('Panel 1');
+        expect(wrapper.html()).toContain('This is one panel');
+        expect(wrapper.find(Button).last().html()).toContain('Panel 2');
+        expect(wrapper.html()).toContain('This is another panel');
+    });
+
+    it('should show panel when clicking on its button', () => {
+        const childs = [
+            <PanelMock key={1}>This is one panel</PanelMock>,
+        ];
+        props.panelStates = [
+            {name: 'Initially invisible Panel', visible: false},
+        ]
+
+        const wrapper = mount(<MPVContainer {...props}>{[...childs]}</MPVContainer>);
+
+        // Initial state: panel not visible
+        expect(wrapper.html()).not.toContain('This is one panel');
+        expect(wrapper.html()).toContain('All panels are hidden');
+
+        // Panel visible when clicking on its button
+        wrapper.find(Button).simulate('click');
+        expect(wrapper.html()).toContain('This is one panel');
+        expect(wrapper.html()).not.toContain('All panels are hidden');
+    });
+
+    it('should show custom panel buttons when config provided', () => {
+        const childs = [
+            <PanelMock key={1}>This is one panel</PanelMock>,
+            <PanelMock key={2}>This is another panel</PanelMock>,
+        ];
+        props.panelStates = [
+            {name: 'First Panel'},
+        ]
+        const wrapper = mount(<MPVContainer {...props}>{[...childs]}</MPVContainer>);
+        expect(wrapper.find(Button).first().html()).toContain('First Panel');
+        expect(wrapper.html()).toContain('This is one panel');
+        // Second panel received the default button naming and hidden status by default
+        expect(wrapper.find(Button).last().html()).toContain('Panel 2');
+        expect(wrapper.html()).not.toContain('This is another panel');
+        wrapper.find(Button).last().simulate('click');
+        expect(wrapper.html()).toContain('This is another panel');
+    });
+
+    it('should set panel hidden when requested', () => {
+        const childs = [
+            <PanelMock key={1}>This is one panel</PanelMock>,
+        ];
+        props.panelStates = [
+            {name: 'First Panel', visible: false},
+        ]
+        const wrapper = mount(<MPVContainer {...props}>{[...childs]}</MPVContainer>);
+        expect(wrapper.find(Button).html()).toContain('First Panel');
+        expect(wrapper.html()).not.toContain('This is one panel');
+        expect(wrapper.html()).toContain('All panels are hidden');
+    });
+});
\ No newline at end of file
diff --git a/src/components/multi-panel-view/multi-panel-view.tsx b/src/components/multi-panel-view/multi-panel-view.tsx
new file mode 100644 (file)
index 0000000..dbb3792
--- /dev/null
@@ -0,0 +1,198 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { MutableRefObject, ReactElement, ReactNode, useEffect, useRef, useState } from 'react';
+import {
+    Button,
+    Grid,
+    Paper,
+    StyleRulesCallback,
+    Tooltip,
+    withStyles,
+    WithStyles
+} from "@material-ui/core";
+import { GridProps } from '@material-ui/core/Grid';
+import { isArray } from 'lodash';
+import { DefaultView } from 'components/default-view/default-view';
+import { InfoIcon, InvisibleIcon, VisibleIcon } from 'components/icon/icon';
+import { ReactNodeArray } from 'prop-types';
+import classNames from 'classnames';
+
+type CssRules = 'button' | 'buttonIcon' | 'content';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    button: {
+        padding: '2px 5px',
+        marginRight: '5px',
+    },
+    buttonIcon: {
+        boxShadow: 'none',
+        padding: '2px 0px 2px 5px',
+        fontSize: '1rem'
+    },
+    content: {
+        overflow: 'auto',
+    },
+});
+
+interface MPVHideablePanelDataProps {
+    name: string;
+    visible: boolean;
+    maximized: boolean;
+    illuminated: boolean;
+    children: ReactNode;
+    panelRef?: MutableRefObject<any>;
+}
+
+interface MPVHideablePanelActionProps {
+    doHidePanel: () => void;
+    doMaximizePanel: () => void;
+}
+
+type MPVHideablePanelProps = MPVHideablePanelDataProps & MPVHideablePanelActionProps;
+
+const MPVHideablePanel = ({doHidePanel, doMaximizePanel, name, visible, maximized, illuminated, ...props}: MPVHideablePanelProps) =>
+    visible
+    ? <>
+        {React.cloneElement((props.children as ReactElement), { doHidePanel, doMaximizePanel, panelName: name, panelMaximized: maximized, panelIlluminated: illuminated, panelRef: props.panelRef })}
+    </>
+    : null;
+
+interface MPVPanelDataProps {
+    panelName?: string;
+    panelMaximized?: boolean;
+    panelIlluminated?: boolean;
+    panelRef?: MutableRefObject<any>;
+}
+
+interface MPVPanelActionProps {
+    doHidePanel?: () => void;
+    doMaximizePanel?: () => void;
+}
+
+// Props received by panel implementors
+export type MPVPanelProps = MPVPanelDataProps & MPVPanelActionProps;
+
+type MPVPanelContentProps = {children: ReactElement} & MPVPanelProps & GridProps;
+
+// Grid item compatible component for layout and MPV props passing
+export const MPVPanelContent = ({doHidePanel, doMaximizePanel, panelName, panelMaximized, panelIlluminated, panelRef, ...props}: MPVPanelContentProps) => {
+    useEffect(() => {
+        if (panelRef && panelRef.current) {
+            panelRef.current.scrollIntoView({behavior: 'smooth'});
+        }
+    }, [panelRef]);
+
+    return <Grid item {...props}>
+        <span ref={panelRef} /> {/* Element to scroll to when the panel is selected */}
+        <Paper style={{height: '100%'}} elevation={panelIlluminated ? 8 : 0}>
+            {React.cloneElement(props.children, { doHidePanel, doMaximizePanel, panelName, panelMaximized })}
+        </Paper>
+    </Grid>;
+}
+
+export interface MPVPanelState {
+    name: string;
+    visible?: boolean;
+}
+interface MPVContainerDataProps {
+    panelStates?: MPVPanelState[];
+}
+type MPVContainerProps = MPVContainerDataProps & GridProps;
+
+// Grid container compatible component that also handles panel toggling.
+const MPVContainerComponent = ({children, panelStates, classes, ...props}: MPVContainerProps & WithStyles<CssRules>) => {
+    if (children === undefined || children === null || children === {}) {
+        children = [];
+    } else if (!isArray(children)) {
+        children = [children];
+    }
+    const visibility = (children as ReactNodeArray).map((_, idx) =>
+        !!!panelStates || // if panelStates wasn't passed, default to all visible panels
+            (panelStates[idx] &&
+                (panelStates[idx].visible || panelStates[idx].visible === undefined)));
+    const [panelVisibility, setPanelVisibility] = useState<boolean[]>(visibility);
+    const [brightenedPanel, setBrightenedPanel] = useState<number>(-1);
+    const panelRef = useRef<any>(null);
+
+    let panels: JSX.Element[] = [];
+    let toggles: JSX.Element[] = [];
+
+    if (isArray(children)) {
+        for (let idx = 0; idx < children.length; idx++) {
+            const showFn = (idx: number) => () => {
+                setPanelVisibility([
+                    ...panelVisibility.slice(0, idx),
+                    true,
+                    ...panelVisibility.slice(idx+1)
+                ]);
+            };
+            const hideFn = (idx: number) => () => {
+                setPanelVisibility([
+                    ...panelVisibility.slice(0, idx),
+                    false,
+                    ...panelVisibility.slice(idx+1)
+                ])
+            };
+            const maximizeFn = (idx: number) => () => {
+                // Maximize X == hide all but X
+                setPanelVisibility([
+                    ...panelVisibility.slice(0, idx).map(() => false),
+                    true,
+                    ...panelVisibility.slice(idx+1).map(() => false),
+                ])
+            };
+            const toggleIcon = panelVisibility[idx]
+                ? <VisibleIcon className={classNames(classes.buttonIcon)} />
+                : <InvisibleIcon className={classNames(classes.buttonIcon)}/>
+            const panelName = panelStates === undefined
+                ? `Panel ${idx+1}`
+                : (panelStates[idx] && panelStates[idx].name) || `Panel ${idx+1}`;
+            const toggleVariant = "outlined";
+            const toggleTooltip = panelVisibility[idx]
+                ? ''
+                :`Show ${panelName} panel`;
+            const panelIsMaximized = panelVisibility[idx] &&
+                panelVisibility.filter(e => e).length === 1;
+
+            toggles = [
+                ...toggles,
+                <Tooltip title={toggleTooltip} disableFocusListener>
+                    <Button variant={toggleVariant} size="small" color="primary"
+                        className={classNames(classes.button)}
+                        onMouseEnter={() => setBrightenedPanel(idx)}
+                        onMouseLeave={() => setBrightenedPanel(-1)}
+                        onClick={showFn(idx)}>
+                            {panelName}
+                            {toggleIcon}
+                    </Button>
+                </Tooltip>
+            ];
+
+            const aPanel =
+                <MPVHideablePanel key={idx} visible={panelVisibility[idx]} name={panelName}
+                    panelRef={(idx === brightenedPanel) ? panelRef : undefined}
+                    maximized={panelIsMaximized} illuminated={idx === brightenedPanel}
+                    doHidePanel={hideFn(idx)} doMaximizePanel={maximizeFn(idx)}>
+                    {children[idx]}
+                </MPVHideablePanel>;
+            panels = [...panels, aPanel];
+        };
+    };
+
+    return <Grid container {...props}>
+        <Grid container item direction="row">
+            { toggles.map((tgl, idx) => <Grid item key={idx}>{tgl}</Grid>) }
+        </Grid>
+        <Grid container item {...props} xs className={classes.content}>
+            { panelVisibility.includes(true)
+                ? panels
+                : <Grid container item alignItems='center' justify='center'>
+                    <DefaultView messages={["All panels are hidden.", "Click on the buttons above to show them."]} icon={InfoIcon} />
+                </Grid> }
+        </Grid>
+    </Grid>;
+};
+
+export const MPVContainer = withStyles(styles)(MPVContainerComponent);
\ No newline at end of file
diff --git a/src/components/panel-default-view/panel-default-view.tsx b/src/components/panel-default-view/panel-default-view.tsx
deleted file mode 100644 (file)
index c364bb7..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { DefaultViewDataProps, DefaultView } from 'components/default-view/default-view';
-
-type CssRules = 'classRoot' | 'classIcon' | 'classMessage';
-
-const styles: StyleRulesCallback<CssRules> = () => ({
-    classRoot: {
-        position: 'absolute',
-        width: '80%',
-        left: '50%',
-        top: '50%',
-        transform: 'translate(-50%, -50%)'
-    },
-    classMessage: {
-        fontSize: '1.75rem',
-    },
-    classIcon: {
-        fontSize: '6rem'
-    }
-});
-
-type PanelDefaultViewProps = Pick<DefaultViewDataProps, 'icon' | 'messages'> & WithStyles<CssRules>;
-
-export const PanelDefaultView = withStyles(styles)(
-    ({ classes, ...props }: PanelDefaultViewProps) =>
-        <DefaultView {...classes} {...props} />);
index deaaab6a85515b5e4464f361c958ab5894ec06b1..87b50bd2f97ff89f1a7d057e580aa3093a13bf80 100644 (file)
@@ -1,9 +1,10 @@
-import { RootState } from '../store';
-import { matchProcessLogRoute } from 'routes/routes';
 // Copyright (C) The Arvados Authors. All rights reserved.
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { RootState } from '../store';
+import { matchProcessLogRoute, matchProcessRoute } from 'routes/routes';
+
 export interface ProcessLogsPanel {
     filters: string[];
     selectedFilter: string;
@@ -20,6 +21,6 @@ export const getProcessPanelLogs = ({ selectedFilter, logs }: ProcessLogsPanel)
 
 export const getProcessLogsPanelCurrentUuid = ({ router }: RootState) => {
     const pathname = router.location ? router.location.pathname : '';
-    const match = matchProcessLogRoute(pathname);
+    const match = matchProcessLogRoute(pathname) || matchProcessRoute(pathname);
     return match ? match.params.id : undefined;
 };
index c4b374b9c130a2b96fac93b4ad7fe5e34822f640..d9c991f5858ea4c1ca3a50f1903716d044dfd350 100644 (file)
@@ -5,12 +5,8 @@
 import React from 'react';
 import { ProcessIcon } from 'components/icon/icon';
 import { ProcessResource } from 'models/process';
-import { formatDate } from 'common/formatters';
-import { ResourceKind } from 'models/resource';
-import { resourceLabel } from 'common/labels';
 import { DetailsData } from "./details-data";
-import { DetailsAttribute } from "components/details-attribute/details-attribute";
-import { ResourceOwnerWithName } from '../data-explorer/renderers';
+import { ProcessDetailsAttributes } from 'views/process-panel/process-details-attributes';
 
 export class ProcessDetails extends DetailsData<ProcessResource> {
 
@@ -19,25 +15,6 @@ export class ProcessDetails extends DetailsData<ProcessResource> {
     }
 
     getDetails() {
-        return <div>
-            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
-            <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} value={this.item.ownerUuid}
-                uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
-
-            <DetailsAttribute label='Status' value={this.item.state} />
-            <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
-
-            <DetailsAttribute label='Started at' value={formatDate(this.item.createdAt)} />
-            <DetailsAttribute label='Finished at' value={formatDate(this.item.expiresAt)} />
-
-            <DetailsAttribute label='Outputs' value={this.item.outputPath} />
-            <DetailsAttribute label='UUID' linkToUuid={this.item.uuid} value={this.item.uuid} />
-            <DetailsAttribute label='Container UUID' value={this.item.containerUuid} />
-
-            <DetailsAttribute label='Priority' value={this.item.priority} />
-            <DetailsAttribute label='Runtime Constraints' value={JSON.stringify(this.item.runtimeConstraints)} />
-
-            <DetailsAttribute label='Docker Image locator' linkToUuid={this.item.containerImage} value={this.item.containerImage} />
-        </div>;
+        return <ProcessDetailsAttributes item={this.item} />;
     }
 }
index f9fab44d3fd52d76a55202a3a161fe16dff3b3d2..928b4fff1a9a92750af31fcdce8de29ffccefac0 100644 (file)
@@ -33,7 +33,7 @@ import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from 'st
 import { getProcess } from 'store/processes/process';
 import { ResourcesState } from 'store/resources/resources';
 
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
@@ -43,6 +43,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     button: {
         marginLeft: theme.spacing.unit
     },
+    root: {
+        width: '100%',
+    }
 });
 
 export enum AllProcessesPanelColumnNames {
@@ -142,18 +145,17 @@ export const AllProcessesPanel = withStyles(styles)(
             }
 
             render() {
-                return <DataExplorer
+                return <div className={this.props.classes.root}><DataExplorer
                     id={ALL_PROCESSES_PANEL_ID}
                     onRowClick={this.handleRowClick}
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={true}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={ProcessIcon}
-                            messages={['Processes list empty.']}
-                            />
-                    } />;
+                    dataTableDefaultView={ <DataTableDefaultView
+                        icon={ProcessIcon}
+                        messages={['Processes list empty.']}
+                        /> } />
+                </div>
             }
         }
     )
index 703bbec5ab2c82c4ed5eae321cfd76e40c02f2cb..8f87cb269eaef7a37cdfa438ebb7eef2d2100546 100644 (file)
@@ -4,10 +4,10 @@
 
 import React from 'react';
 import {
-    StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Tooltip, IconButton
+    StyleRulesCallback, WithStyles, withStyles
 } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
-import { HelpIcon, ShareMeIcon } from 'components/icon/icon';
+import { ShareMeIcon } from 'components/icon/icon';
 import { createTree } from 'models/tree';
 import { DataColumns } from 'components/data-table/data-table';
 import { SortDirection } from 'components/data-table/data-column';
@@ -20,21 +20,11 @@ import {
     TokenLastUsedAt, TokenLastUsedByIpAddress, TokenScopes, TokenUserId
 } from 'views-components/data-explorer/renderers';
 
-type CssRules = 'card' | 'cardContent' | 'helpIconGrid';
+type CssRules = 'root';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    card: {
+    root: {
         width: '100%',
-        overflow: 'auto'
-    },
-    cardContent: {
-        padding: 0,
-        '&:last-child': {
-            paddingBottom: 0
-        }
-    },
-    helpIconGrid: {
-        textAlign: 'right'
     }
 });
 
@@ -132,7 +122,6 @@ export interface ApiClientAuthorizationPanelRootActionProps {
     onItemClick: (item: string) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
     onItemDoubleClick: (item: string) => void;
-    openHelpDialog: () => void;
 }
 
 export interface ApiClientAuthorizationPanelRootDataProps {
@@ -143,33 +132,18 @@ type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActio
     & ApiClientAuthorizationPanelRootDataProps & WithStyles<CssRules>;
 
 export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
-    ({ classes, onItemDoubleClick, onItemClick, onContextMenu, openHelpDialog }: ApiClientAuthorizationPanelRootProps) =>
-        <Card className={classes.card}>
-            <CardContent className={classes.cardContent}>
-                <Grid container direction="row" justify="flex-end">
-                    <Grid item xs={12} className={classes.helpIconGrid}>
-                        <Tooltip title="Api token - help">
-                            <IconButton onClick={openHelpDialog}>
-                                <HelpIcon />
-                            </IconButton>
-                        </Tooltip>
-                    </Grid>
-                    <Grid item xs={12}>
-                        <DataExplorer
-                            id={API_CLIENT_AUTHORIZATION_PANEL_ID}
-                            onRowClick={onItemClick}
-                            onRowDoubleClick={onItemDoubleClick}
-                            onContextMenu={onContextMenu}
-                            contextMenuColumn={true}
-                            hideColumnSelector
-                            hideSearchInput
-                            dataTableDefaultView={
-                                <DataTableDefaultView
-                                    icon={ShareMeIcon}
-                                    messages={[DEFAULT_MESSAGE]} />
-                            } />
-                    </Grid>
-                </Grid>
-            </CardContent>
-        </Card>
+    ({ classes, onItemDoubleClick, onItemClick, onContextMenu }: ApiClientAuthorizationPanelRootProps) =>
+        <div className={classes.root}><DataExplorer
+            id={API_CLIENT_AUTHORIZATION_PANEL_ID}
+            onRowClick={onItemClick}
+            onRowDoubleClick={onItemDoubleClick}
+            onContextMenu={onContextMenu}
+            contextMenuColumn={true}
+            hideColumnSelector
+            hideSearchInput
+            dataTableDefaultView={
+                <DataTableDefaultView
+                    icon={ShareMeIcon}
+                    messages={[DEFAULT_MESSAGE]} />
+            } /></div>
 );
\ No newline at end of file
index 89254dcc61b38b20ae99303a7f891856383bab11..9604bf50d0309a4bced9daa092e1a0df06c26aa4 100644 (file)
@@ -11,7 +11,6 @@ import {
     ApiClientAuthorizationPanelRootActionProps
 } from 'views/api-client-authorization-panel/api-client-authorization-panel-root';
 import { openApiClientAuthorizationContextMenu } from 'store/context-menu/context-menu-actions';
-import { openApiClientAuthorizationsHelpDialog } from 'store/api-client-authorizations/api-client-authorizations-actions';
 
 const mapStateToProps = (state: RootState): ApiClientAuthorizationPanelRootDataProps => {
     return {
@@ -25,9 +24,6 @@ const mapDispatchToProps = (dispatch: Dispatch): ApiClientAuthorizationPanelRoot
     },
     onItemClick: (resourceUuid: string) => { return; },
     onItemDoubleClick: uuid => { return; },
-    openHelpDialog: () => {
-        dispatch<any>(openApiClientAuthorizationsHelpDialog());
-    }
 });
 
 export const ApiClientAuthorizationPanel = connect(mapStateToProps, mapDispatchToProps)(ApiClientAuthorizationPanelRoot);
\ No newline at end of file
index 88638085fa736fc891c43aafbe2072e834eed291..f1278049963b7804b4681b4e1898a5d3f65e4c26 100644 (file)
@@ -7,7 +7,6 @@ import {
     StyleRulesCallback,
     WithStyles,
     withStyles,
-    Grid,
     Button
 } from '@material-ui/core';
 import { CollectionIcon } from 'components/icon/icon';
@@ -38,7 +37,7 @@ import { getResource, ResourcesState } from 'store/resources/resources';
 import { RootState } from 'store/store';
 import { CollectionResource } from 'models/collection';
 
-type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
+type CssRules = 'backLink' | 'backIcon' | 'root' | 'content';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     backLink: {
@@ -53,24 +52,13 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     backIcon: {
         marginRight: theme.spacing.unit
     },
-    card: {
-        width: '100%'
+    root: {
+        width: '100%',
     },
-    title: {
-        color: theme.palette.grey["700"]
+    content: {
+        // reserve space for the content address bar
+        height: `calc(100% - ${theme.spacing.unit * 7}px)`,
     },
-    iconHeader: {
-        fontSize: '1.875rem',
-        color: theme.customs.colors.green700
-    },
-    link: {
-        fontSize: '0.875rem',
-        color: theme.palette.primary.main,
-        textAlign: 'right',
-        '&:hover': {
-            cursor: 'pointer'
-        }
-    }
 });
 
 enum CollectionContentAddressPanelColumnNames {
@@ -162,14 +150,14 @@ export const CollectionsContentAddressPanel = withStyles(styles)(
     connect(mapStateToProps, mapDispatchToProps)(
         class extends React.Component<CollectionContentAddressPanelActionProps & CollectionContentAddressPanelDataProps & CollectionContentAddressDataProps & WithStyles<CssRules>> {
             render() {
-                return <Grid item xs={12}>
+                return <div className={this.props.classes.root}>
                     <Button
                         onClick={() => window.history.back()}
                         className={this.props.classes.backLink}>
                         <BackIcon className={this.props.classes.backIcon} />
                         Back
                     </Button>
-                    <DataExplorer
+                    <div className={this.props.classes.content}><DataExplorer
                         id={COLLECTIONS_CONTENT_ADDRESS_PANEL_ID}
                         hideSearchInput
                         onRowClick={this.props.onItemClick}
@@ -181,8 +169,8 @@ export const CollectionsContentAddressPanel = withStyles(styles)(
                             <DataTableDefaultView
                                 icon={CollectionIcon}
                                 messages={['Collections with this content address not found.']} />
-                        } />;
-                    </Grid >;
+                        } /></div>
+                    </div>;
             }
         }
     )
index e78b1f3d04c2b3494195479a9a3d959b9f5032cb..794e093f3d8d2129c5be959f97a4c6239b7b701a 100644 (file)
@@ -4,15 +4,20 @@
 
 import React from 'react';
 import {
-    StyleRulesCallback, WithStyles, withStyles,
-    IconButton, Grid, Tooltip, Typography, ExpansionPanel,
-    ExpansionPanelSummary, ExpansionPanelDetails
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    IconButton,
+    Grid,
+    Tooltip,
+    Typography,
+    Card, CardHeader, CardContent,
 } from '@material-ui/core';
 import { connect, DispatchProp } from "react-redux";
 import { RouteComponentProps } from 'react-router';
 import { ArvadosTheme } from 'common/custom-theme';
 import { RootState } from 'store/store';
-import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, ExpandIcon, CollectionOldVersionIcon } from 'components/icon/icon';
+import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, CollectionOldVersionIcon } from 'components/icon/icon';
 import { DetailsAttribute } from 'components/details-attribute/details-attribute';
 import { CollectionResource, getCollectionUrl } from 'models/collection';
 import { CollectionPanelFiles } from 'views-components/collection-panel-files/collection-panel-files';
@@ -33,9 +38,12 @@ import { COLLECTION_PANEL_LOAD_FILES, loadCollectionFiles, COLLECTION_PANEL_LOAD
 import { Link } from 'react-router-dom';
 import { Link as ButtonLink } from '@material-ui/core';
 import { ResourceOwnerWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers';
+import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
 
 type CssRules = 'root'
     | 'button'
+    | 'infoCard'
+    | 'propertiesCard'
     | 'filesCard'
     | 'iconHeader'
     | 'tag'
@@ -49,16 +57,21 @@ type CssRules = 'root'
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
-        display: 'flex',
-        flexFlow: 'column',
-        height: 'calc(100vh - 130px)', // (100% viewport height) - (top bar + breadcrumbs)
+        width: '100%',
     },
     button: {
         cursor: 'pointer'
     },
+    infoCard: {
+        paddingLeft: theme.spacing.unit * 2,
+        paddingRight: theme.spacing.unit * 2,
+        paddingBottom: theme.spacing.unit * 2,
+    },
+    propertiesCard: {
+        padding: 0,
+    },
     filesCard: {
-        marginBottom: theme.spacing.unit * 2,
-        flex: 1,
+        padding: 0,
     },
     iconHeader: {
         fontSize: '1.875rem',
@@ -133,10 +146,15 @@ export const CollectionPanel = withStyles(styles)(
         class extends React.Component<CollectionPanelProps> {
             render() {
                 const { classes, item, dispatch, isWritable, isOldVersion, isLoadingFiles, tooManyFiles } = this.props;
+                const panelsData: MPVPanelState[] = [
+                    {name: "Details"},
+                    {name: "Properties"},
+                    {name: "Files"},
+                ];
                 return item
-                    ? <div className={classes.root}>
-                        <ExpansionPanel data-cy='collection-info-panel' defaultExpanded>
-                            <ExpansionPanelSummary expandIcon={<ExpandIcon />}>
+                    ? <MPVContainer className={classes.root} spacing={8} direction="column" justify-content="flex-start" wrap="nowrap" panelStates={panelsData}>
+                        <MPVPanelContent xs="auto" data-cy='collection-info-panel'>
+                            <Card className={classes.infoCard}>
                                 <Grid container justify="space-between">
                                     <Grid item xs={11}><span>
                                         <IconButton onClick={this.openCollectionDetails}>
@@ -165,8 +183,6 @@ export const CollectionPanel = withStyles(styles)(
                                         </Tooltip>
                                     </Grid>
                                 </Grid>
-                            </ExpansionPanelSummary>
-                            <ExpansionPanelDetails>
                                 <Grid container justify="space-between">
                                     <Grid item xs={12}>
                                         <Typography variant="caption">
@@ -185,15 +201,12 @@ export const CollectionPanel = withStyles(styles)(
                                         }
                                     </Grid>
                                 </Grid>
-                            </ExpansionPanelDetails>
-                        </ExpansionPanel>
-
-                        <ExpansionPanel data-cy='collection-properties-panel' defaultExpanded>
-                            <ExpansionPanelSummary expandIcon={<ExpandIcon />}>
-                                {"Properties"}
-                            </ExpansionPanelSummary>
-                            <ExpansionPanelDetails>
-                                <Grid container>
+                            </Card>
+                        </MPVPanelContent>
+                        <MPVPanelContent xs="auto" data-cy='collection-properties-panel'>
+                            <Card className={classes.propertiesCard}>
+                                <CardHeader title="Properties" />
+                                <CardContent><Grid container>
                                     {isWritable && <Grid item xs={12}>
                                         <CollectionTagForm />
                                     </Grid>}
@@ -218,21 +231,23 @@ export const CollectionPanel = withStyles(styles)(
                                             : <div className={classes.centeredLabel}>No properties set on this collection.</div>
                                         }
                                     </Grid>
-                                </Grid>
-                            </ExpansionPanelDetails>
-                        </ExpansionPanel>
-                        <div className={classes.filesCard}>
-                            <CollectionPanelFiles
-                                isWritable={isWritable}
-                                isLoading={isLoadingFiles}
-                                tooManyFiles={tooManyFiles}
-                                loadFilesFunc={() => {
-                                    dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true));
-                                    dispatch<any>(loadCollectionFiles(this.props.item.uuid));
-                                }
-                                } />
-                        </div>
-                    </div>
+                                </Grid></CardContent>
+                            </Card>
+                        </MPVPanelContent>
+                        <MPVPanelContent xs>
+                            <Card className={classes.filesCard}>
+                                <CollectionPanelFiles
+                                    isWritable={isWritable}
+                                    isLoading={isLoadingFiles}
+                                    tooManyFiles={tooManyFiles}
+                                    loadFilesFunc={() => {
+                                        dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true));
+                                        dispatch<any>(loadCollectionFiles(this.props.item.uuid));
+                                    }
+                                    } />
+                            </Card>
+                        </MPVPanelContent>
+                    </MPVContainer>
                     : null;
             }
 
index 404baeb9d1c274ac199dc6ced4fb557963a47e1e..0b6532c13ed383fbb876cb972bae452d826f3b21 100644 (file)
@@ -41,7 +41,7 @@ import { getProperty } from 'store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
 import { CollectionResource } from 'models/collection';
 
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
@@ -51,6 +51,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     button: {
         marginLeft: theme.spacing.unit
     },
+    root: {
+        width: '100%',
+    },
 });
 
 export enum FavoritePanelColumnNames {
@@ -176,7 +179,7 @@ export const FavoritePanel = withStyles(styles)(
             }
 
             render() {
-                return <DataExplorer
+                return <div className={this.props.classes.root}><DataExplorer
                     id={FAVORITE_PANEL_ID}
                     onRowClick={this.handleRowClick}
                     onRowDoubleClick={this.handleRowDoubleClick}
@@ -187,7 +190,7 @@ export const FavoritePanel = withStyles(styles)(
                             icon={FavoriteIcon}
                             messages={['Your favorites list is empty.']}
                             />
-                    } />;
+                    } /></div>;
             }
         }
     )
index 4d15118c7fad3f905e0966b40ba5ee2c1d734e63..faefab107de3b0365f37a917eaf1f4a3c41cb673 100644 (file)
@@ -4,7 +4,7 @@
 
 import React from 'react';
 import { connect } from 'react-redux';
-import { Grid, Button, Typography } from "@material-ui/core";
+import { Grid, Button, Typography, StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { DataColumns } from 'components/data-table/data-table';
 import { SortDirection } from 'components/data-table/data-column';
@@ -22,6 +22,15 @@ import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ResourceKind } from 'models/resource';
 import { LinkClass, LinkResource } from 'models/link';
 import { navigateToGroupDetails } from 'store/navigation/navigation-action';
+import { ArvadosTheme } from 'common/custom-theme';
+
+type CssRules = "root";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+    }
+});
 
 export enum GroupsPanelColumnNames {
     GROUP = "Name",
@@ -74,14 +83,14 @@ export interface GroupsPanelProps {
     resources: ResourcesState;
 }
 
-export const GroupsPanel = connect(
+export const GroupsPanel = withStyles(styles)(connect(
     mapStateToProps, mapDispatchToProps
 )(
-    class GroupsPanel extends React.Component<GroupsPanelProps> {
+    class GroupsPanel extends React.Component<GroupsPanelProps & WithStyles<CssRules>> {
 
         render() {
             return (
-                <DataExplorer
+                <div className={this.props.classes.root}><DataExplorer
                     id={GROUPS_PANEL_ID}
                     onRowClick={noop}
                     onRowDoubleClick={this.props.onRowDoubleClick}
@@ -97,7 +106,7 @@ export const GroupsPanel = connect(
                                 <AddIcon /> New group
                         </Button>
                         </Grid>
-                    } />
+                    } /></div>
             );
         }
 
@@ -113,7 +122,7 @@ export const GroupsPanel = connect(
                 });
             }
         }
-    });
+    }));
 
 
 const GroupMembersCount = connect(
index 7a5f0503fb46b42dc67f868709bd9ec573cefc35..b32208cd74a04895c569d5e227b2cf037b4207e3 100644 (file)
@@ -11,10 +11,20 @@ import { DataTableDefaultView } from 'components/data-table-default-view/data-ta
 import { ResourcesState } from 'store/resources/resources';
 import { ShareMeIcon } from 'components/icon/icon';
 import { createTree } from 'models/tree';
-import { 
-    ResourceLinkUuid, ResourceLinkHead, ResourceLinkTail, 
-    ResourceLinkClass, ResourceLinkName } 
+import {
+    ResourceLinkUuid, ResourceLinkHead, ResourceLinkTail,
+    ResourceLinkClass, ResourceLinkName }
 from 'views-components/data-explorer/renderers';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+
+type CssRules = "root";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+    }
+});
 
 export enum LinkPanelColumnNames {
     NAME = "Name",
@@ -73,20 +83,20 @@ export interface LinkPanelRootActionProps {
     onItemDoubleClick: (item: string) => void;
 }
 
-export type LinkPanelRootProps = LinkPanelRootDataProps & LinkPanelRootActionProps;
+export type LinkPanelRootProps = LinkPanelRootDataProps & LinkPanelRootActionProps & WithStyles<CssRules>;
 
-export const LinkPanelRoot = (props: LinkPanelRootProps) => {
-    return <DataExplorer
+export const LinkPanelRoot = withStyles(styles)((props: LinkPanelRootProps) => {
+    return <div className={props.classes.root}><DataExplorer
         id={LINK_PANEL_ID}
         onRowClick={props.onItemClick}
         onRowDoubleClick={props.onItemDoubleClick}
         onContextMenu={props.onContextMenu}
-        contextMenuColumn={true} 
+        contextMenuColumn={true}
         hideColumnSelector
         hideSearchInput
         dataTableDefaultView={
             <DataTableDefaultView
                 icon={ShareMeIcon}
                 messages={['Your link list is empty.']} />
-        }/>;
-};
\ No newline at end of file
+        }/></div>;
+});
\ No newline at end of file
diff --git a/src/views/process-panel/process-details-attributes.tsx b/src/views/process-panel/process-details-attributes.tsx
new file mode 100644 (file)
index 0000000..4f26a71
--- /dev/null
@@ -0,0 +1,65 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { Grid } from "@material-ui/core";
+import { formatDate } from "common/formatters";
+import { resourceLabel } from "common/labels";
+import { DetailsAttribute } from "components/details-attribute/details-attribute";
+import { ProcessResource } from "models/process";
+import { ResourceKind } from "models/resource";
+import { ResourceOwnerWithName } from "views-components/data-explorer/renderers";
+
+type CssRules = 'label' | 'value';
+
+export const ProcessDetailsAttributes = (props: { item: ProcessResource, twoCol?: boolean, classes?: Record<CssRules, string> }) => {
+    const item = props.item;
+    const classes = props.classes || { label: '', value: '', button: '' };
+    const mdSize = props.twoCol ? 6 : 12;
+    return <Grid container>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                label='Owner' linkToUuid={item.ownerUuid}
+                uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+        </Grid>
+        <Grid item xs={12} md={12}>
+            <DetailsAttribute label='Status' value={item.state} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Last modified' value={formatDate(item.modifiedAt)} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Started at' value={formatDate(item.createdAt)} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Created at' value={formatDate(item.createdAt)} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Finished at' value={formatDate(item.expiresAt)} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Outputs' value={item.outputPath} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='UUID' linkToUuid={item.uuid} value={item.uuid} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Container UUID' value={item.containerUuid} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Priority' value={item.priority} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Runtime Constraints'
+            value={JSON.stringify(item.runtimeConstraints)} />
+        </Grid>
+        <Grid item xs={12} md={mdSize}>
+            <DetailsAttribute label='Docker Image locator'
+            linkToUuid={item.containerImage} value={item.containerImage} />
+        </Grid>
+    </Grid>;
+};
diff --git a/src/views/process-panel/process-details-card.tsx b/src/views/process-panel/process-details-card.tsx
new file mode 100644 (file)
index 0000000..1861078
--- /dev/null
@@ -0,0 +1,63 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Card,
+    CardHeader,
+    IconButton,
+    CardContent,
+    Tooltip,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon } from 'components/icon/icon';
+import { Process } from 'store/processes/process';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import { ProcessDetailsAttributes } from './process-details-attributes';
+
+type CssRules = 'card' | 'content' | 'title';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    card: {
+        height: '100%'
+    },
+    content: {
+        '&:last-child': {
+            paddingBottom: theme.spacing.unit * 2,
+        }
+    },
+    title: {
+        overflow: 'hidden',
+        paddingTop: theme.spacing.unit * 0.5
+    },
+});
+
+export interface ProcessDetailsCardDataProps {
+    process: Process;
+}
+
+type ProcessDetailsCardProps = ProcessDetailsCardDataProps & WithStyles<CssRules> & MPVPanelProps;
+
+export const ProcessDetailsCard = withStyles(styles)(
+    ({ classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => {
+        return <Card className={classes.card}>
+            <CardHeader
+                classes={{
+                    content: classes.title,
+                }}
+                title='Details'
+                action={ doHidePanel &&
+                        <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+                            <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+                        </Tooltip> } />
+            <CardContent className={classes.content}>
+                <ProcessDetailsAttributes item={process.containerRequest} twoCol />
+            </CardContent>
+        </Card>;
+    }
+);
+
index e70a047898d878b0a5947a0d85ac01204ae6e151..4c93801707864341a4698b93bb4449827ecd32cf 100644 (file)
@@ -8,13 +8,14 @@ import {
     CardHeader, IconButton, CardContent, Grid, Chip, Typography, Tooltip
 } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
-import { MoreOptionsIcon, ProcessIcon } from 'components/icon/icon';
+import { CloseIcon, MoreOptionsIcon, ProcessIcon } from 'components/icon/icon';
 import { DetailsAttribute } from 'components/details-attribute/details-attribute';
 import { Process } from 'store/processes/process';
 import { getProcessStatus, getProcessStatusColor } from 'store/processes/process';
 import { formatDate } from 'common/formatters';
 import classNames from 'classnames';
 import { ContainerState } from 'models/container';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 
 type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'chip' | 'link' | 'content' | 'title' | 'avatar' | 'cancelButton';
 
@@ -83,10 +84,10 @@ export interface ProcessInformationCardDataProps {
     cancelProcess: (uuid: string) => void;
 }
 
-type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles<CssRules, true>;
+type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles<CssRules, true> & MPVPanelProps;
 
 export const ProcessInformationCard = withStyles(styles, { withTheme: true })(
-    ({ classes, process, onContextMenu, theme, openProcessInputDialog, navigateToOutput, openWorkflow, cancelProcess }: ProcessInformationCardProps) => {
+    ({ classes, process, onContextMenu, theme, openProcessInputDialog, navigateToOutput, openWorkflow, cancelProcess, doHidePanel, panelName }: ProcessInformationCardProps) => {
         const { container } = process;
         const startedAt = container ? formatDate(container.startedAt) : 'N/A';
         const finishedAt = container ? formatDate(container.finishedAt) : 'N/A';
@@ -111,6 +112,10 @@ export const ProcessInformationCard = withStyles(styles, { withTheme: true })(
                                 <MoreOptionsIcon />
                             </IconButton>
                         </Tooltip>
+                        { doHidePanel &&
+                        <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+                            <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+                        </Tooltip> }
                     </div>
                 }
                 title={
index e7f66573ae02af087d4320eae88ca0c29166a29f..deb5f1b0dde8f19f46ef4bd5ed4889159953e78c 100644 (file)
@@ -3,13 +3,24 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { Grid } from '@material-ui/core';
+import { Grid, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { ProcessInformationCard } from './process-information-card';
 import { DefaultView } from 'components/default-view/default-view';
 import { ProcessIcon } from 'components/icon/icon';
 import { Process } from 'store/processes/process';
 import { SubprocessPanel } from 'views/subprocess-panel/subprocess-panel';
 import { SubprocessFilterDataProps } from 'components/subprocess-filter/subprocess-filter';
+import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
+import { ArvadosTheme } from 'common/custom-theme';
+import { ProcessDetailsCard } from './process-details-card';
+
+type CssRules = 'root';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+    },
+});
 
 export interface ProcessPanelRootDataProps {
     process?: Process;
@@ -26,12 +37,18 @@ export interface ProcessPanelRootActionProps {
     cancelProcess: (uuid: string) => void;
 }
 
-export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps;
+export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
+
+const panelsData: MPVPanelState[] = [
+    {name: "Info"},
+    {name: "Details", visible: false},
+    {name: "Subprocesses"},
+];
 
-export const ProcessPanelRoot = ({ process, ...props }: ProcessPanelRootProps) =>
+export const ProcessPanelRoot = withStyles(styles)(({ process, ...props }: ProcessPanelRootProps) =>
     process
-        ? <Grid container spacing={16} alignItems="stretch">
-            <Grid item sm={12} md={12}>
+        ? <MPVContainer className={props.classes.root} spacing={8} panelStates={panelsData}  justify-content="flex-start" direction="column" wrap="nowrap">
+            <MPVPanelContent xs="auto">
                 <ProcessInformationCard
                     process={process}
                     onContextMenu={event => props.onContextMenu(event, process)}
@@ -40,11 +57,14 @@ export const ProcessPanelRoot = ({ process, ...props }: ProcessPanelRootProps) =
                     openWorkflow={props.navigateToWorkflow}
                     cancelProcess={props.cancelProcess}
                 />
-            </Grid>
-            <Grid item sm={12} md={12}>
+            </MPVPanelContent>
+            <MPVPanelContent xs="auto">
+                <ProcessDetailsCard process={process} />
+            </MPVPanelContent>
+            <MPVPanelContent xs>
                 <SubprocessPanel />
-            </Grid>
-        </Grid>
+            </MPVPanelContent>
+        </MPVContainer>
         : <Grid container
             alignItems='center'
             justify='center'
@@ -52,5 +72,5 @@ export const ProcessPanelRoot = ({ process, ...props }: ProcessPanelRootProps) =
             <DefaultView
                 icon={ProcessIcon}
                 messages={['Process not found']} />
-        </Grid>;
+        </Grid>);
 
index 672645111743769f0dcec12644277479e43f0c8a..97f79517a21f3529448fec11903c9d36e1725b1d 100644 (file)
@@ -51,9 +51,7 @@ type CssRules = 'root' | "button";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
-        position: 'relative',
         width: '100%',
-        height: '100%'
     },
     button: {
         marginLeft: theme.spacing.unit
index ee09654a998ec0ea74df07be3c8bb25e28046374..b58aa2f09f3fbf73b3ee91093b9d346e3d7a6be0 100644 (file)
@@ -39,7 +39,7 @@ import { getResource, ResourcesState } from 'store/resources/resources';
 import { GroupContentsResource } from 'services/groups-service/groups-service';
 import { CollectionResource } from 'models/collection';
 
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
@@ -49,6 +49,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     button: {
         marginLeft: theme.spacing.unit
     },
+    root: {
+        width: '100%',
+    },
 });
 
 export enum PublicFavoritePanelColumnNames {
@@ -160,7 +163,7 @@ export const PublicFavoritePanel = withStyles(styles)(
     connect(mapStateToProps, mapDispatchToProps)(
         class extends React.Component<FavoritePanelProps> {
             render() {
-                return <DataExplorer
+                return <div className={this.props.classes.root}><DataExplorer
                     id={PUBLIC_FAVORITE_PANEL_ID}
                     onRowClick={this.props.onItemClick}
                     onRowDoubleClick={this.props.onItemDoubleClick}
@@ -170,7 +173,7 @@ export const PublicFavoritePanel = withStyles(styles)(
                         <DataTableDefaultView
                             icon={PublicFavoriteIcon}
                             messages={['Public favorites list is empty.']} />
-                    } />;
+                    } /></div>;
             }
         }
     )
index eb3127a7612f257ae0baa639998cfd56427ec008..219410c54e3583655e90ca7f2e82e9a05ccfdf63 100644 (file)
@@ -20,7 +20,7 @@ import {
 } from 'store/context-menu/context-menu-actions';
 import { GroupContentsResource } from 'services/groups-service/groups-service';
 
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
@@ -30,6 +30,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     button: {
         marginLeft: theme.spacing.unit
     },
+    root: {
+        width: '100%',
+    },
 });
 
 interface SharedWithMePanelDataProps {
@@ -46,13 +49,13 @@ export const SharedWithMePanel = withStyles(styles)(
     }))(
         class extends React.Component<SharedWithMePanelProps> {
             render() {
-                return <DataExplorer
+                return <div className={this.props.classes.root}><DataExplorer
                     id={SHARED_WITH_ME_PANEL_ID}
                     onRowClick={this.handleRowClick}
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={false}
-                    dataTableDefaultView={<DataTableDefaultView icon={ShareMeIcon} />} />;
+                    dataTableDefaultView={<DataTableDefaultView icon={ShareMeIcon} />} /></div>;
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
index b8e1b08141a3219baed3013c9b8a93cafbed1036..41a8f66b80457a6fdbe693a8f0884e6a7d7edb07 100644 (file)
@@ -17,6 +17,7 @@ import { DataTableDefaultView } from 'components/data-table-default-view/data-ta
 import { createTree } from 'models/tree';
 import { getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
 import { ResourcesState } from 'store/resources/resources';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 
 export enum SubprocessPanelColumnNames {
     NAME = "Name",
@@ -80,7 +81,7 @@ const DEFAULT_VIEW_MESSAGES = [
     'The current process may not have any or none matches current filtering.'
 ];
 
-export const SubprocessPanelRoot = (props: SubprocessPanelProps) => {
+export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps) => {
     return <DataExplorer
         id={SUBPROCESS_PANEL_ID}
         onRowClick={props.onItemClick}
@@ -91,5 +92,9 @@ export const SubprocessPanelRoot = (props: SubprocessPanelProps) => {
             <DataTableDefaultView
                 icon={ProcessIcon}
                 messages={DEFAULT_VIEW_MESSAGES} />
-        } />;
+        }
+        doHidePanel={props.doHidePanel}
+        doMaximizePanel={props.doMaximizePanel}
+        panelMaximized={props.panelMaximized}
+        panelName={props.panelName} />;
 };
\ No newline at end of file
index b67b666c973239a55215e46daee95010c042b5f3..d303c2f700713337364af00f9965812eefade800 100644 (file)
@@ -36,7 +36,7 @@ import {
     getTrashPanelTypeFilters
 } from 'store/resource-type-filters/resource-type-filters';
 
-type CssRules = "toolbar" | "button";
+type CssRules = "toolbar" | "button" | "root";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
@@ -46,6 +46,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     button: {
         marginLeft: theme.spacing.unit
     },
+    root: {
+        width: '100%',
+    },
 });
 
 export enum TrashPanelColumnNames {
@@ -146,7 +149,7 @@ export const TrashPanel = withStyles(styles)(
     }))(
         class extends React.Component<TrashPanelProps> {
             render() {
-                return <DataExplorer
+                return <div className={this.props.classes.root}><DataExplorer
                     id={TRASH_PANEL_ID}
                     onRowClick={this.handleRowClick}
                     onRowDoubleClick={this.handleRowDoubleClick}
@@ -156,7 +159,7 @@ export const TrashPanel = withStyles(styles)(
                         <DataTableDefaultView
                             icon={TrashIcon}
                             messages={['Your trash list is empty.']}/>
-                    } />;
+                    } /></div>;
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
index c86ca519ecca23f09bc95b670f53000429d207f2..5fb979a2194c62af81a0de64b37cdc509096ead0 100644 (file)
@@ -30,7 +30,7 @@ import { ShareMeIcon, AddIcon } from 'components/icon/icon';
 import { USERS_PANEL_ID, openUserCreateDialog } from 'store/users/users-actions';
 import { noop } from 'lodash';
 
-type UserPanelRules = "button";
+type UserPanelRules = "button" | 'root' | 'content';
 
 const styles = withStyles<UserPanelRules>(theme => ({
     button: {
@@ -39,6 +39,13 @@ const styles = withStyles<UserPanelRules>(theme => ({
         textAlign: 'right',
         alignSelf: 'center'
     },
+    root: {
+        width: '100%',
+    },
+    content: {
+        // reserve space for the tab bar
+        height: `calc(100% - ${theme.spacing.unit * 7}px)`,
+    }
 }));
 
 export enum UserPanelColumnNames {
@@ -149,13 +156,13 @@ export const UserPanel = compose(
 
             render() {
                 const { value } = this.state;
-                return <Paper>
+                return <Paper className={this.props.classes.root}>
                     <Tabs value={value} onChange={this.handleChange} fullWidth>
                         <Tab label="USERS" />
                         <Tab label="ACTIVITY" disabled />
                     </Tabs>
                     {value === 0 &&
-                        <span>
+                        <div className={this.props.classes.content}>
                             <DataExplorer
                                 id={USERS_PANEL_ID}
                                 onRowClick={noop}
@@ -178,7 +185,7 @@ export const UserPanel = compose(
                                         icon={ShareMeIcon}
                                         messages={['Your user list is empty.']} />
                                 } />
-                        </span>}
+                        </div>}
                 </Paper>;
             }
 
index 9ce93bf2ae6e38214186defb3bc87dc02b6455d8..1c6bf03fd9baf73dc2dd1b2675d219c70ea1379b 100644 (file)
@@ -129,6 +129,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         minWidth: 0,
         paddingLeft: theme.spacing.unit * 3,
         paddingRight: theme.spacing.unit * 3,
+        // Reserve vertical space for app bar + MainContentBar
+        minHeight: `calc(100vh - ${theme.spacing.unit * 16}px)`,
+        display: 'flex',
     }
 });