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>
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) =>
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,
// 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
// SPDX-License-Identifier: AGPL-3.0
export interface ProcessPanel {
+ containerRequestUuid: string;
filters: { [status: string]: boolean };
}
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';
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 });
default: data.mounts[MOUNT_PATH_CWL_INPUT].content[it.id],
doc: it.doc
}
- )
+ )
) : [];
};
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' &&
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),
computeNodeMiddleware,
apiClientAuthorizationMiddlewareService,
publicFavoritesMiddleware,
- collectionsContentAddress
+ collectionsContentAddress,
+ subprocessMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { 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
--- /dev/null
+// 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
+ });
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';
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());
const status = props.process ? getProcessStatus(props.process) : "-";
return <Typography
noWrap
- align="center"
style={{ color: getProcessStatusColor(status, props.theme) }} >
{status}
</Typography>;
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
width: '100%',
- overflow: 'auto'
+ overflow: 'auto',
+ display: 'flex'
}
});
{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>
</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>
<> <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>}
{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>}
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 {
navigateToOutput={props.navigateToOutput}
openWorkflow={props.navigateToWorkflow}
cancelProcess={props.cancelProcess}
- />
+ />
</Grid>
<Grid item sm={12} md={5}>
<SubprocessesCard
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
--- /dev/null
+// 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
--- /dev/null
+// 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