[arvados] created: 2.7.0-5936-g1efba8f3b7
git repository hosting
git at public.arvados.org
Mon Jan 29 15:57:43 UTC 2024
at 1efba8f3b728a3b8aa3c64c5aa09f441318ff2a8 (commit)
commit 1efba8f3b728a3b8aa3c64c5aa09f441318ff2a8
Merge: 67068b56fc 3b735dd933
Author: Peter Amstutz <peter.amstutz at curii.com>
Date: Mon Jan 29 10:57:08 2024 -0500
Merge commit '3b735dd9330e0989f51a76771c3303031154154e' into 21158-wf-page-list
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>
diff --cc services/workbench2/src/store/processes/processes-middleware-service.ts
index 0000000000,3154e1aec9..3154e1aec9
mode 000000,100644..100644
--- a/services/workbench2/src/store/processes/processes-middleware-service.ts
+++ b/services/workbench2/src/store/processes/processes-middleware-service.ts
diff --cc services/workbench2/src/store/workbench/workbench-actions.ts
index ed05c0b172,0000000000..0d324206f8
mode 100644,000000..100644
--- a/services/workbench2/src/store/workbench/workbench-actions.ts
+++ b/services/workbench2/src/store/workbench/workbench-actions.ts
@@@ -1,880 -1,0 +1,885 @@@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from "store/store";
+import { getUserUuid } from "common/getuser";
+import { loadDetailsPanel } from "store/details-panel/details-panel-action";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { favoritePanelActions, loadFavoritePanel } from "store/favorite-panel/favorite-panel-action";
+import { getProjectPanelCurrentUuid, setIsProjectPanelTrashed } from "store/project-panel/project-panel-action";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
+import {
+ activateSidePanelTreeItem,
+ initSidePanelTree,
+ loadSidePanelTreeProjects,
+ SidePanelTreeCategory,
- SIDE_PANEL_TREE,
++ SIDE_PANEL_TREE,
+} from "store/side-panel-tree/side-panel-tree-actions";
+import { updateResources } from "store/resources/resources-actions";
+import { projectPanelColumns } from "views/project-panel/project-panel";
+import { favoritePanelColumns } from "views/favorite-panel/favorite-panel";
+import { matchRootRoute } from "routes/routes";
+import {
+ setGroupDetailsBreadcrumbs,
+ setGroupsBreadcrumbs,
+ setProcessBreadcrumbs,
+ setSharedWithMeBreadcrumbs,
+ setSidePanelBreadcrumbs,
+ setTrashBreadcrumbs,
+ setUsersBreadcrumbs,
+ setMyAccountBreadcrumbs,
+ setUserProfileBreadcrumbs,
+ setInstanceTypesBreadcrumbs,
+ setVirtualMachinesBreadcrumbs,
+ setVirtualMachinesAdminBreadcrumbs,
+ setRepositoriesBreadcrumbs,
+} from "store/breadcrumbs/breadcrumbs-actions";
+import { navigateTo, navigateToRootProject } from "store/navigation/navigation-action";
+import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
+import { ServiceRepository } from "services/services";
+import { getResource } from "store/resources/resources";
+import * as projectCreateActions from "store/projects/project-create-actions";
+import * as projectMoveActions from "store/projects/project-move-actions";
+import * as projectUpdateActions from "store/projects/project-update-actions";
+import * as collectionCreateActions from "store/collections/collection-create-actions";
+import * as collectionCopyActions from "store/collections/collection-copy-actions";
+import * as collectionMoveActions from "store/collections/collection-move-actions";
+import * as processesActions from "store/processes/processes-actions";
+import * as processMoveActions from "store/processes/process-move-actions";
+import * as processUpdateActions from "store/processes/process-update-actions";
+import * as processCopyActions from "store/processes/process-copy-actions";
+import { trashPanelColumns } from "views/trash-panel/trash-panel";
+import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
+import { loadProcessPanel } from "store/process-panel/process-panel-actions";
+import { loadSharedWithMePanel, sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with-me-panel-actions";
+import { sharedWithMePanelColumns } from "views/shared-with-me-panel/shared-with-me-panel";
+import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
+import { workflowPanelActions } from "store/workflow-panel/workflow-panel-actions";
+import { loadSshKeysPanel } from "store/auth/auth-action-ssh";
+import { loadLinkAccountPanel, linkAccountPanelActions } from "store/link-account-panel/link-account-panel-actions";
+import { loadSiteManagerPanel } from "store/auth/auth-action-session";
+import { workflowPanelColumns } from "views/workflow-panel/workflow-panel-view";
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { getProgressIndicator } from "store/progress-indicator/progress-indicator-reducer";
+import { extractUuidKind, Resource, ResourceKind } from "models/resource";
+import { FilterBuilder } from "services/api/filter-builder";
+import { GroupContentsResource } from "services/groups-service/groups-service";
+import { MatchCases, ofType, unionize, UnionOf } from "common/unionize";
+import { loadRunProcessPanel } from "store/run-process-panel/run-process-panel-actions";
+import { collectionPanelActions, loadCollectionPanel } from "store/collection-panel/collection-panel-action";
+import { CollectionResource } from "models/collection";
+import { WorkflowResource } from "models/workflow";
+import { loadSearchResultsPanel, searchResultsPanelActions } from "store/search-results-panel/search-results-panel-actions";
+import { searchResultsPanelColumns } from "views/search-results-panel/search-results-panel-view";
+import { loadVirtualMachinesPanel } from "store/virtual-machines/virtual-machines-actions";
+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 * as userProfilePanelActions from "store/user-profile/user-profile-actions";
+import { linkPanelActions, loadLinkPanel } from "store/link-panel/link-panel-actions";
+import { linkPanelColumns } from "views/link-panel/link-panel-root";
+import { userPanelColumns } from "views/user-panel/user-panel";
+import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from "store/api-client-authorizations/api-client-authorizations-actions";
+import { apiClientAuthorizationPanelColumns } from "views/api-client-authorization-panel/api-client-authorization-panel-root";
+import * as groupPanelActions from "store/groups-panel/groups-panel-actions";
+import { groupsPanelColumns } from "views/groups-panel/groups-panel";
+import * as groupDetailsPanelActions from "store/group-details-panel/group-details-panel-actions";
+import { groupDetailsMembersPanelColumns, groupDetailsPermissionsPanelColumns } from "views/group-details-panel/group-details-panel";
+import { DataTableFetchMode } from "components/data-table/data-table";
+import { loadPublicFavoritePanel, publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
+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";
+import { loadAllProcessesPanel, allProcessesPanelActions } from "../all-processes-panel/all-processes-panel-action";
+import { allProcessesPanelColumns } from "views/all-processes-panel/all-processes-panel";
+import { userProfileGroupsColumns } from "views/user-profile-panel/user-profile-panel-root";
+import { selectedToArray, selectedToKindSet } from "components/multiselect-toolbar/MultiselectToolbar";
+import { deselectOne } from "store/multiselect/multiselect-actions";
+import { treePickerActions } from "store/tree-picker/tree-picker-actions";
++import { multiselectActions } from "store/multiselect/multiselect-actions";
++import { workflowProcessesPanelColumns } from "views/workflow-panel/workflow-processes-panel-root";
++import { workflowProcessesPanelActions } from "store/workflow-panel/workflow-panel-actions";
+
+export const WORKBENCH_LOADING_SCREEN = "workbenchLoadingScreen";
+
+export const isWorkbenchLoading = (state: RootState) => {
+ const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
+ return progress ? progress.working : false;
+};
+
+export const handleFirstTimeLoad = (action: any) => async (dispatch: Dispatch<any>, getState: () => RootState) => {
+ try {
+ await dispatch(action);
+ } catch (e) {
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Error " + e,
+ hideDuration: 8000,
+ kind: SnackbarKind.WARNING,
+ })
+ } finally {
+ if (isWorkbenchLoading(getState())) {
+ dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+ }
+ }
+};
+
+export const loadWorkbench = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
+ const { auth, router } = getState();
+ const { user } = auth;
+ if (user) {
+ dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+ dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
+ dispatch(
+ allProcessesPanelActions.SET_COLUMNS({
+ columns: allProcessesPanelColumns,
+ })
+ );
+ dispatch(
+ publicFavoritePanelActions.SET_COLUMNS({
+ columns: publicFavoritePanelColumns,
+ })
+ );
+ dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
+ dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: sharedWithMePanelColumns }));
+ dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
+ dispatch(
+ searchResultsPanelActions.SET_FETCH_MODE({
+ fetchMode: DataTableFetchMode.INFINITE,
+ })
+ );
+ dispatch(
+ searchResultsPanelActions.SET_COLUMNS({
+ columns: searchResultsPanelColumns,
+ })
+ );
+ dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
+ dispatch(
+ groupPanelActions.GroupsPanelActions.SET_COLUMNS({
+ columns: groupsPanelColumns,
+ })
+ );
+ dispatch(
+ groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({
+ columns: groupDetailsMembersPanelColumns,
+ })
+ );
+ dispatch(
+ groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({
+ columns: groupDetailsPermissionsPanelColumns,
+ })
+ );
+ dispatch(
+ userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({
+ columns: userProfileGroupsColumns,
+ })
+ );
+ dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
+ dispatch(
+ apiClientAuthorizationsActions.SET_COLUMNS({
+ columns: apiClientAuthorizationPanelColumns,
+ })
+ );
+ dispatch(
+ collectionsContentAddressActions.SET_COLUMNS({
+ columns: collectionContentAddressPanelColumns,
+ })
+ );
+ dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
++ dispatch(workflowProcessesPanelActions.SET_COLUMNS({ columns: workflowProcessesPanelColumns }));
+
+ if (services.linkAccountService.getAccountToLink()) {
+ dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
+ }
+
+ dispatch<any>(initSidePanelTree());
+ if (router.location) {
+ const match = matchRootRoute(router.location.pathname);
+ if (match) {
+ dispatch<any>(navigateToRootProject);
+ }
+ }
+ } else {
+ dispatch(userIsNotAuthenticated);
+ }
+};
+
+export const loadFavorites = () =>
+ handleFirstTimeLoad((dispatch: Dispatch) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
+ dispatch<any>(loadFavoritePanel());
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
+ });
+
+export const loadCollectionContentAddress = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadCollectionsContentAddressPanel());
+});
+
+export const loadTrash = () =>
+ handleFirstTimeLoad((dispatch: Dispatch) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
+ dispatch<any>(loadTrashPanel());
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
+ });
+
+export const loadAllProcesses = () =>
+ handleFirstTimeLoad((dispatch: Dispatch) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.ALL_PROCESSES));
+ dispatch<any>(loadAllProcessesPanel());
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.ALL_PROCESSES));
+ });
+
+export const loadProject = (uuid: string) =>
+ handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = getUserUuid(getState());
+ dispatch(setIsProjectPanelTrashed(false));
+ if (!userUuid) {
+ return;
+ }
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid));
+ if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
+ // Load another users home projects
+ dispatch(finishLoadingProject(uuid));
+ } else if (userUuid !== uuid) {
+ await dispatch(finishLoadingProject(uuid));
+ const match = await loadGroupContentsResource({
+ uuid,
+ userUuid,
+ services,
+ });
+ match({
+ OWNED: async () => {
+ await dispatch(activateSidePanelTreeItem(uuid));
+ dispatch<any>(setSidePanelBreadcrumbs(uuid));
+ },
+ SHARED: async () => {
+ await dispatch(activateSidePanelTreeItem(uuid));
+ dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
+ },
+ TRASHED: async () => {
+ await dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
+ dispatch<any>(setTrashBreadcrumbs(uuid));
+ dispatch(setIsProjectPanelTrashed(true));
+ },
+ });
+ } else {
+ await dispatch(finishLoadingProject(userUuid));
+ await dispatch(activateSidePanelTreeItem(userUuid));
+ dispatch<any>(setSidePanelBreadcrumbs(userUuid));
+ }
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid));
+ }
+ });
+
+export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) => async (dispatch: Dispatch) => {
+ const newProject = await dispatch<any>(projectCreateActions.createProject(data));
+ if (newProject) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been successfully created.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
+ dispatch<any>(navigateTo(newProject.uuid));
+ }
+};
+
+export const moveProject =
+ (data: MoveToFormDialogData, isSecondaryMove = false) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const checkedList = getState().multiselect.checkedList;
+ const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist default to normal context menu behavior
+ if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
+
+ const sourceUuid = getResource(data.uuid)(getState().resources)?.ownerUuid;
+ const destinationUuid = data.ownerUuid;
+
+ const projectsToMove: MoveableResource[] = uuidsToMove
+ .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
+ .filter(resource => resource.kind === ResourceKind.PROJECT);
+
+ for (const project of projectsToMove) {
+ await moveSingleProject(project);
+ }
+
+ //omly propagate if this call is the original
+ if (!isSecondaryMove) {
+ const kindsToMove: Set<string> = selectedToKindSet(checkedList);
+ kindsToMove.delete(ResourceKind.PROJECT);
+
+ kindsToMove.forEach(kind => {
+ secondaryMove[kind](data, true)(dispatch, getState, services);
+ });
+ }
+
+ async function moveSingleProject(project: MoveableResource) {
+ try {
+ const oldProject: MoveToFormDialogData = { name: project.name, uuid: project.uuid, ownerUuid: data.ownerUuid };
+ const oldOwnerUuid = oldProject ? oldProject.ownerUuid : "";
+ const movedProject = await dispatch<any>(projectMoveActions.moveProject(oldProject));
+ if (movedProject) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been moved",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ await dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
+ }
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: !!(project as any).frozenByUuid ? 'Could not move frozen project.' : e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+ }
+ if (sourceUuid) await dispatch<any>(loadSidePanelTreeProjects(sourceUuid));
+ await dispatch<any>(loadSidePanelTreeProjects(destinationUuid));
+ };
+
+export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
+ const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
+ if (updatedProject) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been successfully updated.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
+ dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
+ }
+};
+
+export const updateGroup = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
+ const updatedGroup = await dispatch<any>(groupPanelActions.updateGroup(data));
+ if (updatedGroup) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Group has been successfully updated.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
+ dispatch<any>(reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid]));
+ }
+};
+
+export const loadCollection = (uuid: string) =>
+ handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = getUserUuid(getState());
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid));
+ if (userUuid) {
+ const match = await loadGroupContentsResource({
+ uuid,
+ userUuid,
+ services,
+ });
+ let collection: CollectionResource | undefined;
+ let breadcrumbfunc:
+ | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
+ | undefined;
+ let sidepanel: string | undefined;
+ match({
+ OWNED: thecollection => {
+ collection = thecollection as CollectionResource;
+ sidepanel = collection.ownerUuid;
+ breadcrumbfunc = setSidePanelBreadcrumbs;
+ },
+ SHARED: thecollection => {
+ collection = thecollection as CollectionResource;
+ sidepanel = collection.ownerUuid;
+ breadcrumbfunc = setSharedWithMeBreadcrumbs;
+ },
+ TRASHED: thecollection => {
+ collection = thecollection as CollectionResource;
+ sidepanel = SidePanelTreeCategory.TRASH;
+ breadcrumbfunc = () => setTrashBreadcrumbs("");
+ },
+ });
+ if (collection && breadcrumbfunc && sidepanel) {
+ dispatch(updateResources([collection]));
+ await dispatch<any>(finishLoadingProject(collection.ownerUuid));
+ dispatch(collectionPanelActions.SET_COLLECTION(collection));
+ await dispatch(activateSidePanelTreeItem(sidepanel));
+ dispatch(breadcrumbfunc(collection.ownerUuid));
+ dispatch(loadCollectionPanel(collection.uuid));
+ }
+ }
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid));
+ }
+ });
+
+export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) => async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
+ if (collection) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully created.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(navigateTo(collection.uuid));
+ }
+};
+
+export const copyCollection = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const checkedList = getState().multiselect.checkedList;
+ const uuidsToCopy: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist && no items passed in, default to normal context menu behavior
+ if (!uuidsToCopy.length) uuidsToCopy.push(data.uuid);
+
+ const collectionsToCopy: CollectionCopyResource[] = uuidsToCopy
+ .map(uuid => getResource(uuid)(getState().resources) as CollectionCopyResource)
+ .filter(resource => resource.kind === ResourceKind.COLLECTION);
+
+ for (const collection of collectionsToCopy) {
+ await copySingleCollection({ ...collection, ownerUuid: data.ownerUuid } as CollectionCopyResource);
+ }
+
+ async function copySingleCollection(copyToProject: CollectionCopyResource) {
+ const newName = data.fromContextMenu || collectionsToCopy.length === 1 ? data.name : `Copy of: ${copyToProject.name}`;
+ try {
+ const collection = await dispatch<any>(
+ collectionCopyActions.copyCollection({
+ ...copyToProject,
+ name: newName,
+ fromContextMenu: collectionsToCopy.length === 1 ? true : data.fromContextMenu,
+ })
+ );
+ if (copyToProject && collection) {
+ await dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been copied.",
+ hideDuration: 3000,
+ kind: SnackbarKind.SUCCESS,
+ link: collection.ownerUuid,
+ })
+ );
+ dispatch<any>(deselectOne(copyToProject.uuid));
+ }
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+ }
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+};
+
+export const moveCollection =
+ (data: MoveToFormDialogData, isSecondaryMove = false) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const checkedList = getState().multiselect.checkedList;
+ const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist && no items passed in, default to normal context menu behavior
+ if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
+
+ const collectionsToMove: MoveableResource[] = uuidsToMove
+ .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
+ .filter(resource => resource.kind === ResourceKind.COLLECTION);
+
+ for (const collection of collectionsToMove) {
+ await moveSingleCollection(collection);
+ }
+
+ //omly propagate if this call is the original
+ if (!isSecondaryMove) {
+ const kindsToMove: Set<string> = selectedToKindSet(checkedList);
+ kindsToMove.delete(ResourceKind.COLLECTION);
+
+ kindsToMove.forEach(kind => {
+ secondaryMove[kind](data, true)(dispatch, getState, services);
+ });
+ }
+
+ async function moveSingleCollection(collection: MoveableResource) {
+ try {
+ const oldCollection: MoveToFormDialogData = { name: collection.name, uuid: collection.uuid, ownerUuid: data.ownerUuid };
+ const movedCollection = await dispatch<any>(collectionMoveActions.moveCollection(oldCollection));
+ dispatch<any>(updateResources([movedCollection]));
+ dispatch<any>(reloadProjectMatchingUuid([movedCollection.ownerUuid]));
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been moved.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+ }
+ };
+
+export const loadProcess = (uuid: string) =>
+ handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState) => {
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid));
+ dispatch<any>(loadProcessPanel(uuid));
+ const process = await dispatch<any>(processesActions.loadProcess(uuid));
+ if (process) {
+ await dispatch<any>(finishLoadingProject(process.containerRequest.ownerUuid));
+ await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
+ dispatch<any>(setProcessBreadcrumbs(uuid));
+ dispatch<any>(loadDetailsPanel(uuid));
+ }
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid));
+ }
+ });
+
+export const loadRegisteredWorkflow = (uuid: string) =>
+ handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = getUserUuid(getState());
+ if (userUuid) {
+ const match = await loadGroupContentsResource({
+ uuid,
+ userUuid,
+ services,
+ });
+ let workflow: WorkflowResource | undefined;
+ let breadcrumbfunc:
+ | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
+ | undefined;
+ match({
+ OWNED: async theworkflow => {
+ workflow = theworkflow as WorkflowResource;
+ breadcrumbfunc = setSidePanelBreadcrumbs;
+ },
+ SHARED: async theworkflow => {
+ workflow = theworkflow as WorkflowResource;
+ breadcrumbfunc = setSharedWithMeBreadcrumbs;
+ },
+ TRASHED: () => { },
+ });
+ if (workflow && breadcrumbfunc) {
+ dispatch(updateResources([workflow]));
+ await dispatch<any>(finishLoadingProject(workflow.ownerUuid));
+ await dispatch<any>(activateSidePanelTreeItem(workflow.ownerUuid));
+ dispatch<any>(breadcrumbfunc(workflow.ownerUuid));
++ dispatch(workflowProcessesPanelActions.REQUEST_ITEMS());
+ }
+ }
+ });
+
+export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) => async (dispatch: Dispatch) => {
+ try {
+ const process = await dispatch<any>(processUpdateActions.updateProcess(data));
+ if (process) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Process has been successfully updated.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ dispatch<any>(updateResources([process]));
+ dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ }
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+};
+
+export const moveProcess =
+ (data: MoveToFormDialogData, isSecondaryMove = false) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const checkedList = getState().multiselect.checkedList;
+ const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist && no items passed in, default to normal context menu behavior
+ if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
+
+ const processesToMove: MoveableResource[] = uuidsToMove
+ .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
+ .filter(resource => resource.kind === ResourceKind.PROCESS);
+
+ for (const process of processesToMove) {
+ await moveSingleProcess(process);
+ }
+
+ //omly propagate if this call is the original
+ if (!isSecondaryMove) {
+ const kindsToMove: Set<string> = selectedToKindSet(checkedList);
+ kindsToMove.delete(ResourceKind.PROCESS);
+
+ kindsToMove.forEach(kind => {
+ secondaryMove[kind](data, true)(dispatch, getState, services);
+ });
+ }
+
+ async function moveSingleProcess(process: MoveableResource) {
+ try {
+ const oldProcess: MoveToFormDialogData = { name: process.name, uuid: process.uuid, ownerUuid: data.ownerUuid };
+ const movedProcess = await dispatch<any>(processMoveActions.moveProcess(oldProcess));
+ dispatch<any>(updateResources([movedProcess]));
+ dispatch<any>(reloadProjectMatchingUuid([movedProcess.ownerUuid]));
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Process has been moved.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+ }
+ };
+
+export const copyProcess = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const process = await dispatch<any>(processCopyActions.copyProcess(data));
+ dispatch<any>(updateResources([process]));
+ dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Process has been copied.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ dispatch<any>(navigateTo(process.uuid));
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+};
+
+export const resourceIsNotLoaded = (uuid: string) =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: `Resource identified by ${uuid} is not loaded.`,
+ kind: SnackbarKind.ERROR,
+ });
+
+export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
+ message: "User is not authenticated",
+ kind: SnackbarKind.ERROR,
+});
+
+export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
+ message: "Could not load user",
+ kind: SnackbarKind.ERROR,
+});
+
+export const reloadProjectMatchingUuid =
+ (matchingUuids: string[]) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
+ if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
+ dispatch<any>(loadProject(currentProjectPanelUuid));
+ }
+ };
+
+export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
+ dispatch<any>(loadSharedWithMePanel());
+ await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
+ await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
+});
+
+export const loadRunProcess = handleFirstTimeLoad(async (dispatch: Dispatch) => {
+ await dispatch<any>(loadRunProcessPanel());
+});
+
+export const loadPublicFavorites = () =>
+ handleFirstTimeLoad((dispatch: Dispatch) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES));
+ dispatch<any>(loadPublicFavoritePanel());
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES));
+ });
+
+export const loadSearchResults = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadSearchResultsPanel());
+});
+
+export const loadLinks = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadLinkPanel());
+});
+
+export const loadVirtualMachines = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadVirtualMachinesPanel());
+ dispatch(setVirtualMachinesBreadcrumbs());
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHELL_ACCESS));
+});
+
+export const loadVirtualMachinesAdmin = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadVirtualMachinesPanel());
+ dispatch(setVirtualMachinesAdminBreadcrumbs());
- dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({pickerId: SIDE_PANEL_TREE} ))
++ dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({ pickerId: SIDE_PANEL_TREE }))
+});
+
+export const loadRepositories = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadRepositoriesPanel());
+ dispatch(setRepositoriesBreadcrumbs());
+});
+
+export const loadSshKeys = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadSshKeysPanel());
+});
+
+export const loadInstanceTypes = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.INSTANCE_TYPES));
+ dispatch(setInstanceTypesBreadcrumbs());
+});
+
+export const loadSiteManager = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadSiteManagerPanel());
+});
+
+export const loadUserProfile = (userUuid?: string) =>
+ handleFirstTimeLoad((dispatch: Dispatch<any>) => {
+ if (userUuid) {
+ dispatch(setUserProfileBreadcrumbs(userUuid));
+ dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
+ } else {
+ dispatch(setMyAccountBreadcrumbs());
+ dispatch(userProfilePanelActions.loadUserProfilePanel());
+ }
+ });
+
+export const loadLinkAccount = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
+ dispatch(loadLinkAccountPanel());
+});
+
+export const loadKeepServices = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadKeepServicesPanel());
+});
+
+export const loadUsers = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadUsersPanel());
+ dispatch(setUsersBreadcrumbs());
+});
+
+export const loadApiClientAuthorizations = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadApiClientAuthorizationsPanel());
+});
+
+export const loadGroupsPanel = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
+ dispatch(setGroupsBreadcrumbs());
+ dispatch(groupPanelActions.loadGroupsPanel());
+});
+
+export const loadGroupDetailsPanel = (groupUuid: string) =>
+ handleFirstTimeLoad((dispatch: Dispatch<any>) => {
+ dispatch(setGroupDetailsBreadcrumbs(groupUuid));
+ dispatch(groupDetailsPanelActions.loadGroupDetailsPanel(groupUuid));
+ });
+
+const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch<any>) => {
+ const uuid = typeof project === "string" ? project : project.uuid;
+ dispatch(loadDetailsPanel(uuid));
+ if (typeof project !== "string") {
+ dispatch(updateResources([project]));
+ }
+};
+
+const loadGroupContentsResource = async (params: { uuid: string; userUuid: string; services: ServiceRepository }) => {
+ const filters = new FilterBuilder().addEqual("uuid", params.uuid).getFilters();
+ const { items } = await params.services.groupsService.contents(params.userUuid, {
+ filters,
+ recursive: true,
+ includeTrash: true,
+ });
+ const resource = items.shift();
+ let handler: GroupContentsHandler;
+ if (resource) {
+ handler =
+ (resource.kind === ResourceKind.COLLECTION || resource.kind === ResourceKind.PROJECT) && resource.isTrashed
+ ? groupContentsHandlers.TRASHED(resource)
+ : groupContentsHandlers.OWNED(resource);
+ } else {
+ const kind = extractUuidKind(params.uuid);
+ let resource: GroupContentsResource;
+ if (kind === ResourceKind.COLLECTION) {
+ resource = await params.services.collectionService.get(params.uuid);
+ } else if (kind === ResourceKind.PROJECT) {
+ resource = await params.services.projectService.get(params.uuid);
+ } else if (kind === ResourceKind.WORKFLOW) {
+ resource = await params.services.workflowService.get(params.uuid);
+ } else if (kind === ResourceKind.CONTAINER_REQUEST) {
+ resource = await params.services.containerRequestService.get(params.uuid);
+ } else {
+ throw new Error("loadGroupContentsResource unsupported kind " + kind);
+ }
+ handler = groupContentsHandlers.SHARED(resource);
+ }
+ return (cases: MatchCases<typeof groupContentsHandlersRecord, GroupContentsHandler, void>) => groupContentsHandlers.match(handler, cases);
+};
+
+const groupContentsHandlersRecord = {
+ TRASHED: ofType<GroupContentsResource>(),
+ SHARED: ofType<GroupContentsResource>(),
+ OWNED: ofType<GroupContentsResource>(),
+};
+
+const groupContentsHandlers = unionize(groupContentsHandlersRecord);
+
+type GroupContentsHandler = UnionOf<typeof groupContentsHandlers>;
+
+type CollectionCopyResource = Resource & { name: string; fromContextMenu: boolean };
+
+type MoveableResource = Resource & { name: string };
+
+type MoveFunc = (
+ data: MoveToFormDialogData,
+ isSecondaryMove?: boolean
+) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>;
+
+const secondaryMove: Record<string, MoveFunc> = {
+ [ResourceKind.PROJECT]: moveProject,
+ [ResourceKind.PROCESS]: moveProcess,
+ [ResourceKind.COLLECTION]: moveCollection,
+};
diff --cc services/workbench2/src/views/workflow-panel/registered-workflow-panel.tsx
index 53c5928023,0000000000..578ca1a0e1
mode 100644,000000..100644
--- a/services/workbench2/src/views/workflow-panel/registered-workflow-panel.tsx
+++ b/services/workbench2/src/views/workflow-panel/registered-workflow-panel.tsx
@@@ -1,229 -1,0 +1,234 @@@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+ StyleRulesCallback,
+ WithStyles,
+ withStyles,
+ Tooltip,
+ Typography,
+ Card,
+ CardHeader,
+ CardContent,
+ IconButton
+} from '@material-ui/core';
+import { connect, DispatchProp } from "react-redux";
+import { RouteComponentProps } from 'react-router';
+import { ArvadosTheme } from 'common/custom-theme';
+import { RootState } from 'store/store';
+import { WorkflowIcon, MoreVerticalIcon } from 'components/icon/icon';
+import { WorkflowResource } from 'models/workflow';
+import { ProcessOutputCollectionFiles } from 'views/process-panel/process-output-collection-files';
+import { WorkflowDetailsAttributes, RegisteredWorkflowPanelDataProps, getRegisteredWorkflowPanelData } from 'views-components/details-panel/workflow-details';
+import { getResource } from 'store/resources/resources';
+import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
+import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
+import { ProcessIOCard, ProcessIOCardType } from 'views/process-panel/process-io-card';
+import { NotFoundView } from 'views/not-found-panel/not-found-panel';
++import { WorkflowProcessesPanel } from './workflow-processes-panel';
+
+type CssRules = 'root'
+ | 'button'
+ | 'infoCard'
+ | 'propertiesCard'
+ | 'filesCard'
+ | 'iconHeader'
+ | 'tag'
+ | 'label'
+ | 'value'
+ | 'link'
+ | 'centeredLabel'
+ | 'warningLabel'
+ | 'collectionName'
+ | 'readOnlyIcon'
+ | 'header'
+ | 'title'
+ | 'avatar'
+ | 'content';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ },
+ button: {
+ cursor: 'pointer'
+ },
+ infoCard: {
+ },
+ propertiesCard: {
+ padding: 0,
+ },
+ filesCard: {
+ padding: 0,
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.greyL
+ },
+ tag: {
+ marginRight: theme.spacing.unit / 2,
+ marginBottom: theme.spacing.unit / 2
+ },
+ label: {
+ fontSize: '0.875rem',
+ },
+ centeredLabel: {
+ fontSize: '0.875rem',
+ textAlign: 'center'
+ },
+ warningLabel: {
+ fontStyle: 'italic'
+ },
+ collectionName: {
+ flexDirection: 'column',
+ },
+ value: {
+ textTransform: 'none',
+ fontSize: '0.875rem'
+ },
+ link: {
+ fontSize: '0.875rem',
+ color: theme.palette.primary.main,
+ '&:hover': {
+ cursor: 'pointer'
+ }
+ },
+ readOnlyIcon: {
+ marginLeft: theme.spacing.unit,
+ fontSize: 'small',
+ },
+ header: {
+ paddingTop: theme.spacing.unit,
+ paddingBottom: theme.spacing.unit,
+ },
+ title: {
+ overflow: 'hidden',
+ paddingTop: theme.spacing.unit * 0.5,
+ color: theme.customs.colors.green700,
+ },
+ avatar: {
+ alignSelf: 'flex-start',
+ paddingTop: theme.spacing.unit * 0.5
+ },
+ content: {
+ padding: theme.spacing.unit * 1.0,
+ paddingTop: theme.spacing.unit * 0.5,
+ '&:last-child': {
+ paddingBottom: theme.spacing.unit * 1,
+ }
+ }
+});
+
+type RegisteredWorkflowPanelProps = RegisteredWorkflowPanelDataProps & DispatchProp & WithStyles<CssRules>
+
+export const RegisteredWorkflowPanel = withStyles(styles)(connect(
+ (state: RootState, props: RouteComponentProps<{ id: string }>) => {
+ const item = getResource<WorkflowResource>(props.match.params.id)(state.resources);
+ if (item) {
+ return getRegisteredWorkflowPanelData(item, state.auth);
+ }
+ return { item, inputParams: [], outputParams: [], workflowCollection: "", gitprops: {} };
+ })(
+ class extends React.Component<RegisteredWorkflowPanelProps> {
+ render() {
+ const { classes, item, inputParams, outputParams, workflowCollection } = this.props;
+ const panelsData: MPVPanelState[] = [
+ { name: "Details" },
+ { name: "Inputs" },
+ { name: "Outputs" },
- { name: "Files" },
++ { name: "Executions" },
++ { name: "Definition" },
+ ];
+ return item
+ ? <MPVContainer className={classes.root} spacing={8} direction="column" justify-content="flex-start" wrap="nowrap" panelStates={panelsData}>
+ <MPVPanelContent xs="auto" data-cy='registered-workflow-info-panel'>
+ <Card className={classes.infoCard}>
+ <CardHeader
+ className={classes.header}
+ classes={{
+ content: classes.title,
+ avatar: classes.avatar,
+ }}
+ avatar={<WorkflowIcon className={classes.iconHeader} />}
+ title={
+ <Tooltip title={item.name} placement="bottom-start">
+ <Typography noWrap variant='h6'>
+ {item.name}
+ </Typography>
+ </Tooltip>
+ }
+ subheader={
+ <Tooltip title={item.description || '(no-description)'} placement="bottom-start">
+ <Typography noWrap variant='body1' color='inherit'>
+ {item.description || '(no-description)'}
+ </Typography>
+ </Tooltip>}
+ action={
+ <Tooltip title="More options" disableFocusListener>
+ <IconButton
+ aria-label="More options"
+ onClick={event => this.handleContextMenu(event)}>
+ <MoreVerticalIcon />
+ </IconButton>
+ </Tooltip>}
+
+ />
+
+ <CardContent className={classes.content}>
+ <WorkflowDetailsAttributes workflow={item} />
+ </CardContent>
+ </Card>
+ </MPVPanelContent>
+ <MPVPanelContent forwardProps xs data-cy="process-inputs">
+ <ProcessIOCard
+ label={ProcessIOCardType.INPUT}
+ params={inputParams}
+ raw={{}}
+ forceShowParams={true}
+ />
+ </MPVPanelContent>
+ <MPVPanelContent forwardProps xs data-cy="process-outputs">
+ <ProcessIOCard
+ label={ProcessIOCardType.OUTPUT}
+ params={outputParams}
+ raw={{}}
+ forceShowParams={true}
+ />
+ </MPVPanelContent>
++ <MPVPanelContent forwardProps xs>
++ <WorkflowProcessesPanel />
++ </MPVPanelContent>
+ <MPVPanelContent xs>
+ <Card className={classes.filesCard}>
+ <ProcessOutputCollectionFiles isWritable={false} currentItemUuid={workflowCollection} />
+ </Card>
+ </MPVPanelContent>
+ </MPVContainer>
+ :
+ <NotFoundView
+ icon={WorkflowIcon}
+ messages={["Workflow not found"]}
+ />
+ }
+
+ handleContextMenu = (event: React.MouseEvent<any>) => {
+ const { uuid, ownerUuid, name, description,
+ kind } = this.props.item;
+ const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(uuid));
+ const resource = {
+ uuid,
+ ownerUuid,
+ name,
+ description,
+ kind,
+ menuKind,
+ };
+ // Avoid expanding/collapsing the panel
+ event.stopPropagation();
+ this.props.dispatch<any>(openContextMenu(event, resource));
+ }
+ }
+ )
+);
diff --cc services/workbench2/src/views/workflow-panel/workflow-processes-panel-root.tsx
index 0000000000,1ca36efc89..1ca36efc89
mode 000000,100644..100644
--- a/services/workbench2/src/views/workflow-panel/workflow-processes-panel-root.tsx
+++ b/services/workbench2/src/views/workflow-panel/workflow-processes-panel-root.tsx
diff --cc services/workbench2/src/views/workflow-panel/workflow-processes-panel.tsx
index 0000000000,548f8fc432..548f8fc432
mode 000000,100644..100644
--- a/services/workbench2/src/views/workflow-panel/workflow-processes-panel.tsx
+++ b/services/workbench2/src/views/workflow-panel/workflow-processes-panel.tsx
commit 3b735dd9330e0989f51a76771c3303031154154e
Author: Peter Amstutz <peter.amstutz at curii.com>
Date: Fri Oct 27 17:07:04 2023 -0400
21158: Adjust panel order
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>
diff --git a/src/views/workflow-panel/registered-workflow-panel.tsx b/src/views/workflow-panel/registered-workflow-panel.tsx
index 98fcabded4..0e9e2b404f 100644
--- a/src/views/workflow-panel/registered-workflow-panel.tsx
+++ b/src/views/workflow-panel/registered-workflow-panel.tsx
@@ -137,8 +137,8 @@ export const RegisteredWorkflowPanel = withStyles(styles)(connect(
{ name: "Details" },
{ name: "Inputs" },
{ name: "Outputs" },
- { name: "Files" },
{ name: "Executions" },
+ { name: "Definition" },
];
return item
? <MPVContainer className={classes.root} spacing={8} direction="column" justify-content="flex-start" wrap="nowrap" panelStates={panelsData}>
@@ -196,14 +196,14 @@ export const RegisteredWorkflowPanel = withStyles(styles)(connect(
showParams={true}
/>
</MPVPanelContent>
+ <MPVPanelContent forwardProps xs>
+ <WorkflowProcessesPanel />
+ </MPVPanelContent>
<MPVPanelContent xs>
<Card className={classes.filesCard}>
<ProcessOutputCollectionFiles isWritable={false} currentItemUuid={workflowCollection} />
</Card>
</MPVPanelContent>
- <MPVPanelContent forwardProps xs>
- <WorkflowProcessesPanel />
- </MPVPanelContent>
</MPVContainer>
: null;
}
commit 8ebe9d6acf1aef414231093c5c5cd8e2912cf84d
Author: Peter Amstutz <peter.amstutz at curii.com>
Date: Fri Oct 27 17:04:24 2023 -0400
21158: Missing files
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>
diff --git a/src/views/workflow-panel/workflow-processes-panel-root.tsx b/src/views/workflow-panel/workflow-processes-panel-root.tsx
new file mode 100644
index 0000000000..1ca36efc89
--- /dev/null
+++ b/src/views/workflow-panel/workflow-processes-panel-root.tsx
@@ -0,0 +1,126 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import 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 { ResourceCreatedAtDate, ProcessStatus, ContainerRunTime } from 'views-components/data-explorer/renderers';
+import { ProcessIcon } from 'components/icon/icon';
+import { ResourceName } from 'views-components/data-explorer/renderers';
+import { WORKFLOW_PROCESSES_PANEL_ID } from 'store/workflow-panel/workflow-panel-actions';
+import { createTree } from 'models/tree';
+import { getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
+import { ResourcesState } from 'store/resources/resources';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import { StyleRulesCallback, Typography, WithStyles, withStyles } from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { ProcessResource } from 'models/process';
+
+type CssRules = 'iconHeader' | 'cardHeader';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.greyL,
+ marginRight: theme.spacing.unit * 2,
+ },
+ cardHeader: {
+ display: 'flex',
+ },
+});
+
+export enum WorkflowProcessesPanelColumnNames {
+ NAME = "Name",
+ STATUS = "Status",
+ CREATED_AT = "Created At",
+ RUNTIME = "Run Time"
+}
+
+export interface WorkflowProcessesPanelFilter extends DataTableFilterItem {
+ type: ResourceKind | ContainerRequestState;
+}
+
+export const workflowProcessesPanelColumns: DataColumns<string, ProcessResource> = [
+ {
+ name: WorkflowProcessesPanelColumnNames.NAME,
+ selected: true,
+ configurable: true,
+ sort: { direction: SortDirection.NONE, field: "name" },
+ filters: createTree(),
+ render: uuid => <ResourceName uuid={uuid} />
+ },
+ {
+ name: WorkflowProcessesPanelColumnNames.STATUS,
+ selected: true,
+ configurable: true,
+ mutuallyExclusiveFilters: true,
+ filters: getInitialProcessStatusFilters(),
+ render: uuid => <ProcessStatus uuid={uuid} />,
+ },
+ {
+ name: WorkflowProcessesPanelColumnNames.CREATED_AT,
+ selected: true,
+ configurable: true,
+ sort: { direction: SortDirection.DESC, field: "createdAt" },
+ filters: createTree(),
+ render: uuid => <ResourceCreatedAtDate uuid={uuid} />
+ },
+ {
+ name: WorkflowProcessesPanelColumnNames.RUNTIME,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ContainerRunTime uuid={uuid} />
+ }
+];
+
+export interface WorkflowProcessesPanelDataProps {
+ resources: ResourcesState;
+}
+
+export interface WorkflowProcessesPanelActionProps {
+ onItemClick: (item: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string, resources: ResourcesState) => void;
+ onItemDoubleClick: (item: string) => void;
+}
+
+type WorkflowProcessesPanelProps = WorkflowProcessesPanelActionProps & WorkflowProcessesPanelDataProps;
+
+const DEFAULT_VIEW_MESSAGES = [
+ 'No processes available for listing.',
+ 'The current process may not have any or none matches current filtering.'
+];
+
+type WorkflowProcessesTitleProps = WithStyles<CssRules>;
+
+const WorkflowProcessesTitle = withStyles(styles)(
+ ({ classes }: WorkflowProcessesTitleProps) =>
+ <div className={classes.cardHeader}>
+ <ProcessIcon className={classes.iconHeader} /><span></span>
+ <Typography noWrap variant='h6' color='inherit'>
+ Workflow Processes
+ </Typography>
+ </div>
+);
+
+export const WorkflowProcessesPanelRoot = (props: WorkflowProcessesPanelProps & MPVPanelProps) => {
+ return <DataExplorer
+ id={WORKFLOW_PROCESSES_PANEL_ID}
+ onRowClick={props.onItemClick}
+ onRowDoubleClick={props.onItemDoubleClick}
+ onContextMenu={(event, item) => props.onContextMenu(event, item, props.resources)}
+ contextMenuColumn={true}
+ defaultViewIcon={ProcessIcon}
+ defaultViewMessages={DEFAULT_VIEW_MESSAGES}
+ doHidePanel={props.doHidePanel}
+ doMaximizePanel={props.doMaximizePanel}
+ doUnMaximizePanel={props.doUnMaximizePanel}
+ panelMaximized={props.panelMaximized}
+ panelName={props.panelName}
+ title={<WorkflowProcessesTitle />} />;
+};
diff --git a/src/views/workflow-panel/workflow-processes-panel.tsx b/src/views/workflow-panel/workflow-processes-panel.tsx
new file mode 100644
index 0000000000..548f8fc432
--- /dev/null
+++ b/src/views/workflow-panel/workflow-processes-panel.tsx
@@ -0,0 +1,33 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { openProcessContextMenu } from "store/context-menu/context-menu-actions";
+import { WorkflowProcessesPanelRoot, WorkflowProcessesPanelActionProps, WorkflowProcessesPanelDataProps } from "views/workflow-panel/workflow-processes-panel-root";
+import { RootState } from "store/store";
+import { navigateTo } from "store/navigation/navigation-action";
+import { loadDetailsPanel } from "store/details-panel/details-panel-action";
+import { getProcess } from "store/processes/process";
+
+const mapDispatchToProps = (dispatch: Dispatch): WorkflowProcessesPanelActionProps => ({
+ onContextMenu: (event, resourceUuid, resources) => {
+ const process = getProcess(resourceUuid)(resources);
+ if (process) {
+ dispatch<any>(openProcessContextMenu(event, process));
+ }
+ },
+ onItemClick: (uuid: string) => {
+ dispatch<any>(loadDetailsPanel(uuid));
+ },
+ onItemDoubleClick: uuid => {
+ dispatch<any>(navigateTo(uuid));
+ },
+});
+
+const mapStateToProps = (state: RootState): WorkflowProcessesPanelDataProps => ({
+ resources: state.resources,
+});
+
+export const WorkflowProcessesPanel = connect(mapStateToProps, mapDispatchToProps)(WorkflowProcessesPanelRoot);
commit 2bbfc3fdfa59d668c291b7f4f3ad76979f30231e
Author: Peter Amstutz <peter.amstutz at curii.com>
Date: Fri Oct 27 17:03:51 2023 -0400
21158: Displays executions from the current workflow
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>
diff --git a/src/store/store.ts b/src/store/store.ts
index daa9812e72..ee861f18be 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -20,9 +20,11 @@ import { collectionPanelFilesReducer } from "./collection-panel/collection-panel
import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
+import { WORKFLOW_PROCESSES_PANEL_ID } from "./workflow-panel/workflow-panel-actions";
import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
import { AllProcessesPanelMiddlewareService } from "./all-processes-panel/all-processes-panel-middleware-service";
+import { WorkflowProcessesMiddlewareService } from "./workflow-panel/workflow-middleware-service";
import { collectionPanelReducer } from "./collection-panel/collection-panel-reducer";
import { dialogReducer } from "./dialog/dialog-reducer";
import { ServiceRepository } from "services/services";
@@ -96,6 +98,7 @@ export function configureStore(history: History, services: ServiceRepository, co
const projectPanelMiddleware = dataExplorerMiddleware(new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID));
const favoritePanelMiddleware = dataExplorerMiddleware(new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID));
const allProcessessPanelMiddleware = dataExplorerMiddleware(new AllProcessesPanelMiddlewareService(services, ALL_PROCESSES_PANEL_ID));
+ const workflowProcessessPanelMiddleware = dataExplorerMiddleware(new WorkflowProcessesMiddlewareService(services, WORKFLOW_PROCESSES_PANEL_ID));
const trashPanelMiddleware = dataExplorerMiddleware(new TrashPanelMiddlewareService(services, TRASH_PANEL_ID));
const searchResultsPanelMiddleware = dataExplorerMiddleware(new SearchResultsMiddlewareService(services, SEARCH_RESULTS_PANEL_ID));
const sharedWithMePanelMiddleware = dataExplorerMiddleware(new SharedWithMeMiddlewareService(services, SHARED_WITH_ME_PANEL_ID));
@@ -152,6 +155,7 @@ export function configureStore(history: History, services: ServiceRepository, co
collectionsContentAddress,
subprocessMiddleware,
treePickerSearchMiddleware,
+ workflowProcessessPanelMiddleware
];
const reduceMiddlewaresFn: (a: Middleware[], b: MiddlewareListReducer) => Middleware[] = (a, b) => b(a, services);
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index b03400d5ae..b4c5df887e 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -98,6 +98,8 @@ import { AdminMenuIcon } from "components/icon/icon";
import { userProfileGroupsColumns } from "views/user-profile-panel/user-profile-panel-root";
import { selectedToArray, selectedToKindSet } from "components/multiselect-toolbar/MultiselectToolbar";
import { multiselectActions } from "store/multiselect/multiselect-actions";
+import { workflowProcessesPanelColumns } from "views/workflow-panel/workflow-processes-panel-root";
+import { workflowProcessesPanelActions } from "store/workflow-panel/workflow-panel-actions";
export const WORKBENCH_LOADING_SCREEN = "workbenchLoadingScreen";
@@ -179,6 +181,7 @@ export const loadWorkbench = () => async (dispatch: Dispatch, getState: () => Ro
})
);
dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
+ dispatch(workflowProcessesPanelActions.SET_COLUMNS({ columns: workflowProcessesPanelColumns }));
if (services.linkAccountService.getAccountToLink()) {
dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
@@ -579,6 +582,7 @@ export const loadRegisteredWorkflow = (uuid: string) =>
await dispatch<any>(finishLoadingProject(workflow.ownerUuid));
await dispatch<any>(activateSidePanelTreeItem(workflow.ownerUuid));
dispatch<any>(breadcrumbfunc(workflow.ownerUuid));
+ dispatch(workflowProcessesPanelActions.REQUEST_ITEMS());
}
}
});
diff --git a/src/store/workflow-panel/workflow-middleware-service.ts b/src/store/workflow-panel/workflow-middleware-service.ts
index 587f02246c..aa34218942 100644
--- a/src/store/workflow-panel/workflow-middleware-service.ts
+++ b/src/store/workflow-panel/workflow-middleware-service.ts
@@ -13,6 +13,10 @@ import { FilterBuilder } from 'services/api/filter-builder';
import { WorkflowResource } from 'models/workflow';
import { ListResults } from 'services/common-service/common-service';
import { workflowPanelActions } from 'store/workflow-panel/workflow-panel-actions';
+import { matchRegisteredWorkflowRoute } from 'routes/routes';
+import { ProcessesMiddlewareService } from "store/processes/processes-middleware-service";
+import { workflowProcessesPanelActions } from "./workflow-panel-actions";
+import { joinFilters } from "services/api/filter-builder";
export class WorkflowMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
@@ -56,3 +60,27 @@ const couldNotFetchWorkflows = () =>
message: 'Could not fetch workflows.',
kind: SnackbarKind.ERROR
});
+
+
+export class WorkflowProcessesMiddlewareService extends ProcessesMiddlewareService {
+ constructor(services: ServiceRepository, id: string) {
+ super(services, workflowProcessesPanelActions, id);
+ }
+
+ getFilters(api: MiddlewareAPI<Dispatch, RootState>, dataExplorer: DataExplorer): string | null {
+ const state = api.getState();
+
+ if (!state.router.location) { return null; }
+
+ const registeredWorkflowMatch = matchRegisteredWorkflowRoute(state.router.location.pathname);
+ if (!registeredWorkflowMatch) { return null; }
+
+ const workflow_uuid = registeredWorkflowMatch.params.id;
+
+ const requesting_container = new FilterBuilder().addEqual('properties.template_uuid', workflow_uuid).getFilters();
+ const sup = super.getFilters(api, dataExplorer);
+ if (sup === null) { return null; }
+
+ return joinFilters(sup, requesting_container);
+ }
+}
diff --git a/src/store/workflow-panel/workflow-panel-actions.ts b/src/store/workflow-panel/workflow-panel-actions.ts
index d8c3b65141..b4c1d3fb7f 100644
--- a/src/store/workflow-panel/workflow-panel-actions.ts
+++ b/src/store/workflow-panel/workflow-panel-actions.ts
@@ -30,6 +30,9 @@ const UUID_PREFIX_PROPERTY_NAME = 'uuidPrefix';
const WORKFLOW_PANEL_DETAILS_UUID = 'workflowPanelDetailsUuid';
export const workflowPanelActions = bindDataExplorerActions(WORKFLOW_PANEL_ID);
+export const WORKFLOW_PROCESSES_PANEL_ID = "workflowProcessesPanel";
+export const workflowProcessesPanelActions = bindDataExplorerActions(WORKFLOW_PROCESSES_PANEL_ID);
+
export const loadWorkflowPanel = () =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(workflowPanelActions.REQUEST_ITEMS());
diff --git a/src/views/workflow-panel/registered-workflow-panel.tsx b/src/views/workflow-panel/registered-workflow-panel.tsx
index 5973efedc8..98fcabded4 100644
--- a/src/views/workflow-panel/registered-workflow-panel.tsx
+++ b/src/views/workflow-panel/registered-workflow-panel.tsx
@@ -26,6 +26,7 @@ import { getResource } from 'store/resources/resources';
import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
import { ProcessIOCard, ProcessIOCardType } from 'views/process-panel/process-io-card';
+import { WorkflowProcessesPanel } from './workflow-processes-panel';
type CssRules = 'root'
| 'button'
@@ -137,6 +138,7 @@ export const RegisteredWorkflowPanel = withStyles(styles)(connect(
{ name: "Inputs" },
{ name: "Outputs" },
{ name: "Files" },
+ { name: "Executions" },
];
return item
? <MPVContainer className={classes.root} spacing={8} direction="column" justify-content="flex-start" wrap="nowrap" panelStates={panelsData}>
@@ -199,6 +201,9 @@ export const RegisteredWorkflowPanel = withStyles(styles)(connect(
<ProcessOutputCollectionFiles isWritable={false} currentItemUuid={workflowCollection} />
</Card>
</MPVPanelContent>
+ <MPVPanelContent forwardProps xs>
+ <WorkflowProcessesPanel />
+ </MPVPanelContent>
</MPVContainer>
: null;
}
commit b6378ecb409ca394dd3cd866fb1749c5decb316c
Author: Peter Amstutz <peter.amstutz at curii.com>
Date: Fri Oct 27 16:19:02 2023 -0400
21158: Remove unused imports
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>
diff --git a/src/store/all-processes-panel/all-processes-panel-middleware-service.ts b/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
index e6d0192dad..079cf11e71 100644
--- a/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
+++ b/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
@@ -2,21 +2,15 @@
//
// SPDX-License-Identifier: AGPL-3.0
-import { dataExplorerToListParams, getDataExplorerColumnFilters, getOrder } from "store/data-explorer/data-explorer-middleware-service";
+import { getDataExplorerColumnFilters } from "store/data-explorer/data-explorer-middleware-service";
import { RootState } from "../store";
import { ServiceRepository } from "services/services";
-import { FilterBuilder, joinFilters } from "services/api/filter-builder";
+import { joinFilters } from "services/api/filter-builder";
import { allProcessesPanelActions } from "./all-processes-panel-action";
import { Dispatch, MiddlewareAPI } from "redux";
-import { resourcesActions } from "store/resources/resources-actions";
-import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
-import { getDataExplorer, DataExplorer } from "store/data-explorer/data-explorer-reducer";
-import { loadMissingProcessesInformation } from "store/project-panel/project-panel-middleware-service";
+import { DataExplorer } from "store/data-explorer/data-explorer-reducer";
import { DataColumns } from "components/data-table/data-table";
import {
- ProcessStatusFilter,
- buildProcessStatusFilters,
serializeOnlyProcessTypeFilters
} from "../resource-type-filters/resource-type-filters";
import { AllProcessesPanelColumnNames } from "views/all-processes-panel/all-processes-panel";
diff --git a/src/store/processes/processes-middleware-service.ts b/src/store/processes/processes-middleware-service.ts
index d252527276..3154e1aec9 100644
--- a/src/store/processes/processes-middleware-service.ts
+++ b/src/store/processes/processes-middleware-service.ts
@@ -12,7 +12,7 @@ import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
import { DataExplorer, getDataExplorer } from 'store/data-explorer/data-explorer-reducer';
import { BoundDataExplorerActions } from 'store/data-explorer/data-explorer-action';
import { updateResources } from 'store/resources/resources-actions';
-import { ListArguments, ListResults } from 'services/common-service/common-service';
+import { ListArguments } from 'services/common-service/common-service';
import { ProcessResource } from 'models/process';
import { FilterBuilder, joinFilters } from 'services/api/filter-builder';
import { DataColumns } from 'components/data-table/data-table';
diff --git a/src/store/subprocess-panel/subprocess-panel-middleware-service.ts b/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
index 1ae9c41e41..0ac5df6a0f 100644
--- a/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
+++ b/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
@@ -2,25 +2,13 @@
//
// SPDX-License-Identifier: AGPL-3.0
-import { ServiceRepository } from 'services/services';
-import { MiddlewareAPI, Dispatch } from 'redux';
-import {
- dataExplorerToListParams, listResultsToDataExplorerItemsMeta, getDataExplorerColumnFilters, getOrder
-} 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 { ListResults } from 'services/common-service/common-service';
-import { ProcessResource } from 'models/process';
-import { FilterBuilder, joinFilters } from 'services/api/filter-builder';
-import { subprocessPanelActions } from './subprocess-panel-actions';
-import { DataColumns } from 'components/data-table/data-table';
-import { ProcessStatusFilter, buildProcessStatusFilters } from '../resource-type-filters/resource-type-filters';
-import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
-import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
-import { loadMissingProcessesInformation } from '../project-panel/project-panel-middleware-service';
+import { RootState } from "../store";
+import { ServiceRepository } from "services/services";
+import { FilterBuilder, joinFilters } from "services/api/filter-builder";
+import { Dispatch, MiddlewareAPI } from "redux";
+import { DataExplorer } from "store/data-explorer/data-explorer-reducer";
import { ProcessesMiddlewareService } from "store/processes/processes-middleware-service";
+import { subprocessPanelActions } from './subprocess-panel-actions';
import { getProcess } from "store/processes/process";
export class SubprocessMiddlewareService extends ProcessesMiddlewareService {
commit 68dee98de77221374456e635881f7e268f2745ea
Author: Peter Amstutz <peter.amstutz at curii.com>
Date: Fri Oct 27 16:05:18 2023 -0400
21158: Refactor Process list middleware to a common base class
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>
diff --git a/src/store/all-processes-panel/all-processes-panel-middleware-service.ts b/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
index 955d9689af..e6d0192dad 100644
--- a/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
+++ b/src/store/all-processes-panel/all-processes-panel-middleware-service.ts
@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0
-import { DataExplorerMiddlewareService, dataExplorerToListParams, getDataExplorerColumnFilters, getOrder } from "store/data-explorer/data-explorer-middleware-service";
+import { dataExplorerToListParams, getDataExplorerColumnFilters, getOrder } from "store/data-explorer/data-explorer-middleware-service";
import { RootState } from "../store";
import { ServiceRepository } from "services/services";
import { FilterBuilder, joinFilters } from "services/api/filter-builder";
@@ -20,82 +20,20 @@ import {
serializeOnlyProcessTypeFilters
} from "../resource-type-filters/resource-type-filters";
import { AllProcessesPanelColumnNames } from "views/all-processes-panel/all-processes-panel";
-import { containerRequestFieldsNoMounts, ContainerRequestResource } from "models/container-request";
+import { ProcessesMiddlewareService } from "store/processes/processes-middleware-service";
+import { ContainerRequestResource } from 'models/container-request';
-export class AllProcessesPanelMiddlewareService extends DataExplorerMiddlewareService {
- constructor(private services: ServiceRepository, id: string) {
- super(id);
+export class AllProcessesPanelMiddlewareService extends ProcessesMiddlewareService {
+ constructor(services: ServiceRepository, id: string) {
+ super(services, allProcessesPanelActions, id);
}
- async requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean, background?: boolean) {
- const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
- if (!dataExplorer) {
- api.dispatch(allProcessesPanelDataExplorerIsNotSet());
- } else {
- try {
- if (!background) { api.dispatch(progressIndicatorActions.START_WORKING(this.getId())); }
- const processItems = await this.services.containerRequestService.list(
- {
- ...getParams(dataExplorer),
- // Omit mounts when viewing all process panel
- select: containerRequestFieldsNoMounts,
- });
+ getFilters(api: MiddlewareAPI<Dispatch, RootState>, dataExplorer: DataExplorer): string | null {
+ const sup = super.getFilters(api, dataExplorer);
+ if (sup === null) { return null; }
+ const columns = dataExplorer.columns as DataColumns<string, ContainerRequestResource>;
- if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
- api.dispatch(resourcesActions.SET_RESOURCES(processItems.items));
- await api.dispatch<any>(loadMissingProcessesInformation(processItems.items));
- api.dispatch(allProcessesPanelActions.SET_ITEMS({
- items: processItems.items.map((resource: any) => resource.uuid),
- itemsAvailable: processItems.itemsAvailable,
- page: Math.floor(processItems.offset / processItems.limit),
- rowsPerPage: processItems.limit
- }));
- } catch {
- if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
- api.dispatch(allProcessesPanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
- api.dispatch(couldNotFetchAllProcessesListing());
- }
- }
+ const typeFilters = serializeOnlyProcessTypeFilters(getDataExplorerColumnFilters(columns, AllProcessesPanelColumnNames.TYPE));
+ return joinFilters(sup, typeFilters);
}
}
-
-const getParams = (dataExplorer: DataExplorer) => ({
- ...dataExplorerToListParams(dataExplorer),
- order: getOrder<ContainerRequestResource>(dataExplorer),
- filters: getFilters(dataExplorer)
-});
-
-const getFilters = (dataExplorer: DataExplorer) => {
- const columns = dataExplorer.columns as DataColumns<string, ContainerRequestResource>;
- const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
- const activeStatusFilter = Object.keys(statusColumnFilters).find(
- filterName => statusColumnFilters[filterName].selected
- ) || ProcessStatusFilter.ALL;
-
- const nameFilter = new FilterBuilder().addILike("name", dataExplorer.searchValue).getFilters();
- const statusFilter = buildProcessStatusFilters(new FilterBuilder(), activeStatusFilter).getFilters();
- const typeFilters = serializeOnlyProcessTypeFilters(getDataExplorerColumnFilters(columns, AllProcessesPanelColumnNames.TYPE));
-
- return joinFilters(
- nameFilter,
- statusFilter,
- typeFilters
- );
-};
-
-const allProcessesPanelDataExplorerIsNotSet = () =>
- snackbarActions.OPEN_SNACKBAR({
- message: 'All Processes panel is not ready.',
- kind: SnackbarKind.ERROR
- });
-
-const couldNotFetchAllProcessesListing = () =>
- snackbarActions.OPEN_SNACKBAR({
- message: 'Could not fetch All Processes listing.',
- kind: SnackbarKind.ERROR
- });
diff --git a/src/store/data-explorer/data-explorer-action.ts b/src/store/data-explorer/data-explorer-action.ts
index ea050e609f..98df6f0c4a 100644
--- a/src/store/data-explorer/data-explorer-action.ts
+++ b/src/store/data-explorer/data-explorer-action.ts
@@ -52,3 +52,5 @@ export const bindDataExplorerActions = (id: string) => ({
RESET_EXPLORER_SEARCH_VALUE: () => dataExplorerActions.RESET_EXPLORER_SEARCH_VALUE({ id }),
SET_REQUEST_STATE: (payload: { requestState: DataTableRequestState }) => dataExplorerActions.SET_REQUEST_STATE({ ...payload, id }),
});
+
+export type BoundDataExplorerActions = ReturnType<typeof bindDataExplorerActions>;
diff --git a/src/store/processes/processes-middleware-service.ts b/src/store/processes/processes-middleware-service.ts
new file mode 100644
index 0000000000..d252527276
--- /dev/null
+++ b/src/store/processes/processes-middleware-service.ts
@@ -0,0 +1,95 @@
+// 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, getDataExplorerColumnFilters, getOrder
+} 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 { BoundDataExplorerActions } from 'store/data-explorer/data-explorer-action';
+import { updateResources } from 'store/resources/resources-actions';
+import { ListArguments, ListResults } from 'services/common-service/common-service';
+import { ProcessResource } from 'models/process';
+import { FilterBuilder, joinFilters } from 'services/api/filter-builder';
+import { DataColumns } from 'components/data-table/data-table';
+import { ProcessStatusFilter, buildProcessStatusFilters } from '../resource-type-filters/resource-type-filters';
+import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
+import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
+import { loadMissingProcessesInformation } from '../project-panel/project-panel-middleware-service';
+
+export class ProcessesMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, private actions: BoundDataExplorerActions, id: string) {
+ super(id);
+ }
+
+ getFilters(api: MiddlewareAPI<Dispatch, RootState>, dataExplorer: DataExplorer): string | null {
+ const columns = dataExplorer.columns as DataColumns<string, ContainerRequestResource>;
+ const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
+ const activeStatusFilter = Object.keys(statusColumnFilters).find(
+ filterName => statusColumnFilters[filterName].selected
+ ) || ProcessStatusFilter.ALL;
+
+ const nameFilter = new FilterBuilder().addILike("name", dataExplorer.searchValue).getFilters();
+ const statusFilter = buildProcessStatusFilters(new FilterBuilder(), activeStatusFilter).getFilters();
+
+ return joinFilters(
+ nameFilter,
+ statusFilter,
+ );
+ }
+
+ getParams(api: MiddlewareAPI<Dispatch, RootState>, dataExplorer: DataExplorer): ListArguments | null {
+ const filters = this.getFilters(api, dataExplorer)
+ if (filters === null) {
+ return null;
+ }
+ return {
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder<ProcessResource>(dataExplorer),
+ filters
+ };
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean, background?: boolean) {
+ const state = api.getState();
+ const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+
+ try {
+ if (!background) { api.dispatch(progressIndicatorActions.START_WORKING(this.getId())); }
+
+ const params = this.getParams(api, dataExplorer);
+
+ if (params !== null) {
+ const containerRequests = await this.services.containerRequestService.list(
+ {
+ ...this.getParams(api, dataExplorer),
+ select: containerRequestFieldsNoMounts
+ });
+ api.dispatch(updateResources(containerRequests.items));
+ await api.dispatch<any>(loadMissingProcessesInformation(containerRequests.items));
+ api.dispatch(this.actions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(containerRequests),
+ items: containerRequests.items.map(resource => resource.uuid),
+ }));
+ } else {
+ api.dispatch(this.actions.SET_ITEMS({
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage,
+ items: [],
+ }));
+ }
+ if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
+ } catch {
+ api.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch process list.',
+ kind: SnackbarKind.ERROR
+ }));
+ if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
+ }
+ }
+}
diff --git a/src/store/subprocess-panel/subprocess-panel-middleware-service.ts b/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
index 5124c8346a..1ae9c41e41 100644
--- a/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
+++ b/src/store/subprocess-panel/subprocess-panel-middleware-service.ts
@@ -5,7 +5,7 @@
import { ServiceRepository } from 'services/services';
import { MiddlewareAPI, Dispatch } from 'redux';
import {
- DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta, getDataExplorerColumnFilters, getOrder
+ dataExplorerToListParams, listResultsToDataExplorerItemsMeta, getDataExplorerColumnFilters, getOrder
} from 'store/data-explorer/data-explorer-middleware-service';
import { RootState } from 'store/store';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
@@ -20,81 +20,26 @@ import { ProcessStatusFilter, buildProcessStatusFilters } from '../resource-type
import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
import { loadMissingProcessesInformation } from '../project-panel/project-panel-middleware-service';
+import { ProcessesMiddlewareService } from "store/processes/processes-middleware-service";
+import { getProcess } from "store/processes/process";
-export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
- constructor(private services: ServiceRepository, id: string) {
- super(id);
+export class SubprocessMiddlewareService extends ProcessesMiddlewareService {
+ constructor(services: ServiceRepository, id: string) {
+ super(services, subprocessPanelActions, id);
}
- async requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean, background?: boolean) {
+ getFilters(api: MiddlewareAPI<Dispatch, RootState>, dataExplorer: DataExplorer): string | null {
const state = api.getState();
const parentContainerRequestUuid = state.processPanel.containerRequestUuid;
- if (parentContainerRequestUuid === "") { return; }
- const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+ if (!parentContainerRequestUuid) { return null; }
- try {
- if (!background) { api.dispatch(progressIndicatorActions.START_WORKING(this.getId())); }
- const parentContainerRequest = await this.services.containerRequestService.get(parentContainerRequestUuid);
- if (parentContainerRequest.containerUuid) {
- const containerRequests = await this.services.containerRequestService.list(
- {
- ...getParams(dataExplorer, parentContainerRequest),
- select: containerRequestFieldsNoMounts
- });
- api.dispatch(updateResources(containerRequests.items));
- await api.dispatch<any>(loadMissingProcessesInformation(containerRequests.items));
- // Populate the actual user view
- api.dispatch(setItems(containerRequests));
- }
- if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
- } catch {
- if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
- api.dispatch(couldNotFetchSubprocesses());
- }
- }
-}
-
-export const getParams = (
- dataExplorer: DataExplorer,
- parentContainerRequest: ContainerRequestResource) => ({
- ...dataExplorerToListParams(dataExplorer),
- order: getOrder<ProcessResource>(dataExplorer),
- filters: getFilters(dataExplorer, parentContainerRequest)
- });
-
-export const getFilters = (
- dataExplorer: DataExplorer,
- parentContainerRequest: ContainerRequestResource) => {
- const columns = dataExplorer.columns as DataColumns<string, ProcessResource>;
- const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
- const activeStatusFilter = Object.keys(statusColumnFilters).find(
- filterName => statusColumnFilters[filterName].selected
- ) || ProcessStatusFilter.ALL;
-
- // Get all the subprocess' container requests and containers.
- const fb = new FilterBuilder().addEqual('requesting_container_uuid', parentContainerRequest.containerUuid);
- const statusFilters = buildProcessStatusFilters(fb, activeStatusFilter).getFilters();
+ const process = getProcess(parentContainerRequestUuid)(state.resources);
+ if (!process?.container) { return null; }
- const nameFilters = dataExplorer.searchValue
- ? new FilterBuilder()
- .addILike("name", dataExplorer.searchValue)
- .getFilters()
- : '';
+ const requesting_container = new FilterBuilder().addEqual('requesting_container_uuid', process.container.uuid).getFilters();
+ const sup = super.getFilters(api, dataExplorer);
+ if (sup === null) { return null; }
- return joinFilters(
- nameFilters,
- statusFilters
- );
-};
-
-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
- });
+ return joinFilters(sup, requesting_container);
+ }
+}
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list