From: Eric Biagiotti Date: Fri, 8 Nov 2019 22:50:53 +0000 (-0500) Subject: 15672: Adds minimal subprocess data explorer (WIP) X-Git-Tag: 2.0.0~15^2~28 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/d9ee24d0f78a21603e2276d0d3142d4db40a831b 15672: Adds minimal subprocess data explorer (WIP) 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 --- diff --git a/src/store/process-panel/process-panel-actions.ts b/src/store/process-panel/process-panel-actions.ts index da917f71..8bdbe4f2 100644 --- a/src/store/process-panel/process-panel-actions.ts +++ b/src/store/process-panel/process-panel-actions.ts @@ -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(), SET_PROCESS_PANEL_FILTERS: ofType(), TOGGLE_PROCESS_PANEL_FILTER: ofType(), }); -export type ProcessPanelAction = UnionOf; +export type ProcessPanelAction = UnionOf; -export const toggleProcessPanelFilter = procesPanelActions.TOGGLE_PROCESS_PANEL_FILTER; +export const toggleProcessPanelFilter = processPanelActions.TOGGLE_PROCESS_PANEL_FILTER; export const loadProcessPanel = (uuid: string) => (dispatch: Dispatch) => { + dispatch(processPanelActions.SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID(uuid)); dispatch(loadProcess(uuid)); dispatch(initProcessPanelFilters); + dispatch(loadSubprocessPanel()); }; export const navigateToOutput = (uuid: string) => @@ -44,7 +48,7 @@ export const openWorkflow = (uuid: string) => dispatch(showWorkflowDetails(uuid)); }; -export const initProcessPanelFilters = procesPanelActions.SET_PROCESS_PANEL_FILTERS([ +export const initProcessPanelFilters = processPanelActions.SET_PROCESS_PANEL_FILTERS([ ProcessStatus.QUEUED, ProcessStatus.COMPLETED, ProcessStatus.FAILED, diff --git a/src/store/process-panel/process-panel-reducer.ts b/src/store/process-panel/process-panel-reducer.ts index 3e31f564..a4487ded 100644 --- a/src/store/process-panel/process-panel-reducer.ts +++ b/src/store/process-panel/process-panel-reducer.ts @@ -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 diff --git a/src/store/process-panel/process-panel.ts b/src/store/process-panel/process-panel.ts index b521a672..935cfa58 100644 --- a/src/store/process-panel/process-panel.ts +++ b/src/store/process-panel/process-panel.ts @@ -3,5 +3,6 @@ // SPDX-License-Identifier: AGPL-3.0 export interface ProcessPanel { + containerRequestUuid: string; filters: { [status: string]: boolean }; } diff --git a/src/store/processes/processes-actions.ts b/src/store/processes/processes-actions.ts index a94e56d6..d3d715ad 100644 --- a/src/store/processes/processes-actions.ts +++ b/src/store/processes/processes-actions.ts @@ -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 => { const response = await services.workflowService.list(); dispatch(runProcessPanelActions.SET_WORKFLOWS(response.items)); + const containerRequest = await services.containerRequestService.get(containerRequestUuid); dispatch(updateResources([containerRequest])); + if (containerRequest.containerUuid) { const container = await services.containerService.get(containerRequest.containerUuid); dispatch(updateResources([container])); - await dispatch(loadSubprocesses(containerRequest.containerUuid)); return { containerRequest, container }; } return { containerRequest }; }; -export const loadSubprocesses = (containerUuid: string) => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const containerRequests = await dispatch(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(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(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 } - ) + ) ) : []; }; diff --git a/src/store/store.ts b/src/store/store.ts index 76a3b7e4..83dca37d 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -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 index 00000000..d3a230cb --- /dev/null +++ b/src/store/subprocess-panel/subprocess-panel-actions.ts @@ -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(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 index 00000000..4655bd02 --- /dev/null +++ b/src/store/subprocess-panel/subprocess-panel-middleware-service.ts @@ -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) { + 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(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(); + 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) => + 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 + }); diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index 81e84ac5..9f50402d 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -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()); diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 8d2713f5..47ec76c8 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -445,7 +445,6 @@ export const ProcessStatus = compose( const status = props.process ? getProcessStatus(props.process) : "-"; return {status} ; diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx index 98d19ace..2438f04e 100644 --- a/src/views/link-account-panel/link-account-panel-root.tsx +++ b/src/views/link-account-panel/link-account-panel-root.tsx @@ -26,7 +26,8 @@ type CssRules = 'root'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { width: '100%', - overflow: 'auto' + overflow: 'auto', + display: 'flex' } }); @@ -77,7 +78,7 @@ export const LinkAccountPanelRoot = withStyles(styles)( {isProcessing && Loading user info. Please wait. - + @@ -90,27 +91,27 @@ export const LinkAccountPanelRoot = withStyles(styles)( You can link Arvados accounts. After linking, either login will take you to the same account. - + + + {hasRemoteHosts && selectedCluster && You can also link {displayUser(targetUser, false)} with an account from a remote cluster. - + Please select the cluster that hosts the account you want to link with: - @@ -132,24 +133,24 @@ export const LinkAccountPanelRoot = withStyles(styles)( <> 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 - {localCluster} cluster as user {targetUser.email} + {localCluster} cluster as user {targetUser.email} from {targetUser.uuid.substr(0, 5)}. - - - - - : Please visit cluster - {loginCluster} - to perform account linking. + + + + + : Please visit cluster + {loginCluster} + to perform account linking. ) - : - 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 {localCluster} - cluster to this one. - } + : + 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 {localCluster} + cluster to this one. + } } } @@ -158,36 +159,36 @@ export const LinkAccountPanelRoot = withStyles(styles)( {status === LinkAccountPanelStatus.LINKING && Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}. - + {(isLocalUser(targetUser.uuid, localCluster)) && After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}. - } + } Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}. - + {!isLocalUser(targetUser.uuid, localCluster) && You can access {userToLink.email} data by logging into {localCluster} with the {targetUser.email} account. - } + } } {error === LinkAccountPanelError.NON_ADMIN && Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(targetUser)}. - } + } {error === LinkAccountPanelError.SAME_USER && Cannot link {displayUser(targetUser)} to the same account. - } + } {error === LinkAccountPanelError.INACTIVE && Cannot link account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}. - } + } + + } diff --git a/src/views/process-panel/process-panel-root.tsx b/src/views/process-panel/process-panel-root.tsx index 63b6aa4d..d3ceef26 100644 --- a/src/views/process-panel/process-panel-root.tsx +++ b/src/views/process-panel/process-panel-root.tsx @@ -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} - /> + /> - - + + : = [ + { + name: SubprocessPanelColumnNames.NAME, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: "Status", + selected: true, + configurable: true, + filters: createTree(), + render: uuid => , + }, + { + name: SubprocessPanelColumnNames.LAST_MODIFIED, + selected: true, + configurable: true, + sortDirection: SortDirection.DESC, + filters: createTree(), + render: uuid => + } +]; + +export interface SubprocessActionProps { + onItemClick: (item: string) => void; + onContextMenu: (event: React.MouseEvent, item: string) => void; + onItemDoubleClick: (item: string) => void; +} + +export const SubprocessPanelRoot = (props: SubprocessActionProps) => { + return + } />; +}; \ 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 index 00000000..4717f2d7 --- /dev/null +++ b/src/views/subprocess-panel/subprocess-panel.tsx @@ -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(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