15672: Adds minimal subprocess data explorer (WIP)
authorEric Biagiotti <ebiagiotti@veritasgenetics.com>
Fri, 8 Nov 2019 22:50:53 +0000 (17:50 -0500)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 17 Dec 2019 20:44:01 +0000 (17:44 -0300)
The process panel now stores the parent container_request uuid, which is used by the subprocess panel to populate a dataexplorer. The data explorer has minimal functionality. Actions and search still need to be fixed.

Also fixes link account panel formatting and horizontal scroll

Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti@veritasgenetics.com>

13 files changed:
src/store/process-panel/process-panel-actions.ts
src/store/process-panel/process-panel-reducer.ts
src/store/process-panel/process-panel.ts
src/store/processes/processes-actions.ts
src/store/store.ts
src/store/subprocess-panel/subprocess-panel-actions.ts [new file with mode: 0644]
src/store/subprocess-panel/subprocess-panel-middleware-service.ts [new file with mode: 0644]
src/store/workbench/workbench-actions.ts
src/views-components/data-explorer/renderers.tsx
src/views/link-account-panel/link-account-panel-root.tsx
src/views/process-panel/process-panel-root.tsx
src/views/subprocess-panel/subprocess-panel-root.tsx [new file with mode: 0644]
src/views/subprocess-panel/subprocess-panel.tsx [new file with mode: 0644]

index da917f71f70f275aa25e96e3d4861b6b3b9011be..8bdbe4f297dfb363555c2850fd895c42ea3322c9 100644 (file)
@@ -12,20 +12,24 @@ import { navigateTo, navigateToWorkflows } from '~/store/navigation/navigation-a
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { SnackbarKind } from '../snackbar/snackbar-actions';
 import { showWorkflowDetails } from '~/store/workflow-panel/workflow-panel-actions';
+import { loadSubprocessPanel } from "../subprocess-panel/subprocess-panel-actions";
 
-export const procesPanelActions = unionize({
+export const processPanelActions = unionize({
+    SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID: ofType<string>(),
     SET_PROCESS_PANEL_FILTERS: ofType<string[]>(),
     TOGGLE_PROCESS_PANEL_FILTER: ofType<string>(),
 });
 
-export type ProcessPanelAction = UnionOf<typeof procesPanelActions>;
+export type ProcessPanelAction = UnionOf<typeof processPanelActions>;
 
-export const toggleProcessPanelFilter = procesPanelActions.TOGGLE_PROCESS_PANEL_FILTER;
+export const toggleProcessPanelFilter = processPanelActions.TOGGLE_PROCESS_PANEL_FILTER;
 
 export const loadProcessPanel = (uuid: string) =>
     (dispatch: Dispatch) => {
+        dispatch<ProcessPanelAction>(processPanelActions.SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID(uuid));
         dispatch<any>(loadProcess(uuid));
         dispatch(initProcessPanelFilters);
+        dispatch<any>(loadSubprocessPanel());
     };
 
 export const navigateToOutput = (uuid: string) =>
@@ -44,7 +48,7 @@ export const openWorkflow = (uuid: string) =>
         dispatch<any>(showWorkflowDetails(uuid));
     };
 
-export const initProcessPanelFilters = procesPanelActions.SET_PROCESS_PANEL_FILTERS([
+export const initProcessPanelFilters = processPanelActions.SET_PROCESS_PANEL_FILTERS([
     ProcessStatus.QUEUED,
     ProcessStatus.COMPLETED,
     ProcessStatus.FAILED,
index 3e31f56446171aa92dafbfbb58a5c54158f6b753..a4487ded3f6f0e39c33deb62c72bc2395abce629 100644 (file)
@@ -3,22 +3,25 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ProcessPanel } from '~/store/process-panel/process-panel';
-import { ProcessPanelAction, procesPanelActions } from '~/store/process-panel/process-panel-actions';
+import { ProcessPanelAction, processPanelActions } from '~/store/process-panel/process-panel-actions';
 
 const initialState: ProcessPanel = {
+    containerRequestUuid: "",
     filters: {}
 };
 
 export const processPanelReducer = (state = initialState, action: ProcessPanelAction): ProcessPanel =>
-    procesPanelActions.match(action, {
+    processPanelActions.match(action, {
+        SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID: containerRequestUuid => ({
+            ...state, containerRequestUuid
+        }),
         SET_PROCESS_PANEL_FILTERS: statuses => {
             const filters = statuses.reduce((filters, status) => ({ ...filters, [status]: true }), {});
-            return { filters };
+            return { ...state, filters };
         },
         TOGGLE_PROCESS_PANEL_FILTER: status => {
             const filters = { ...state.filters, [status]: !state.filters[status] };
-            return { filters };
+            return { ...state, filters };
         },
         default: () => state,
     });
-    
\ No newline at end of file
index b521a6729f8851f3ac2d7e090d967d5601ea0b2c..935cfa58b4bf6929a1ed6c3e27428fd4b258a79d 100644 (file)
@@ -3,5 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 export interface ProcessPanel {
+    containerRequestUuid: string;
     filters: { [status: string]: boolean };
 }
index a94e56d69f57f75b760178a02390e03cf8a07ad8..d3d715ad91782bce0c55b76ebc7af028edae4360 100644 (file)
@@ -6,8 +6,6 @@ import { Dispatch } from "redux";
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { updateResources } from '~/store/resources/resources-actions';
-import { FilterBuilder } from '~/services/api/filter-builder';
-import { ContainerRequestResource } from '~/models/container-request';
 import { Process } from './process';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
@@ -25,44 +23,18 @@ export const loadProcess = (containerRequestUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
         const response = await services.workflowService.list();
         dispatch(runProcessPanelActions.SET_WORKFLOWS(response.items));
+
         const containerRequest = await services.containerRequestService.get(containerRequestUuid);
         dispatch<any>(updateResources([containerRequest]));
+
         if (containerRequest.containerUuid) {
             const container = await services.containerService.get(containerRequest.containerUuid);
             dispatch<any>(updateResources([container]));
-            await dispatch<any>(loadSubprocesses(containerRequest.containerUuid));
             return { containerRequest, container };
         }
         return { containerRequest };
     };
 
-export const loadSubprocesses = (containerUuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const containerRequests = await dispatch<any>(loadContainerRequests(
-            new FilterBuilder().addEqual('requesting_container_uuid', containerUuid).getFilters()
-        )) as ContainerRequestResource[];
-
-        const containerUuids: string[] = containerRequests.reduce((uuids, { containerUuid }) =>
-            containerUuid
-                ? [...uuids, containerUuid]
-                : uuids, []);
-
-        if (containerUuids.length > 0) {
-            await dispatch<any>(loadContainers(
-                new FilterBuilder().addIn('uuid', containerUuids).getFilters()
-            ));
-        }
-    };
-
-const MAX_AMOUNT_OF_SUBPROCESSES = 10000;
-
-export const loadContainerRequests = (filters: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { items } = await services.containerRequestService.list({ filters, limit: MAX_AMOUNT_OF_SUBPROCESSES });
-        dispatch<any>(updateResources(items));
-        return items;
-    };
-
 export const loadContainers = (filters: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { items } = await services.containerService.list({ filters });
@@ -125,7 +97,7 @@ const getInputs = (data: any) => {
                 default: data.mounts[MOUNT_PATH_CWL_INPUT].content[it.id],
                 doc: it.doc
             }
-    )
+        )
     ) : [];
 };
 
index 76a3b7e4398e3d38a84dfc3623b82bfd9eb8f0ed..83dca37d2ecf176c59dd160fdf1d7827bebb6a26 100644 (file)
@@ -64,6 +64,8 @@ import { linkAccountPanelReducer } from './link-account-panel/link-account-panel
 import { CollectionsWithSameContentAddressMiddlewareService } from '~/store/collections-content-address-panel/collections-content-address-middleware-service';
 import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
 import { ownerNameReducer } from '~/store/owner-name/owner-name-reducer';
+import { SubprocessMiddlewareService } from '~/store/subprocess-panel/subprocess-panel-middleware-service';
+import { SUBPROCESS_PANEL_ID } from '~/store/subprocess-panel/subprocess-panel-actions';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -120,6 +122,9 @@ export function configureStore(history: History, services: ServiceRepository): R
     const collectionsContentAddress = dataExplorerMiddleware(
         new CollectionsWithSameContentAddressMiddlewareService(services, COLLECTIONS_CONTENT_ADDRESS_PANEL_ID)
     );
+    const subprocessMiddleware = dataExplorerMiddleware(
+        new SubprocessMiddlewareService(services, SUBPROCESS_PANEL_ID)
+    );
 
     const middlewares: Middleware[] = [
         routerMiddleware(history),
@@ -138,7 +143,8 @@ export function configureStore(history: History, services: ServiceRepository): R
         computeNodeMiddleware,
         apiClientAuthorizationMiddlewareService,
         publicFavoritesMiddleware,
-        collectionsContentAddress
+        collectionsContentAddress,
+        subprocessMiddleware
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
diff --git a/src/store/subprocess-panel/subprocess-panel-actions.ts b/src/store/subprocess-panel/subprocess-panel-actions.ts
new file mode 100644 (file)
index 0000000..d3a230c
--- /dev/null
@@ -0,0 +1,56 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+/* import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { ProcessResource } from '~/models/process';
+import { getResource } from '~/store/resources/resources';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { REMOVE_PROCESS_DIALOG } from '~/store/processes/processes-actions';
+*/
+export const SUBPROCESS_PANEL_ID = "subprocessPanel";
+export const SUBPROCESS_ATTRIBUTES_DIALOG = 'subprocessAttributesDialog';
+export const subprocessPanelActions = bindDataExplorerActions(SUBPROCESS_PANEL_ID);
+
+export const loadSubprocessPanel = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(subprocessPanelActions.REQUEST_ITEMS());
+    };
+
+/*
+export const openSubprocessAttributesDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const { resources } = getState();
+        const subprocess = getResource<ProcessResource>(uuid)(resources);
+        dispatch(dialogActions.OPEN_DIALOG({ id: SUBPROCESS_ATTRIBUTES_DIALOG, data: { subprocess } }));
+    };
+
+export const openLinkRemoveDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: REMOVE_PROCESS_DIALOG,
+            data: {
+                title: 'Remove process',
+                text: 'Are you sure you want to remove this process?',
+                confirmButtonLabel: 'Remove',
+                uuid
+            }
+        }));
+    };
+
+export const removeSubprocess = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
+        try {
+            await services.containerRequestService.delete(uuid);
+            dispatch(subprocessPanelActions.REQUEST_ITEMS());
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been successfully removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+        } catch (e) {
+            return;
+        }
+    };*/
\ No newline at end of file
diff --git a/src/store/subprocess-panel/subprocess-panel-middleware-service.ts b/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
new file mode 100644 (file)
index 0000000..4655bd0
--- /dev/null
@@ -0,0 +1,105 @@
+// 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 { SortDirection } from '~/components/data-table/data-column';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { ListResults } from '~/services/common-service/common-service';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { ProcessResource } from '~/models/process';
+import { SubprocessPanelColumnNames } from '~/views/subprocess-panel/subprocess-panel-root';
+import { FilterBuilder } from '~/services/api/filter-builder';
+import { subprocessPanelActions } from './subprocess-panel-actions';
+
+export class SubprocessMiddlewareService 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 crUuid = state.processPanel.containerRequestUuid;
+            if (crUuid !== "") {
+                const containerRequest = await this.services.containerRequestService.get(crUuid);
+                if (containerRequest.containerUuid) {
+                    const filters = new FilterBuilder().addEqual('requestingContainerUuid', containerRequest.containerUuid).getFilters();
+                    const containerRequests = await this.services.containerRequestService.list({ ...getParams(dataExplorer), filters });
+                    api.dispatch(updateResources(containerRequests.items));
+                    api.dispatch(setItems(containerRequests));
+
+                    const containerUuids: string[] = containerRequests.items.reduce((uuids, { containerUuid }) =>
+                        containerUuid
+                            ? [...uuids, containerUuid]
+                            : uuids, []);
+
+                    if (containerUuids.length > 0) {
+                        const filters = new FilterBuilder().addIn('uuid', containerUuids).getFilters();
+                        const containers = await this.services.containerService.list({ filters });
+                        api.dispatch<any>(updateResources(containers.items));
+                    }
+                }
+            }
+            // TODO: set filters based on process panel state
+
+        } catch {
+            api.dispatch(couldNotFetchSubprocesses());
+        }
+    }
+}
+
+/*export const getFilters = (processPanel: ProcessPanelState, processes: Process[]) => {
+    const grouppedProcesses = groupBy(processes, getProcessStatus);
+    return Object
+        .keys(processPanel.filters)
+        .map(filter => ({
+            label: filter,
+            value: (grouppedProcesses[filter] || []).length,
+            checked: processPanel.filters[filter],
+            key: filter,
+        }));
+    };
+*/
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    order: getOrder(dataExplorer)
+});
+
+const getOrder = (dataExplorer: DataExplorer) => {
+    const sortColumn = getSortColumn(dataExplorer);
+    const order = new OrderBuilder<ProcessResource>();
+    if (sortColumn) {
+        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+            ? OrderDirection.ASC
+            : OrderDirection.DESC;
+
+        const columnName = sortColumn && sortColumn.name === SubprocessPanelColumnNames.NAME ? "name" : "modifiedAt";
+        return order
+            .addOrder(sortDirection, columnName)
+            .getOrder();
+    } else {
+        return order.getOrder();
+    }
+};
+
+export const setItems = (listResults: ListResults<ProcessResource>) =>
+    subprocessPanelActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const couldNotFetchSubprocesses = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch subprocesses.',
+        kind: SnackbarKind.ERROR
+    });
index 81e84ac52951542b2b5647e538a7ec637a5ac342..9f50402d184cd4dba4a87e7902d390e488b546c7 100644 (file)
@@ -97,6 +97,8 @@ import { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/pub
 import { publicFavoritePanelColumns } from '~/views/public-favorites-panel/public-favorites-panel';
 import { loadCollectionsContentAddressPanel, collectionsContentAddressActions } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
 import { collectionContentAddressPanelColumns } from '~/views/collection-content-address-panel/collection-content-address-panel';
+import { subprocessPanelActions } from '~/store/subprocess-panel/subprocess-panel-actions';
+import { subprocessPanelColumns } from '~/views/subprocess-panel/subprocess-panel-root';
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
@@ -137,6 +139,7 @@ export const loadWorkbench = () =>
             dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
             dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
+            dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
 
             if (services.linkAccountService.getAccountToLink()) {
                 dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
index 8d2713f5689012c273cb10e4f337e08965a05fa4..47ec76c892b9a25ca68e97f609ba7ce1c5122b22 100644 (file)
@@ -445,7 +445,6 @@ export const ProcessStatus = compose(
         const status = props.process ? getProcessStatus(props.process) : "-";
         return <Typography
             noWrap
-            align="center"
             style={{ color: getProcessStatusColor(status, props.theme) }} >
             {status}
         </Typography>;
index 98d19acedf24bb9c303151d3436ba00053b500dc..2438f04e4f2dd1ec74e6cb8435848deae4c6eac2 100644 (file)
@@ -26,7 +26,8 @@ type CssRules = 'root';
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         width: '100%',
-        overflow: 'auto'
+        overflow: 'auto',
+        display: 'flex'
     }
 });
 
@@ -77,7 +78,7 @@ export const LinkAccountPanelRoot = withStyles(styles)(
                 {isProcessing && <Grid container item direction="column" alignContent="center" spacing={24}>
                     <Grid item>
                         Loading user info. Please wait.
-              </Grid>
+                       </Grid>
                     <Grid item style={{ alignSelf: 'center' }}>
                         <CircularProgress />
                     </Grid>
@@ -90,27 +91,27 @@ export const LinkAccountPanelRoot = withStyles(styles)(
                             </Grid>
                             <Grid item>
                                 You can link Arvados accounts. After linking, either login will take you to the same account.
-                      </Grid >
+                                   </Grid >
                         </Grid>
                         <Grid container item direction="row" spacing={24}>
                             <Grid item>
                                 <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
                                     Add another login to this account
-                          </Button>
+                                           </Button>
                             </Grid>
                             <Grid item>
                                 <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
                                     Use this login to access another account
-                          </Button>
+                                           </Button>
                             </Grid>
                         </Grid>
                         {hasRemoteHosts && selectedCluster && <Grid container item direction="column" spacing={24}>
                             <Grid item>
                                 You can also link {displayUser(targetUser, false)} with an account from a remote cluster.
-                      </Grid>
+                                   </Grid>
                             <Grid item>
                                 Please select the cluster that hosts the account you want to link with:
-                           <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
+                                <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
                                     onChange={(event) => setSelectedCluster(event.target.value)}>
                                     {Object.keys(remoteHostsConfig).map((k) => k !== localCluster ? <option key={k} value={k}>{k}</option> : null)}
                                 </Select>
@@ -132,24 +133,24 @@ export const LinkAccountPanelRoot = withStyles(styles)(
                                         <> <Grid item>
                                             This a remote account. You can link a local Arvados account to this one.
                                             After linking, you can access the local account's data by logging into the
-                                           <b>{localCluster}</b> cluster as user <b>{targetUser.email}</b>
+                                                               <b>{localCluster}</b> cluster as user <b>{targetUser.email}</b>
                                             from <b>{targetUser.uuid.substr(0, 5)}</b>.
-                                       </Grid >
-                                        <Grid item>
-                                            <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>
-                                                Link an account from {localCluster} to this account
-                                           </Button>
-                                        </Grid> </>
-                                   : <Grid item>Please visit cluster
-                                      <a href={remoteHostsConfig[loginCluster].workbench2Url + "/link_account"}>{loginCluster}</a>
-                                       to perform account linking.</Grid>
+                                                           </Grid >
+                                            <Grid item>
+                                                <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>
+                                                    Link an account from {localCluster} to this account
+                                                               </Button>
+                                            </Grid> </>
+                                        : <Grid item>Please visit cluster
+                                                       <a href={remoteHostsConfig[loginCluster].workbench2Url + "/link_account"}>{loginCluster}</a>
+                                            to perform account linking.</Grid>
                                     )
-                                 : <Grid item>
-                                     This an inactive remote account. An administrator must activate your
-                                     account before you can proceed.  After your accounts is activated,
-                                    you can link a local Arvados account hosted by the <b>{localCluster}</b>
-                                     cluster to this one.
-                                </Grid >}
+                                    : <Grid item>
+                                        This an inactive remote account. An administrator must activate your
+                                        account before you can proceed.  After your accounts is activated,
+                                                   you can link a local Arvados account hosted by the <b>{localCluster}</b>
+                                        cluster to this one.
+                                               </Grid >}
                             </Grid>
                         </Grid>}
                 </div>}
@@ -158,36 +159,36 @@ export const LinkAccountPanelRoot = withStyles(styles)(
                         {status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
                             <Grid item>
                                 Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}.
-                   </Grid>
+                                   </Grid>
                             {(isLocalUser(targetUser.uuid, localCluster)) && <Grid item>
                                 After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}.
-                   </Grid>}
+                                   </Grid>}
                             <Grid item>
                                 Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
-                   </Grid>
+                                   </Grid>
                             {!isLocalUser(targetUser.uuid, localCluster) && <Grid item>
                                 You can access <b>{userToLink.email}</b> data by logging into <b>{localCluster}</b> with the <b>{targetUser.email}</b> account.
-                   </Grid>}
+                                   </Grid>}
                         </Grid>}
                         {error === LinkAccountPanelError.NON_ADMIN && <Grid item>
                             Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(targetUser)}.
-               </Grid>}
+                               </Grid>}
                         {error === LinkAccountPanelError.SAME_USER && <Grid item>
                             Cannot link {displayUser(targetUser)} to the same account.
-               </Grid>}
+                               </Grid>}
                         {error === LinkAccountPanelError.INACTIVE && <Grid item>
                             Cannot link account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
-               </Grid>}
+                               </Grid>}
                         <Grid container item direction="row" spacing={24}>
                             <Grid item>
                                 <Button variant="contained" onClick={() => cancelLinking()}>
                                     Cancel
-                       </Button>
+                                           </Button>
                             </Grid>
                             <Grid item>
                                 <Button disabled={status === LinkAccountPanelStatus.ERROR} color="primary" variant="contained" onClick={() => linkAccount()}>
                                     Link accounts
-                       </Button>
+                                           </Button>
                             </Grid>
                         </Grid>
                     </Grid>}
index 63b6aa4d19205519e7e63b099197fad41d80b34c..d3ceef264255973ed503e31a1a7ee5f48e0523e2 100644 (file)
@@ -9,7 +9,7 @@ import { DefaultView } from '~/components/default-view/default-view';
 import { ProcessIcon } from '~/components/icon/icon';
 import { Process } from '~/store/processes/process';
 import { SubprocessesCard } from './subprocesses-card';
-import { ProcessSubprocesses } from '~/views/process-panel/process-subprocesses';
+import { SubprocessPanel } from '~/views/subprocess-panel/subprocess-panel';
 import { SubprocessFilterDataProps } from '~/components/subprocess-filter/subprocess-filter';
 
 export interface ProcessPanelRootDataProps {
@@ -41,7 +41,7 @@ export const ProcessPanelRoot = ({ process, ...props }: ProcessPanelRootProps) =
                     navigateToOutput={props.navigateToOutput}
                     openWorkflow={props.navigateToWorkflow}
                     cancelProcess={props.cancelProcess}
-            />
+                />
             </Grid>
             <Grid item sm={12} md={5}>
                 <SubprocessesCard
@@ -50,10 +50,8 @@ export const ProcessPanelRoot = ({ process, ...props }: ProcessPanelRootProps) =
                     onToggle={props.onToggle}
                 />
             </Grid>
-            <Grid item xs={12}>
-                <ProcessSubprocesses
-                    subprocesses={props.subprocesses}
-                    onContextMenu={props.onContextMenu} />
+            <Grid item sm={12} md={12}>
+                <SubprocessPanel />
             </Grid>
         </Grid>
         : <Grid container
diff --git a/src/views/subprocess-panel/subprocess-panel-root.tsx b/src/views/subprocess-panel/subprocess-panel-root.tsx
new file mode 100644 (file)
index 0000000..770139e
--- /dev/null
@@ -0,0 +1,74 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { DataColumns } from '~/components/data-table/data-table';
+import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
+import { ContainerRequestState } from '~/models/container-request';
+import { SortDirection } from '~/components/data-table/data-column';
+import { ResourceKind } from '~/models/resource';
+import { ResourceLastModifiedDate, ProcessStatus } from '~/views-components/data-explorer/renderers';
+import { ProcessIcon } from '~/components/icon/icon';
+import { ResourceName } from '~/views-components/data-explorer/renderers';
+import { SUBPROCESS_PANEL_ID } from '~/store/subprocess-panel/subprocess-panel-actions';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { createTree } from '~/models/tree';
+
+export enum SubprocessPanelColumnNames {
+    NAME = "Name",
+    STATUS = "Status",
+    LAST_MODIFIED = "Last modified"
+}
+
+export interface SubprocessPanelFilter extends DataTableFilterItem {
+    type: ResourceKind | ContainerRequestState;
+}
+
+export const subprocessPanelColumns: DataColumns<string> = [
+    {
+        name: SubprocessPanelColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceName uuid={uuid} />
+    },
+    {
+        name: "Status",
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ProcessStatus uuid={uuid} />,
+    },
+    {
+        name: SubprocessPanelColumnNames.LAST_MODIFIED,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.DESC,
+        filters: createTree(),
+        render: uuid => <ResourceLastModifiedDate uuid={uuid} />
+    }
+];
+
+export interface SubprocessActionProps {
+    onItemClick: (item: string) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
+    onItemDoubleClick: (item: string) => void;
+}
+
+export const SubprocessPanelRoot = (props: SubprocessActionProps) => {
+    return <DataExplorer
+        id={SUBPROCESS_PANEL_ID}
+        onRowClick={props.onItemClick}
+        onRowDoubleClick={props.onItemDoubleClick}
+        onContextMenu={props.onContextMenu}
+        contextMenuColumn={true}
+        hideColumnSelector
+        dataTableDefaultView={
+            <DataTableDefaultView
+                icon={ProcessIcon}
+                messages={['This process has no subprocesses.']} />
+        } />;
+};
\ No newline at end of file
diff --git a/src/views/subprocess-panel/subprocess-panel.tsx b/src/views/subprocess-panel/subprocess-panel.tsx
new file mode 100644 (file)
index 0000000..4717f2d
--- /dev/null
@@ -0,0 +1,28 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import { SubprocessPanelRoot, SubprocessActionProps } from '~/views/subprocess-panel/subprocess-panel-root';
+import { ResourceKind } from '~/models/resource';
+
+const mapDispatchToProps = (dispatch: Dispatch): SubprocessActionProps => ({
+    onContextMenu: (event, resourceUuid) => {
+        const kind = resourceKindToContextMenuKind(resourceUuid);
+        if (kind) {
+            dispatch<any>(openContextMenu(event, {
+                name: '',
+                uuid: resourceUuid,
+                ownerUuid: '',
+                kind: ResourceKind.PROCESS,
+                menuKind: kind
+            }));
+        }
+    },
+    onItemClick: (resourceUuid: string) => { return; },
+    onItemDoubleClick: uuid => { return; }
+});
+
+export const SubprocessPanel = connect(mapDispatchToProps)(SubprocessPanelRoot);
\ No newline at end of file