Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 17 Dec 2018 14:51:40 +0000 (15:51 +0100)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 17 Dec 2018 14:51:40 +0000 (15:51 +0100)
Feature #14505

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

25 files changed:
src/common/formatters.ts
src/components/data-explorer/data-explorer.tsx
src/routes/route-change-handlers.ts
src/store/advanced-tab/advanced-tab.ts
src/store/compute-nodes/compute-nodes-actions.ts
src/store/compute-nodes/compute-nodes-middleware-service.ts [new file with mode: 0644]
src/store/compute-nodes/compute-nodes-reducer.ts [deleted file]
src/store/context-menu/context-menu-actions.ts
src/store/link-panel/link-panel-actions.ts
src/store/store.ts
src/store/workbench/workbench-actions.ts
src/views-components/context-menu/action-sets/process-action-set.ts
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/data-explorer/with-resources.tsx [new file with mode: 0644]
src/views-components/main-app-bar/account-menu.tsx
src/views-components/main-app-bar/admin-menu.tsx
src/views-components/main-app-bar/help-menu.tsx
src/views-components/side-panel/side-panel.tsx
src/views/compute-node-panel/compute-node-panel-root.tsx
src/views/compute-node-panel/compute-node-panel.tsx
src/views/link-panel/link-panel-root.tsx
src/views/link-panel/link-panel.tsx
src/views/run-process-panel/run-process-first-step.tsx
src/views/workflow-panel/workflow-description-card.tsx

index 5383c66e949f59ee1b2d1258f0d2d1402f485bdb..ae50ee8adda3ace82a380b0714961e3ea4fb4394 100644 (file)
@@ -8,9 +8,9 @@ export const formatDate = (isoDate?: string) => {
     if (isoDate) {
         const date = new Date(isoDate);
         const text = date.toLocaleString();
-        return text === 'Invalid Date' ? "" : text;
+        return text === 'Invalid Date' ? "(none)" : text;
     }
-    return "";
+    return "(none)";
 };
 
 export const formatFileSize = (size?: number) => {
index 4175fbc6f23ccc3e18d50a7b70a0dc13808d4fe2..b6ca215d56463ec7f7ba3742ae06f5105ffa69ad 100644 (file)
@@ -49,6 +49,7 @@ interface DataExplorerDataProps<T> {
     paperProps?: PaperProps;
     actions?: React.ReactNode;
     hideSearchInput?: boolean;
+    paperKey?: string;
 }
 
 interface DataExplorerActionProps<T> {
@@ -79,9 +80,10 @@ export const DataExplorer = withStyles(styles)(
                 columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
-                dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput
+                dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
+                paperKey
             } = this.props;
-            return <Paper className={classes.root} {...paperProps}>
+            return <Paper className={classes.root} {...paperProps} key={paperKey}>
                 <Toolbar className={classes.toolbar}>
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
                         {!hideSearchInput && <div className={classes.searchBox}>
@@ -105,8 +107,7 @@ export const DataExplorer = withStyles(styles)(
                     onSortToggle={onSortToggle}
                     extractKey={extractKey}
                     working={working}
-                    defaultView={dataTableDefaultView}
-                />
+                    defaultView={dataTableDefaultView} />
                 <Toolbar className={classes.footer}>
                     <Grid container justify="flex-end">
                         <TablePagination
index 7b37509f90b94be0cd053e3052e52ef438bac8fc..03e2a38aee5deb3de1a0e0663bae503d2bfe64aa 100644 (file)
@@ -8,6 +8,8 @@ import * as Routes from '~/routes/routes';
 import * as WorkbenchActions from '~/store/workbench/workbench-actions';
 import { navigateToRootProject } from '~/store/navigation/navigation-action';
 import { dialogActions } from '~/store/dialog/dialog-actions';
+import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
+import { searchBarActions } from '~/store/search-bar/search-bar-actions';
 
 export const addRouteChangeHandlers = (history: History, store: RootStore) => {
     const handler = handleLocationChange(store);
@@ -42,6 +44,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const linksMatch = Routes.matchLinksRoute(pathname);
 
     store.dispatch(dialogActions.CLOSE_ALL_DIALOGS());
+    store.dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
+    store.dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
 
     if (projectMatch) {
         store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id));
index 659b6e49fb7b805aa31d45e960c021808576f849..0cb1c74038503c5c1f80fc58c36cb07d9f0803e5 100644 (file)
@@ -241,7 +241,8 @@ export const openAdvancedTabDialog = (uuid: string) =>
                 dispatch<any>(initAdvancedTabDialog(advanceDataUser));
                 break;
             case ResourceKind.NODE:
-                const dataComputeNode = getState().computeNodes.find(node => node.uuid === uuid);
+                const computeNodeResources = getState().resources;
+                const dataComputeNode = getResource<NodeResource>(uuid)(computeNodeResources);
                 const advanceDataComputeNode = advancedTabData({
                     uuid,
                     metadata: '',
@@ -251,7 +252,7 @@ export const openAdvancedTabDialog = (uuid: string) =>
                     resourceKind: ComputeNodeData.COMPUTE_NODE,
                     resourcePrefix: ResourcePrefix.COMPUTE_NODES,
                     resourceKindProperty: ComputeNodeData.PROPERTIES,
-                    property: dataComputeNode!.properties
+                    property: dataComputeNode ? dataComputeNode.properties : {}
                 });
                 dispatch<any>(initAdvancedTabDialog(advanceDataComputeNode));
                 break;
index 659b1e8674c7c63138d76c106c3cc23941665e02..f2f6ad0741e6312f7f5e9cd0683288c13ae74d93 100644 (file)
@@ -3,21 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { RootState } from '~/store/store';
 import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
-import { ServiceRepository } from "~/services/services";
-import { NodeResource } from '~/models/node';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { navigateToRootProject } from '~/store/navigation/navigation-action';
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { getResource } from '~/store/resources/resources';
+import { ServiceRepository } from "~/services/services";
+import { NodeResource } from '~/models/node';
 
-export const computeNodesActions = unionize({
-    SET_COMPUTE_NODES: ofType<NodeResource[]>(),
-    REMOVE_COMPUTE_NODE: ofType<string>()
-});
-
-export type ComputeNodesActions = UnionOf<typeof computeNodesActions>;
+export const COMPUTE_NODE_PANEL_ID = "computeNodeId";
+export const computeNodesActions = bindDataExplorerActions(COMPUTE_NODE_PANEL_ID);
 
 export const COMPUTE_NODE_REMOVE_DIALOG = 'computeNodeRemoveDialog';
 export const COMPUTE_NODE_ATTRIBUTES_DIALOG = 'computeNodeAttributesDialog';
@@ -28,8 +25,7 @@ export const loadComputeNodesPanel = () =>
         if (user && user.isAdmin) {
             try {
                 dispatch(setBreadcrumbs([{ label: 'Compute Nodes' }]));
-                const response = await services.nodeService.list();
-                dispatch(computeNodesActions.SET_COMPUTE_NODES(response.items));
+                dispatch(computeNodesActions.REQUEST_ITEMS());
             } catch (e) {
                 return;
             }
@@ -41,7 +37,8 @@ export const loadComputeNodesPanel = () =>
 
 export const openComputeNodeAttributesDialog = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const computeNode = getState().computeNodes.find(node => node.uuid === uuid);
+        const { resources } = getState();
+        const computeNode = getResource<NodeResource>(uuid)(resources);
         dispatch(dialogActions.OPEN_DIALOG({ id: COMPUTE_NODE_ATTRIBUTES_DIALOG, data: { computeNode } }));
     };
 
@@ -63,7 +60,7 @@ export const removeComputeNode = (uuid: string) =>
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
         try {
             await services.nodeService.delete(uuid);
-            dispatch(computeNodesActions.REMOVE_COMPUTE_NODE(uuid));
+            dispatch(computeNodesActions.REQUEST_ITEMS());
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Compute node has been successfully removed.', hideDuration: 2000 }));
         } catch (e) {
             return;
diff --git a/src/store/compute-nodes/compute-nodes-middleware-service.ts b/src/store/compute-nodes/compute-nodes-middleware-service.ts
new file mode 100644 (file)
index 0000000..792da7a
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateResources } from '~/store/resources/resources-actions';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { computeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { ListResults } from '~/services/common-service/common-service';
+import { NodeResource } from '~/models/node';
+import { SortDirection } from '~/components/data-table/data-column';
+import { ComputeNodePanelColumnNames } from '~/views/compute-node-panel/compute-node-panel-root';
+
+export class ComputeNodeMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const state = api.getState();
+        const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+        try {
+            const response = await this.services.nodeService.list(getParams(dataExplorer));
+            api.dispatch(updateResources(response.items));
+            api.dispatch(setItems(response));
+        } catch {
+            api.dispatch(couldNotFetchLinks());
+        }
+    }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    order: getOrder(dataExplorer)
+});
+
+const getOrder = (dataExplorer: DataExplorer) => {
+    const sortColumn = getSortColumn(dataExplorer);
+    const order = new OrderBuilder<NodeResource>();
+    if (sortColumn) {
+        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+            ? OrderDirection.ASC
+            : OrderDirection.DESC;
+
+        const columnName = sortColumn && sortColumn.name === ComputeNodePanelColumnNames.UUID ? "uuid" : "modifiedAt";
+        return order
+            .addOrder(sortDirection, columnName)
+            .getOrder();
+    } else {
+        return order.getOrder();
+    }
+};
+
+export const setItems = (listResults: ListResults<NodeResource>) =>
+    computeNodesActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const couldNotFetchLinks = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch compute nodes.',
+        kind: SnackbarKind.ERROR
+    });
diff --git a/src/store/compute-nodes/compute-nodes-reducer.ts b/src/store/compute-nodes/compute-nodes-reducer.ts
deleted file mode 100644 (file)
index 44a3780..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { computeNodesActions, ComputeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
-import { NodeResource } from '~/models/node';
-
-export type ComputeNodesState = NodeResource[];
-
-const initialState: ComputeNodesState = [];
-
-export const computeNodesReducer = (state: ComputeNodesState = initialState, action: ComputeNodesActions): ComputeNodesState =>
-    computeNodesActions.match(action, {
-        SET_COMPUTE_NODES: nodes => nodes,
-        REMOVE_COMPUTE_NODE: (uuid: string) => state.filter((computeNode) => computeNode.uuid !== uuid),
-        default: () => state
-    });
\ No newline at end of file
index e9b08a8417afdb3d69adb32e8ba44847bb6d9a39..c43d5685655c8667bc272246bedf6597b41ca7c9 100644 (file)
@@ -17,8 +17,8 @@ import { RepositoryResource } from '~/models/repositories';
 import { SshKeyResource } from '~/models/ssh-key';
 import { VirtualMachinesResource } from '~/models/virtual-machines';
 import { KeepServiceResource } from '~/models/keep-services';
-import { NodeResource } from '~/models/node';
 import { ApiClientAuthorization } from '~/models/api-client-authorization';
+import { ProcessResource } from '~/models/process';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -35,7 +35,7 @@ export type ContextMenuResource = {
     kind: ResourceKind,
     menuKind: ContextMenuKind;
     isTrashed?: boolean;
-    index?: number
+    outputUuid?: string;
 };
 
 export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
@@ -111,18 +111,18 @@ export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>,
         }));
     };
 
-export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>, computeNode: NodeResource) =>
+export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
     (dispatch: Dispatch) => {
         dispatch<any>(openContextMenu(event, {
             name: '',
-            uuid: computeNode.uuid,
-            ownerUuid: computeNode.ownerUuid,
+            uuid: resourceUuid,
+            ownerUuid: '',
             kind: ResourceKind.NODE,
             menuKind: ContextMenuKind.NODE
         }));
     };
 
-export const openApiClientAuthorizationContextMenu = 
+export const openApiClientAuthorizationContextMenu =
     (event: React.MouseEvent<HTMLElement>, apiClientAuthorization: ApiClientAuthorization) =>
         (dispatch: Dispatch) => {
             dispatch<any>(openContextMenu(event, {
@@ -178,15 +178,18 @@ export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, i
 
 export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const resource = {
-            uuid: process.containerRequest.uuid,
-            ownerUuid: process.containerRequest.ownerUuid,
-            kind: ResourceKind.PROCESS,
-            name: process.containerRequest.name,
-            description: process.containerRequest.description,
-            menuKind: ContextMenuKind.PROCESS
-        };
-        dispatch<any>(openContextMenu(event, resource));
+        const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
+        if (res) {
+            dispatch<any>(openContextMenu(event, {
+                uuid: res.uuid,
+                ownerUuid: res.ownerUuid,
+                kind: ResourceKind.PROCESS,
+                name: res.name,
+                description: res.description,
+                outputUuid: res.outputUuid || '',
+                menuKind: ContextMenuKind.PROCESS
+            }));
+        }
     };
 
 export const resourceKindToContextMenuKind = (uuid: string) => {
index 944c1bd17703004742f7b5bc16212bf7982ba84a..7cbc507342cd3b301035852f71b989a36837d4be 100644 (file)
@@ -18,6 +18,12 @@ export const linkPanelActions = bindDataExplorerActions(LINK_PANEL_ID);
 export const LINK_REMOVE_DIALOG = 'linkRemoveDialog';
 export const LINK_ATTRIBUTES_DIALOG = 'linkAttributesDialog';
 
+export const loadLinkPanel = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([{ label: 'Links' }]));
+        dispatch(linkPanelActions.REQUEST_ITEMS());
+    };
+
 export const openLinkAttributesDialog = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const { resources } = getState();
@@ -38,12 +44,6 @@ export const openLinkRemoveDialog = (uuid: string) =>
         }));
     };
 
-export const loadLinkPanel = () =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(setBreadcrumbs([{ label: 'Links' }]));
-        dispatch(linkPanelActions.REQUEST_ITEMS());
-    };
-
 export const removeLink = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
index 3aef8f500013fb301d6e6089296b9a05909a561e..14a6ba11d7651930b878919fa3f3f5d2bb54205a 100644 (file)
@@ -48,7 +48,6 @@ import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
 import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
 import { UserMiddlewareService } from '~/store/users/user-panel-middleware-service';
 import { USERS_PANEL_ID } from '~/store/users/users-actions';
-import { computeNodesReducer } from '~/store/compute-nodes/compute-nodes-reducer';
 import { apiClientAuthorizationsReducer } from '~/store/api-client-authorizations/api-client-authorizations-reducer';
 import { GroupsPanelMiddlewareService } from '~/store/groups-panel/groups-panel-middleware-service';
 import { GROUPS_PANEL_ID } from '~/store/groups-panel/groups-panel-actions';
@@ -56,6 +55,8 @@ import { GroupDetailsPanelMiddlewareService } from '~/store/group-details-panel/
 import { GROUP_DETAILS_PANEL_ID } from '~/store/group-details-panel/group-details-panel-actions';
 import { LINK_PANEL_ID } from '~/store/link-panel/link-panel-actions';
 import { LinkMiddlewareService } from '~/store/link-panel/link-panel-middleware-service';
+import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actions';
+import { ComputeNodeMiddlewareService } from '~/store/compute-nodes/compute-nodes-middleware-service';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -100,6 +101,9 @@ export function configureStore(history: History, services: ServiceRepository): R
     const linkPanelMiddleware = dataExplorerMiddleware(
         new LinkMiddlewareService(services, LINK_PANEL_ID)
     );
+    const computeNodeMiddleware = dataExplorerMiddleware(
+        new ComputeNodeMiddlewareService(services, COMPUTE_NODE_PANEL_ID)
+    );
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
@@ -112,7 +116,8 @@ export function configureStore(history: History, services: ServiceRepository): R
         userPanelMiddleware,
         groupsPanelMiddleware,
         groupDetailsPanelMiddleware,
-        linkPanelMiddleware
+        linkPanelMiddleware,
+        computeNodeMiddleware,
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
@@ -144,6 +149,5 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     virtualMachines: virtualMachinesReducer,
     repositories: repositoriesReducer,
     keepServices: keepServicesReducer,
-    computeNodes: computeNodesReducer,
     apiClientAuthorizations: apiClientAuthorizationsReducer
 });
index af2afab29f53ba33b2a06bed69e6174ddf085e9b..5e9dc285ef2050f98794b4b5cfc632070ce32d4b 100644 (file)
@@ -60,9 +60,10 @@ import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions
 import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions';
 import { loadUsersPanel, userBindedActions } from '~/store/users/users-actions';
 import { loadLinkPanel, linkPanelActions } from '~/store/link-panel/link-panel-actions';
+import { loadComputeNodesPanel, computeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
 import { linkPanelColumns } from '~/views/link-panel/link-panel-root';
 import { userPanelColumns } from '~/views/user-panel/user-panel';
-import { loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions';
+import { computeNodePanelColumns } from '~/views/compute-node-panel/compute-node-panel-root';
 import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions';
 import * as groupPanelActions from '~/store/groups-panel/groups-panel-actions';
 import { groupsPanelColumns } from '~/views/groups-panel/groups-panel';
@@ -105,6 +106,7 @@ export const loadWorkbench = () =>
                 dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
                 dispatch(groupDetailsPanelActions.GroupDetailsPanelActions.SET_COLUMNS({columns: groupDetailsPanelColumns}));
                 dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
+                dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
                 dispatch<any>(initSidePanelTree());
                 if (router.location) {
                     const match = matchRootRoute(router.location.pathname);
index 2d152543caa248eb9e5b19e1edc35993aab396a4..05242fbc752009b0342137dcdc0ae8d460cad65a 100644 (file)
@@ -15,11 +15,12 @@ import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
 import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
 import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
 import { openProcessCommandDialog } from '~/store/processes/process-command-actions';
-import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
 import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
 import { openProcessInputDialog } from "~/store/processes/process-input-actions";
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { openRemoveProcessDialog } from "~/store/processes/processes-actions";
+import { navigateToOutput } from "~/store/process-panel/process-panel-actions";
 
 export const processActionSet: ContextMenuActionSet = [[
     {
@@ -76,7 +77,9 @@ export const processActionSet: ContextMenuActionSet = [[
         icon: OutputIcon,
         name: "Outputs",
         execute: (dispatch, resource) => {
-            // add code
+            if(resource.outputUuid){
+                dispatch<any>(navigateToOutput(resource.outputUuid));
+            }
         }
     },
     {
@@ -113,12 +116,12 @@ export const processActionSet: ContextMenuActionSet = [[
         execute: (dispatch, resource) => {
             dispatch<any>(openAdvancedTabDialog(resource.uuid));
         }
+    },
+    {
+        name: "Remove",
+        icon: RemoveIcon,
+        execute: (dispatch, resource) => {
+            dispatch<any>(openRemoveProcessDialog(resource.uuid));
+        }
     }
-    // {
-    //     icon: RemoveIcon,
-    //     name: "Remove",
-    //     execute: (dispatch, resource) => {
-    //         // add code
-    //     }
-    // }
 ]];
index 710d202dfe25997c66dcde62f44ead0d9e469b10..8cddf3ba1a5eea67880519a292a46d5146c58e5f 100644 (file)
@@ -23,7 +23,8 @@ interface Props {
 const mapStateToProps = (state: RootState, { id }: Props) => {
     const progress = state.progressIndicator.find(p => p.id === id);
     const working = progress && progress.working;
-    return { ...getDataExplorer(state.dataExplorer, id), working };
+    const currentRoute = state.router.location ? state.router.location.pathname : '';
+    return { ...getDataExplorer(state.dataExplorer, id), working, paperKey: currentRoute };
 };
 
 const mapDispatchToProps = () => {
index a4713c8dc2337a8df20bb27d2094beff6fab32c5..ce4d430fd18597a0c4af04925eb31fa9a636dd77 100644 (file)
@@ -25,7 +25,7 @@ import { UserResource } from '~/models/user';
 import { toggleIsActive, toggleIsAdmin } from '~/store/users/users-actions';
 import { LinkResource } from '~/models/link';
 import { navigateTo } from '~/store/navigation/navigation-action';
-import { Link } from 'react-router-dom';
+import { withResource, getDataFromResource, withResourceData } from '~/views-components/data-explorer/with-resources';
 
 const renderName = (item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
@@ -191,6 +191,34 @@ export const ResourceUsername = connect(
         return resource || { username: '' };
     })(renderUsername);
 
+// Compute Node Resources
+const renderNodeDate = (date: string) =>
+    <Typography noWrap>{formatDate(date)}</Typography>;
+
+const renderNodeData = (data: string) => {
+    return <Typography noWrap>{data}</Typography>;
+};
+
+const renderNodeInfo = (data: string) => {
+    return <Typography>{JSON.stringify(data, null, 4)}</Typography>;
+};
+
+export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
+
+export const ComputeNodeUuid = withResourceData('uuid', renderNodeData);
+
+export const ComputeNodeDomain = withResourceData('domain', renderNodeData);
+
+export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderNodeDate);
+
+export const ComputeNodeHostname = withResourceData('hostname', renderNodeData);
+
+export const ComputeNodeIpAddress = withResourceData('ipAddress', renderNodeData);
+
+export const ComputeNodeJobUuid = withResourceData('jobUuid', renderNodeData);
+
+export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderNodeDate);
+
 // Links Resources
 const renderLinkName = (item: { name: string }) =>
     <Typography noWrap>{item.name || '(none)'}</Typography>;
diff --git a/src/views-components/data-explorer/with-resources.tsx b/src/views-components/data-explorer/with-resources.tsx
new file mode 100644 (file)
index 0000000..54c9396
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { getResource } from '~/store/resources/resources';
+import { Resource } from '~/models/resource';
+
+interface WithResourceProps {
+    resource?: Resource;
+}
+
+export const withResource = (component: React.ComponentType<WithResourceProps & { uuid: string }>) =>
+    connect<WithResourceProps>(
+        (state: RootState, props: { uuid: string }): WithResourceProps => ({
+            resource: getResource(props.uuid)(state.resources)
+        })
+    )(component);
+
+export const getDataFromResource = (property: string, resource?: Resource) => {
+    return resource && resource[property] ? resource[property] : '(none)';
+};
+
+export const withResourceData = (property: string, render: (data: any) => React.ReactElement<any>) =>
+    withResource(({ resource }) => render(getDataFromResource(property, resource)));
index ec95e38a33cfbba5f5eb9a7d292457307f02511a..53a5753d0c2130380ce900efa2c1f6f73b33698d 100644 (file)
@@ -17,19 +17,22 @@ import { openRepositoriesPanel } from '~/store/repositories/repositories-actions
 
 interface AccountMenuProps {
     user?: User;
+    currentRoute: string;
 }
 
 const mapStateToProps = (state: RootState): AccountMenuProps => ({
-    user: state.auth.user
+    user: state.auth.user,
+    currentRoute: state.router.location ? state.router.location.pathname : ''
 });
 
 export const AccountMenu = connect(mapStateToProps)(
-    ({ user, dispatch }: AccountMenuProps & DispatchProp<any>) =>
+    ({ user, dispatch, currentRoute }: AccountMenuProps & DispatchProp<any>) =>
         user
             ? <DropdownMenu
                 icon={<UserPanelIcon />}
                 id="account-menu"
-                title="Account Management">
+                title="Account Management"
+                key={currentRoute}>
                 <MenuItem>
                     {getUserFullname(user)}
                 </MenuItem>
index 8f9527e03f984fce52d38645cf7f07754a7a4753..9b94c064ed919dc3ed28755456ca729fb4508a27 100644 (file)
@@ -17,19 +17,22 @@ import { openUserPanel } from "~/store/users/users-actions";
 
 interface AdminMenuProps {
     user?: User;
+    currentRoute: string;
 }
 
 const mapStateToProps = (state: RootState): AdminMenuProps => ({
-    user: state.auth.user
+    user: state.auth.user,
+    currentRoute: state.router.location ? state.router.location.pathname : ''
 });
 
 export const AdminMenu = connect(mapStateToProps)(
-    ({ user, dispatch }: AdminMenuProps & DispatchProp<any>) =>
+    ({ user, dispatch, currentRoute }: AdminMenuProps & DispatchProp<any>) =>
         user
             ? <DropdownMenu
                 icon={<AdminMenuIcon />}
                 id="admin-menu"
-                title="Admin Panel">
+                title="Admin Panel"
+                key={currentRoute}>
                 <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
                 <MenuItem onClick={() => dispatch(openAdminVirtualMachines())}>Virtual Machines</MenuItem>
                 <MenuItem onClick={() => dispatch(NavigationAction.navigateToSshKeysAdmin)}>Ssh Keys</MenuItem>
index 26604228fc21ac9fbfb79ba21a96a8372324655c..94da69e7c62311d94d4e324462cd7a73d3e02ac5 100644 (file)
@@ -3,11 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { MenuItem, Typography, ListSubheader } from "@material-ui/core";
+import { MenuItem, Typography } from "@material-ui/core";
 import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
 import { ImportContactsIcon, HelpIcon } from "~/components/icon/icon";
 import { ArvadosTheme } from '~/common/custom-theme';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { RootState } from "~/store/store";
+import { compose } from "redux";
+import { connect } from "react-redux";
 
 type CssRules = 'link' | 'icon' | 'title' | 'linkTitle';
 
@@ -52,22 +55,33 @@ const links = [
     },
 ];
 
-export const HelpMenu = withStyles(styles)(
-    ({ classes }: WithStyles<CssRules>) =>
-        <DropdownMenu
-            icon={<HelpIcon />}
-            id="help-menu"
-            title="Help">
-            <MenuItem disabled>Help</MenuItem>
-            {
-                links.map(link =>
-                    <MenuItem key={link.title}>
-                        <a href={link.link} target="_blank" className={classes.link}>
-                            <ImportContactsIcon className={classes.icon} />
-                            <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
-                        </a>
-                    </MenuItem>
-                )
-            }
-        </DropdownMenu>
-);
+interface HelpMenuProps {
+    currentRoute: string;
+}
+
+const mapStateToProps = ({ router }: RootState) => ({
+    currentRoute: router.location ? router.location.pathname : '',
+});
+
+export const HelpMenu = compose(
+    connect(mapStateToProps),
+    withStyles(styles))(
+        ({ classes, currentRoute }: HelpMenuProps & WithStyles<CssRules>) =>
+            <DropdownMenu
+                icon={<HelpIcon />}
+                id="help-menu"
+                title="Help"
+                key={currentRoute}>
+                <MenuItem disabled>Help</MenuItem>
+                {
+                    links.map(link =>
+                        <MenuItem key={link.title}>
+                            <a href={link.link} target="_blank" className={classes.link}>
+                                <ImportContactsIcon className={classes.icon} />
+                                <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
+                            </a>
+                        </MenuItem>
+                    )
+                }
+            </DropdownMenu>
+    );
index 12e82dfb102f9ade1df6cd928a19c1957b7ebe3b..62d9dc3532e2efc48605b261e99ddffaa20d7133 100644 (file)
@@ -8,7 +8,7 @@ import { ArvadosTheme } from '~/common/custom-theme';
 import { SidePanelTree, SidePanelTreeProps } from '~/views-components/side-panel-tree/side-panel-tree';
 import { compose, Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action';
+import { navigateFromSidePanel } from '~/store/side-panel/side-panel-action';
 import { Grid } from '@material-ui/core';
 import { SidePanelButton } from '~/views-components/side-panel-button/side-panel-button';
 import { RootState } from '~/store/store';
@@ -33,14 +33,15 @@ const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
     }
 });
 
-const mapStateToProps = (state: RootState) => ({
+const mapStateToProps = ({ router }: RootState) => ({
+    currentRoute: router.location ? router.location.pathname : '',
 });
 
 export const SidePanel = withStyles(styles)(
     connect(mapStateToProps, mapDispatchToProps)(
-    ({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
-    <Grid item xs>
-        <SidePanelButton />
-        <SidePanelTree {...props} />
-    </Grid>
-));
+        ({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps & { currentRoute: string }) =>
+            <Grid item xs>
+                <SidePanelButton key={props.currentRoute} />
+                <SidePanelTree {...props} />
+            </Grid>
+    ));
index 2d325b514a8bb20897d3f4ca5959b4b5496af14e..feaadb5e5b86a92232354759506c185c3a334cca 100644 (file)
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { ShareMeIcon } from '~/components/icon/icon';
+import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actions';
+import { DataColumns } from '~/components/data-table/data-table';
+import { SortDirection } from '~/components/data-table/data-column';
+import { createTree } from '~/models/tree';
 import { 
-    StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Table, 
-    TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton 
-} from '@material-ui/core';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { MoreOptionsIcon } from '~/components/icon/icon';
-import { NodeResource } from '~/models/node';
-import { formatDate } from '~/common/formatters';
+    ComputeNodeUuid, ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
+    ComputeNodeFirstPingAt, ComputeNodeLastPingAt, ComputeNodeIpAddress
+} from '~/views-components/data-explorer/renderers';
+import { ResourcesState } from '~/store/resources/resources';
 
-type CssRules = 'root' | 'tableRow';
+export enum ComputeNodePanelColumnNames {
+    INFO = 'Info',
+    UUID = 'UUID',
+    DOMAIN = 'Domain',
+    FIRST_PING_AT = 'First ping at',
+    HOSTNAME = 'Hostname',
+    IP_ADDRESS = 'IP Address',
+    JOB = 'Job',
+    LAST_PING_AT = 'Last ping at'
+}
 
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
-        width: '100%',
-        overflow: 'auto'
+export const computeNodePanelColumns: DataColumns<string> = [
+    {
+        name: ComputeNodePanelColumnNames.INFO,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ComputeNodeInfo uuid={uuid} />
+    },
+    {
+        name: ComputeNodePanelColumnNames.UUID,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ComputeNodeUuid uuid={uuid} />
+    },
+    {
+        name: ComputeNodePanelColumnNames.DOMAIN,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ComputeNodeDomain uuid={uuid} />
+    },
+    {
+        name: ComputeNodePanelColumnNames.FIRST_PING_AT,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ComputeNodeFirstPingAt uuid={uuid} />
     },
-    tableRow: {
-        '& th': {
-            whiteSpace: 'nowrap'
-        }
+    {
+        name: ComputeNodePanelColumnNames.HOSTNAME,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ComputeNodeHostname uuid={uuid} />
+    },
+    {
+        name: ComputeNodePanelColumnNames.IP_ADDRESS,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ComputeNodeIpAddress uuid={uuid} />
+    },
+    {
+        name: ComputeNodePanelColumnNames.JOB,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ComputeNodeJobUuid uuid={uuid} />
+    },
+    {
+        name: ComputeNodePanelColumnNames.LAST_PING_AT,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ComputeNodeLastPingAt uuid={uuid} />
     }
-});
+];
+
+const DEFAULT_MESSAGE = 'Your compute node list is empty.';
 
 export interface ComputeNodePanelRootActionProps {
-    openRowOptions: (event: React.MouseEvent<HTMLElement>, computeNode: NodeResource) => void;
+    onItemClick: (item: string) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
+    onItemDoubleClick: (item: string) => void;
 }
 
 export interface ComputeNodePanelRootDataProps {
-    computeNodes: NodeResource[];
-    hasComputeNodes: boolean;
+    resources: ResourcesState;
 }
 
-type ComputeNodePanelRootProps = ComputeNodePanelRootActionProps & ComputeNodePanelRootDataProps & WithStyles<CssRules>;
+type ComputeNodePanelRootProps = ComputeNodePanelRootActionProps & ComputeNodePanelRootDataProps;
 
-export const ComputeNodePanelRoot = withStyles(styles)(
-    ({ classes, hasComputeNodes, computeNodes, openRowOptions }: ComputeNodePanelRootProps) =>
-        <Card className={classes.root}>
-            <CardContent>
-                {hasComputeNodes && <Grid container direction="row">
-                    <Grid item xs={12}>
-                        <Table>
-                            <TableHead>
-                                <TableRow className={classes.tableRow}>
-                                    <TableCell>Info</TableCell>
-                                    <TableCell>UUID</TableCell>
-                                    <TableCell>Domain</TableCell>
-                                    <TableCell>First ping at</TableCell>
-                                    <TableCell>Hostname</TableCell>
-                                    <TableCell>IP Address</TableCell>
-                                    <TableCell>Job</TableCell>
-                                    <TableCell>Last ping at</TableCell>
-                                    <TableCell />
-                                </TableRow>
-                            </TableHead>
-                            <TableBody>
-                                {computeNodes.map((computeNode, index) =>
-                                    <TableRow key={index} className={classes.tableRow}>
-                                        <TableCell>{JSON.stringify(computeNode.info, null, 4)}</TableCell>
-                                        <TableCell>{computeNode.uuid}</TableCell>
-                                        <TableCell>{computeNode.domain}</TableCell>
-                                        <TableCell>{formatDate(computeNode.firstPingAt) || '(none)'}</TableCell>
-                                        <TableCell>{computeNode.hostname || '(none)'}</TableCell>
-                                        <TableCell>{computeNode.ipAddress || '(none)'}</TableCell>
-                                        <TableCell>{computeNode.jobUuid || '(none)'}</TableCell>
-                                        <TableCell>{formatDate(computeNode.lastPingAt) || '(none)'}</TableCell>
-                                        <TableCell>
-                                            <Tooltip title="More options" disableFocusListener>
-                                                <IconButton onClick={event => openRowOptions(event, computeNode)}>
-                                                    <MoreOptionsIcon />
-                                                </IconButton>
-                                            </Tooltip>
-                                        </TableCell>
-                                    </TableRow>)}
-                            </TableBody>
-                        </Table>
-                    </Grid>
-                </Grid>}
-            </CardContent>
-        </Card>
-);
\ No newline at end of file
+export const ComputeNodePanelRoot = (props: ComputeNodePanelRootProps) => {
+    return <DataExplorer
+        id={COMPUTE_NODE_PANEL_ID}
+        onRowClick={props.onItemClick}
+        onRowDoubleClick={props.onItemDoubleClick}
+        onContextMenu={props.onContextMenu}
+        contextMenuColumn={true}
+        hideColumnSelector
+        hideSearchInput
+        dataTableDefaultView={
+            <DataTableDefaultView
+                icon={ShareMeIcon}
+                messages={[DEFAULT_MESSAGE]} />
+        } />;
+};
\ No newline at end of file
index a4f22c808959cae960d606910411f1d540c7a11f..a531b2d0cb66320b35b1b20200dea4a33e2bdb56 100644 (file)
@@ -5,7 +5,6 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { } from '~/store/compute-nodes/compute-nodes-actions';
 import {
     ComputeNodePanelRoot,
     ComputeNodePanelRootDataProps,
@@ -15,15 +14,16 @@ import { openComputeNodeContextMenu } from '~/store/context-menu/context-menu-ac
 
 const mapStateToProps = (state: RootState): ComputeNodePanelRootDataProps => {
     return {
-        computeNodes: state.computeNodes,
-        hasComputeNodes: state.computeNodes.length > 0
+        resources: state.resources
     };
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ComputeNodePanelRootActionProps => ({
-    openRowOptions: (event, computeNode) => {
-        dispatch<any>(openComputeNodeContextMenu(event, computeNode));
-    }
+    onContextMenu: (event, resourceUuid) => {
+        dispatch<any>(openComputeNodeContextMenu(event, resourceUuid));
+    },
+    onItemClick: (resourceUuid: string) => { return; },
+    onItemDoubleClick: uuid => { return; }
 });
 
 export const ComputeNodePanel = connect(mapStateToProps, mapDispatchToProps)(ComputeNodePanelRoot);
\ No newline at end of file
index d5ba79b335a1bde9c19f8e7cf28f6047eee223b2..a4c8e010bcb32d0a9617554ff87e965e76ee2e78 100644 (file)
@@ -63,19 +63,19 @@ export const linkPanelColumns: DataColumns<string> = [
     }
 ];
 
-export interface LinkPanelDataProps {
+export interface LinkPanelRootDataProps {
     resources: ResourcesState;
 }
 
-export interface LinkPanelActionProps {
+export interface LinkPanelRootActionProps {
     onItemClick: (item: string) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
     onItemDoubleClick: (item: string) => void;
 }
 
-export type LinkPanelProps = LinkPanelDataProps & LinkPanelActionProps;
+export type LinkPanelRootProps = LinkPanelRootDataProps & LinkPanelRootActionProps;
 
-export const LinkPanelRoot = (props: LinkPanelProps) => {
+export const LinkPanelRoot = (props: LinkPanelRootProps) => {
     return <DataExplorer
         id={LINK_PANEL_ID}
         onRowClick={props.onItemClick}
index 2c3bf758850cc30412ba10c42a5dc854f6df7f06..4bff4ee7c6be609cba72092cb0a2d43c153e5685 100644 (file)
@@ -6,16 +6,16 @@ import { Dispatch } from "redux";
 import { connect } from "react-redux";
 import { RootState } from '~/store/store';
 import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
-import { LinkPanelRoot, LinkPanelActionProps } from '~/views/link-panel/link-panel-root';
+import { LinkPanelRoot, LinkPanelRootActionProps, LinkPanelRootDataProps } from '~/views/link-panel/link-panel-root';
 import { ResourceKind } from '~/models/resource';
 
-const mapStateToProps = (state: RootState) => {
+const mapStateToProps = (state: RootState): LinkPanelRootDataProps => {
     return {
         resources: state.resources
     };
 };
 
-const mapDispatchToProps = (dispatch: Dispatch): LinkPanelActionProps => ({
+const mapDispatchToProps = (dispatch: Dispatch): LinkPanelRootActionProps => ({
     onContextMenu: (event, resourceUuid) => {
         const kind = resourceKindToContextMenuKind(resourceUuid);
         if (kind) {
index fe93ef85bc098027d7d94dc7dc129d66510e8821..18f5561db37853a228893e829ccf4082aaf873ef 100644 (file)
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { StyleRulesCallback, withStyles, Grid, Button, WithStyles, List, ListItem, ListItemText, ListItemIcon, Tabs, Tab } from '@material-ui/core';
+import { StyleRulesCallback, withStyles, Grid, Button, WithStyles, List, ListItem, ListItemText, ListItemIcon } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { WorkflowResource } from '~/models/workflow';
 import { WorkflowIcon } from '~/components/icon/icon';
-import { WorkflowDetailsCard } from '../workflow-panel/workflow-description-card';
+import { WorkflowDetailsCard } from '~/views/workflow-panel/workflow-description-card';
 import { SearchInput } from '~/components/search-input/search-input';
 
 type CssRules = 'root' | 'searchGrid' | 'workflowDetailsGrid' | 'list' | 'listItem' | 'itemSelected' | 'listItemText' | 'listItemIcon';
index 02408b06bb7b6fcb03b76760ccfebbb6055f28eb..936c3485746b001b3fa4d3a52e8f493e209677e8 100644 (file)
@@ -22,7 +22,7 @@ import { DataTableDefaultView } from '~/components/data-table-default-view/data-
 import { WorkflowResource, parseWorkflowDefinition, getWorkflowInputs, getInputLabel, stringifyInputType } from '~/models/workflow';
 import { WorkflowGraph } from "~/views/workflow-panel/workflow-graph";
 
-export type CssRules = 'root' | 'tab' | 'inputTab' | 'graphTab' | 'descriptionTab' | 'inputsTable';
+export type CssRules = 'root' | 'tab' | 'inputTab' | 'graphTab' | 'graphTabWithChosenWorkflow' | 'descriptionTab' | 'inputsTable';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -34,12 +34,12 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     inputTab: {
         overflow: 'auto',
         maxHeight: '300px',
-        marginTop: theme.spacing.unit,
-        '&:last-child': {
-            paddingBottom: theme.spacing.unit / 2,
-        }
+        marginTop: theme.spacing.unit
     },
     graphTab: {
+        marginTop: theme.spacing.unit,
+    },
+    graphTabWithChosenWorkflow: {
         overflow: 'auto',
         height: '450px',
         marginTop: theme.spacing.unit,
@@ -99,7 +99,7 @@ export const WorkflowDetailsCard = withStyles(styles)(
                             messages={['Please select a workflow to see its inputs.']} />
                     }
                 </CardContent>}
-                {value === 2 && <CardContent className={classes.graphTab}>
+                {value === 2 && <CardContent className={workflow ? classes.graphTabWithChosenWorkflow : classes.graphTab}>
                     {workflow
                         ? <WorkflowGraph workflow={workflow} />
                         : <DataTableDefaultView