[arvados-workbench2] created: 2.6.2-1-g0213fdd8

git repository hosting git at public.arvados.org
Tue Sep 26 19:36:53 UTC 2023


        at  0213fdd808604f83ef79d0e0a415d8311e018a2f (commit)


commit 0213fdd808604f83ef79d0e0a415d8311e018a2f
Author: Lisa Knox <lisaknox83 at gmail.com>
Date:   Tue Sep 26 15:36:21 2023 -0400

    20382: cancel in context menu works for non-admins Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox at curii.com>

diff --git a/src/index.tsx b/src/index.tsx
index 244d1387..9339391e 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,71 +2,91 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React from 'react';
-import ReactDOM from 'react-dom';
+import React from "react";
+import ReactDOM from "react-dom";
 import { Provider } from "react-redux";
-import { MainPanel } from 'views/main-panel/main-panel';
-import 'index.css';
-import { Route, Switch } from 'react-router';
+import { MainPanel } from "views/main-panel/main-panel";
+import "index.css";
+import { Route, Switch } from "react-router";
 import { createBrowserHistory } from "history";
 import { History } from "history";
-import { configureStore, RootStore } from 'store/store';
+import { configureStore, RootStore } from "store/store";
 import { ConnectedRouter } from "react-router-redux";
 import { ApiToken } from "views-components/api-token/api-token";
 import { AddSession } from "views-components/add-session/add-session";
 import { initAuth, logout } from "store/auth/auth-action";
 import { createServices } from "services/services";
-import { MuiThemeProvider } from '@material-ui/core/styles';
-import { CustomTheme } from 'common/custom-theme';
-import { fetchConfig } from 'common/config';
-import servicesProvider from 'common/service-provider';
-import { addMenuActionSet, ContextMenuKind } from 'views-components/context-menu/context-menu';
+import { MuiThemeProvider } from "@material-ui/core/styles";
+import { CustomTheme } from "common/custom-theme";
+import { fetchConfig } from "common/config";
+import servicesProvider from "common/service-provider";
+import { addMenuActionSet, ContextMenuKind } from "views-components/context-menu/context-menu";
 import { rootProjectActionSet } from "views-components/context-menu/action-sets/root-project-action-set";
-import { filterGroupActionSet, frozenActionSet, projectActionSet, readOnlyProjectActionSet } from "views-components/context-menu/action-sets/project-action-set";
-import { resourceActionSet } from 'views-components/context-menu/action-sets/resource-action-set';
+import {
+    filterGroupActionSet,
+    frozenActionSet,
+    projectActionSet,
+    readOnlyProjectActionSet,
+} from "views-components/context-menu/action-sets/project-action-set";
+import { resourceActionSet } from "views-components/context-menu/action-sets/resource-action-set";
 import { favoriteActionSet } from "views-components/context-menu/action-sets/favorite-action-set";
-import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from 'views-components/context-menu/action-sets/collection-files-action-set';
-import { collectionDirectoryItemActionSet, collectionFileItemActionSet, readOnlyCollectionDirectoryItemActionSet, readOnlyCollectionFileItemActionSet } from 'views-components/context-menu/action-sets/collection-files-item-action-set';
-import { collectionFilesNotSelectedActionSet } from 'views-components/context-menu/action-sets/collection-files-not-selected-action-set';
-import { collectionActionSet, collectionAdminActionSet, oldCollectionVersionActionSet, readOnlyCollectionActionSet } from 'views-components/context-menu/action-sets/collection-action-set';
-import { loadWorkbench } from 'store/workbench/workbench-actions';
-import { Routes } from 'routes/routes';
+import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from "views-components/context-menu/action-sets/collection-files-action-set";
+import {
+    collectionDirectoryItemActionSet,
+    collectionFileItemActionSet,
+    readOnlyCollectionDirectoryItemActionSet,
+    readOnlyCollectionFileItemActionSet,
+} from "views-components/context-menu/action-sets/collection-files-item-action-set";
+import { collectionFilesNotSelectedActionSet } from "views-components/context-menu/action-sets/collection-files-not-selected-action-set";
+import {
+    collectionActionSet,
+    collectionAdminActionSet,
+    oldCollectionVersionActionSet,
+    readOnlyCollectionActionSet,
+} from "views-components/context-menu/action-sets/collection-action-set";
+import { loadWorkbench } from "store/workbench/workbench-actions";
+import { Routes } from "routes/routes";
 import { trashActionSet } from "views-components/context-menu/action-sets/trash-action-set";
-import { ServiceRepository } from 'services/services';
-import { initWebSocket } from 'websocket/websocket';
-import { Config } from 'common/config';
-import { addRouteChangeHandlers } from './routes/route-change-handlers';
-import { setTokenDialogApiHost } from 'store/token-dialog/token-dialog-actions';
+import { ServiceRepository } from "services/services";
+import { initWebSocket } from "websocket/websocket";
+import { Config } from "common/config";
+import { addRouteChangeHandlers } from "./routes/route-change-handlers";
+import { setTokenDialogApiHost } from "store/token-dialog/token-dialog-actions";
 import {
     processResourceActionSet,
+    runningProcessResourceActionSet,
     processResourceAdminActionSet,
-    readOnlyProcessResourceActionSet
-} from 'views-components/context-menu/action-sets/process-resource-action-set';
-import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
-import { trashedCollectionActionSet } from 'views-components/context-menu/action-sets/trashed-collection-action-set';
-import { setBuildInfo } from 'store/app-info/app-info-actions';
-import { getBuildInfo } from 'common/app-info';
-import { DragDropContextProvider } from 'react-dnd';
-import HTML5Backend from 'react-dnd-html5-backend';
-import { initAdvancedFormProjectsTree } from 'store/search-bar/search-bar-actions';
-import { repositoryActionSet } from 'views-components/context-menu/action-sets/repository-action-set';
-import { sshKeyActionSet } from 'views-components/context-menu/action-sets/ssh-key-action-set';
-import { keepServiceActionSet } from 'views-components/context-menu/action-sets/keep-service-action-set';
-import { loadVocabulary } from 'store/vocabulary/vocabulary-actions';
-import { virtualMachineActionSet } from 'views-components/context-menu/action-sets/virtual-machine-action-set';
-import { userActionSet } from 'views-components/context-menu/action-sets/user-action-set';
-import { apiClientAuthorizationActionSet } from 'views-components/context-menu/action-sets/api-client-authorization-action-set';
-import { groupActionSet } from 'views-components/context-menu/action-sets/group-action-set';
-import { groupMemberActionSet } from 'views-components/context-menu/action-sets/group-member-action-set';
-import { linkActionSet } from 'views-components/context-menu/action-sets/link-action-set';
-import { loadFileViewersConfig } from 'store/file-viewers/file-viewers-actions';
-import { filterGroupAdminActionSet, frozenAdminActionSet, projectAdminActionSet } from 'views-components/context-menu/action-sets/project-admin-action-set';
-import { permissionEditActionSet } from 'views-components/context-menu/action-sets/permission-edit-action-set';
-import { workflowActionSet } from 'views-components/context-menu/action-sets/workflow-action-set';
+    readOnlyProcessResourceActionSet,
+} from "views-components/context-menu/action-sets/process-resource-action-set";
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { trashedCollectionActionSet } from "views-components/context-menu/action-sets/trashed-collection-action-set";
+import { setBuildInfo } from "store/app-info/app-info-actions";
+import { getBuildInfo } from "common/app-info";
+import { DragDropContextProvider } from "react-dnd";
+import HTML5Backend from "react-dnd-html5-backend";
+import { initAdvancedFormProjectsTree } from "store/search-bar/search-bar-actions";
+import { repositoryActionSet } from "views-components/context-menu/action-sets/repository-action-set";
+import { sshKeyActionSet } from "views-components/context-menu/action-sets/ssh-key-action-set";
+import { keepServiceActionSet } from "views-components/context-menu/action-sets/keep-service-action-set";
+import { loadVocabulary } from "store/vocabulary/vocabulary-actions";
+import { virtualMachineActionSet } from "views-components/context-menu/action-sets/virtual-machine-action-set";
+import { userActionSet } from "views-components/context-menu/action-sets/user-action-set";
+import { apiClientAuthorizationActionSet } from "views-components/context-menu/action-sets/api-client-authorization-action-set";
+import { groupActionSet } from "views-components/context-menu/action-sets/group-action-set";
+import { groupMemberActionSet } from "views-components/context-menu/action-sets/group-member-action-set";
+import { linkActionSet } from "views-components/context-menu/action-sets/link-action-set";
+import { loadFileViewersConfig } from "store/file-viewers/file-viewers-actions";
+import {
+    filterGroupAdminActionSet,
+    frozenAdminActionSet,
+    projectAdminActionSet,
+} from "views-components/context-menu/action-sets/project-admin-action-set";
+import { permissionEditActionSet } from "views-components/context-menu/action-sets/permission-edit-action-set";
+import { workflowActionSet } from "views-components/context-menu/action-sets/workflow-action-set";
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action';
-import { storeRedirects } from './common/redirect-to';
-import { searchResultsActionSet } from 'views-components/context-menu/action-sets/search-results-action-set';
+import { openNotFoundDialog } from "./store/not-found-panel/not-found-panel-action";
+import { storeRedirects } from "./common/redirect-to";
+import { searchResultsActionSet } from "views-components/context-menu/action-sets/search-results-action-set";
 
 console.log(`Starting arvados [${getBuildInfo()}]`);
 
@@ -88,6 +108,7 @@ addMenuActionSet(ContextMenuKind.READONLY_COLLECTION, readOnlyCollectionActionSe
 addMenuActionSet(ContextMenuKind.OLD_VERSION_COLLECTION, oldCollectionVersionActionSet);
 addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
+addMenuActionSet(ContextMenuKind.RUNNING_PROCESS_RESOURCE, runningProcessResourceActionSet);
 addMenuActionSet(ContextMenuKind.READONLY_PROCESS_RESOURCE, readOnlyProcessResourceActionSet);
 addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
@@ -111,84 +132,106 @@ addMenuActionSet(ContextMenuKind.SEARCH_RESULTS, searchResultsActionSet);
 
 storeRedirects();
 
-fetchConfig()
-    .then(({ config, apiHost }) => {
-        const history = createBrowserHistory();
-
-        // Provide browser's history access to Cypress to allow programmatic
-        // navigation.
-        if ((window as any).Cypress) {
-            (window as any).appHistory = history;
-        }
-
-        const services = createServices(config, {
-            progressFn: (id, working) => {
-                store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
-            },
-            errorFn: (id, error, showSnackBar: boolean) => {
-                if (showSnackBar) {
-                    console.error("Backend error:", error);
-
-                    if (error.status === 404) {
-                        store.dispatch(openNotFoundDialog());
-                    } else if (error.status === 401 && error.errors[0].indexOf("Not logged in") > -1) {
-                        // Catch auth errors when navigating and redirect to login preserving url location
-                        store.dispatch(logout(false, true));
-                    } else {
-                        store.dispatch(snackbarActions.OPEN_SNACKBAR({
-                            message: `${error.errors
-                                ? error.errors[0]
-                                : error.message}`,
+fetchConfig().then(({ config, apiHost }) => {
+    const history = createBrowserHistory();
+
+    // Provide browser's history access to Cypress to allow programmatic
+    // navigation.
+    if ((window as any).Cypress) {
+        (window as any).appHistory = history;
+    }
+
+    const services = createServices(config, {
+        progressFn: (id, working) => {
+            store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
+        },
+        errorFn: (id, error, showSnackBar: boolean) => {
+            if (showSnackBar) {
+                console.error("Backend error:", error);
+
+                if (error.status === 404) {
+                    store.dispatch(openNotFoundDialog());
+                } else if (error.status === 401 && error.errors[0].indexOf("Not logged in") > -1) {
+                    // Catch auth errors when navigating and redirect to login preserving url location
+                    store.dispatch(logout(false, true));
+                } else {
+                    store.dispatch(
+                        snackbarActions.OPEN_SNACKBAR({
+                            message: `${error.errors ? error.errors[0] : error.message}`,
                             kind: SnackbarKind.ERROR,
-                            hideDuration: 8000
+                            hideDuration: 8000,
                         })
-                        );
-                    }
+                    );
                 }
             }
-        });
-
-        // be sure this is initiated before the app starts
-        servicesProvider.setServices(services);
-
-        const store = configureStore(history, services, config);
-
-        servicesProvider.setStore(store);
-
-        store.subscribe(initListener(history, store, services, config));
-        store.dispatch(initAuth(config));
-        store.dispatch(setBuildInfo());
-        store.dispatch(setTokenDialogApiHost(apiHost));
-        store.dispatch(loadVocabulary);
-        store.dispatch(loadFileViewersConfig);
-
-        const TokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={true} {...props} />;
-        const AddSessionComponent = (props: any) => <AddSession {...props} />;
-        const FedTokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={false} {...props} />;
-        const MainPanelComponent = (props: any) => <MainPanel {...props} />;
-
-        const App = () =>
-            <MuiThemeProvider theme={CustomTheme}>
-                <DragDropContextProvider backend={HTML5Backend}>
-                    <Provider store={store}>
-                        <ConnectedRouter history={history}>
-                            <Switch>
-                                <Route path={Routes.TOKEN} component={TokenComponent} />
-                                <Route path={Routes.FED_LOGIN} component={FedTokenComponent} />
-                                <Route path={Routes.ADD_SESSION} component={AddSessionComponent} />
-                                <Route path={Routes.ROOT} component={MainPanelComponent} />
-                            </Switch>
-                        </ConnectedRouter>
-                    </Provider>
-                </DragDropContextProvider>
-            </MuiThemeProvider>;
-
-        ReactDOM.render(
-            <App />,
-            document.getElementById('root') as HTMLElement
-        );
+        },
     });
 
+    // be sure this is initiated before the app starts
+    servicesProvider.setServices(services);
+
+    const store = configureStore(history, services, config);
+
+    servicesProvider.setStore(store);
+
+    store.subscribe(initListener(history, store, services, config));
+    store.dispatch(initAuth(config));
+    store.dispatch(setBuildInfo());
+    store.dispatch(setTokenDialogApiHost(apiHost));
+    store.dispatch(loadVocabulary);
+    store.dispatch(loadFileViewersConfig);
+
+    const TokenComponent = (props: any) => (
+        <ApiToken
+            authService={services.authService}
+            config={config}
+            loadMainApp={true}
+            {...props}
+        />
+    );
+    const AddSessionComponent = (props: any) => <AddSession {...props} />;
+    const FedTokenComponent = (props: any) => (
+        <ApiToken
+            authService={services.authService}
+            config={config}
+            loadMainApp={false}
+            {...props}
+        />
+    );
+    const MainPanelComponent = (props: any) => <MainPanel {...props} />;
+
+    const App = () => (
+        <MuiThemeProvider theme={CustomTheme}>
+            <DragDropContextProvider backend={HTML5Backend}>
+                <Provider store={store}>
+                    <ConnectedRouter history={history}>
+                        <Switch>
+                            <Route
+                                path={Routes.TOKEN}
+                                component={TokenComponent}
+                            />
+                            <Route
+                                path={Routes.FED_LOGIN}
+                                component={FedTokenComponent}
+                            />
+                            <Route
+                                path={Routes.ADD_SESSION}
+                                component={AddSessionComponent}
+                            />
+                            <Route
+                                path={Routes.ROOT}
+                                component={MainPanelComponent}
+                            />
+                        </Switch>
+                    </ConnectedRouter>
+                </Provider>
+            </DragDropContextProvider>
+        </MuiThemeProvider>
+    );
+
+    ReactDOM.render(<App />, document.getElementById("root") as HTMLElement);
+});
+
 const initListener = (history: History, store: RootStore, services: ServiceRepository, config: Config) => {
     let initialized = false;
     return async () => {
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 3bc91ae0..e3d708c7 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -2,31 +2,32 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { unionize, ofType, UnionOf } from 'common/unionize';
+import { unionize, ofType, UnionOf } from "common/unionize";
 import { ContextMenuPosition } from "./context-menu-reducer";
-import { ContextMenuKind } from 'views-components/context-menu/context-menu';
-import { Dispatch } from 'redux';
-import { RootState } from 'store/store';
-import { getResource, getResourceWithEditableStatus } from '../resources/resources';
-import { UserResource } from 'models/user';
-import { isSidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
-import { extractUuidKind, ResourceKind, EditableResource, Resource } from 'models/resource';
-import { Process } from 'store/processes/process';
-import { RepositoryResource } from 'models/repositories';
-import { SshKeyResource } from 'models/ssh-key';
-import { VirtualMachinesResource } from 'models/virtual-machines';
-import { KeepServiceResource } from 'models/keep-services';
-import { ProcessResource } from 'models/process';
-import { CollectionResource } from 'models/collection';
-import { GroupClass, GroupResource } from 'models/group';
-import { GroupContentsResource } from 'services/groups-service/groups-service';
-import { LinkResource } from 'models/link';
-import { resourceIsFrozen } from 'common/frozen-resources';
-import { ProjectResource } from 'models/project';
+import { ContextMenuKind } from "views-components/context-menu/context-menu";
+import { Dispatch } from "redux";
+import { RootState } from "store/store";
+import { getResource, getResourceWithEditableStatus } from "../resources/resources";
+import { UserResource } from "models/user";
+import { isSidePanelTreeCategory } from "store/side-panel-tree/side-panel-tree-actions";
+import { extractUuidKind, ResourceKind, EditableResource, Resource } from "models/resource";
+import { Process, isProcessCancelable } from "store/processes/process";
+import { RepositoryResource } from "models/repositories";
+import { SshKeyResource } from "models/ssh-key";
+import { VirtualMachinesResource } from "models/virtual-machines";
+import { KeepServiceResource } from "models/keep-services";
+import { ProcessResource } from "models/process";
+import { CollectionResource } from "models/collection";
+import { GroupClass, GroupResource } from "models/group";
+import { GroupContentsResource } from "services/groups-service/groups-service";
+import { LinkResource } from "models/link";
+import { resourceIsFrozen } from "common/frozen-resources";
+import { ProjectResource } from "models/project";
+import { getProcess } from "store/processes/process";
 
 export const contextMenuActions = unionize({
-    OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
-    CLOSE_CONTEXT_MENU: ofType<{}>()
+    OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition; resource: ContextMenuResource }>(),
+    CLOSE_CONTEXT_MENU: ofType<{}>(),
 });
 
 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
@@ -36,7 +37,7 @@ export type ContextMenuResource = {
     uuid: string;
     ownerUuid: string;
     description?: string;
-    kind: ResourceKind,
+    kind: ResourceKind;
     menuKind: ContextMenuKind | string;
     isTrashed?: boolean;
     isEditable?: boolean;
@@ -50,187 +51,221 @@ export type ContextMenuResource = {
 
 export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
 
-export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) =>
-    (dispatch: Dispatch) => {
-        event.preventDefault();
-        const { left, top } = event.currentTarget.getBoundingClientRect();
-        dispatch(
-            contextMenuActions.OPEN_CONTEXT_MENU({
-                position: {
-                    x: event.clientX || left,
-                    y: event.clientY || top,
-                },
-                resource
-            })
-        );
-    };
+export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) => (dispatch: Dispatch) => {
+    event.preventDefault();
+    const { left, top } = event.currentTarget.getBoundingClientRect();
+    dispatch(
+        contextMenuActions.OPEN_CONTEXT_MENU({
+            position: {
+                x: event.clientX || left,
+                y: event.clientY || top,
+            },
+            resource,
+        })
+    );
+};
 
-export const openCollectionFilesContextMenu = (event: React.MouseEvent<HTMLElement>, isWritable: boolean) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+export const openCollectionFilesContextMenu =
+    (event: React.MouseEvent<HTMLElement>, isWritable: boolean) => (dispatch: Dispatch, getState: () => RootState) => {
         const isCollectionFileSelected = JSON.stringify(getState().collectionPanelFiles).includes('"selected":true');
-        dispatch<any>(openContextMenu(event, {
-            name: '',
-            uuid: '',
-            ownerUuid: '',
-            description: '',
-            kind: ResourceKind.COLLECTION,
-            menuKind: isCollectionFileSelected
-                ? isWritable
-                    ? ContextMenuKind.COLLECTION_FILES
-                    : ContextMenuKind.READONLY_COLLECTION_FILES
-                : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED
-        }));
+        dispatch<any>(
+            openContextMenu(event, {
+                name: "",
+                uuid: "",
+                ownerUuid: "",
+                description: "",
+                kind: ResourceKind.COLLECTION,
+                menuKind: isCollectionFileSelected
+                    ? isWritable
+                        ? ContextMenuKind.COLLECTION_FILES
+                        : ContextMenuKind.READONLY_COLLECTION_FILES
+                    : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED,
+            })
+        );
     };
 
-export const openRepositoryContextMenu = (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        dispatch<any>(openContextMenu(event, {
-            name: '',
-            uuid: repository.uuid,
-            ownerUuid: repository.ownerUuid,
-            kind: ResourceKind.REPOSITORY,
-            menuKind: ContextMenuKind.REPOSITORY
-        }));
+export const openRepositoryContextMenu =
+    (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) => (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch<any>(
+            openContextMenu(event, {
+                name: "",
+                uuid: repository.uuid,
+                ownerUuid: repository.ownerUuid,
+                kind: ResourceKind.REPOSITORY,
+                menuKind: ContextMenuKind.REPOSITORY,
+            })
+        );
     };
 
-export const openVirtualMachinesContextMenu = (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        dispatch<any>(openContextMenu(event, {
-            name: '',
-            uuid: repository.uuid,
-            ownerUuid: repository.ownerUuid,
-            kind: ResourceKind.VIRTUAL_MACHINE,
-            menuKind: ContextMenuKind.VIRTUAL_MACHINE
-        }));
+export const openVirtualMachinesContextMenu =
+    (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) => (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch<any>(
+            openContextMenu(event, {
+                name: "",
+                uuid: repository.uuid,
+                ownerUuid: repository.ownerUuid,
+                kind: ResourceKind.VIRTUAL_MACHINE,
+                menuKind: ContextMenuKind.VIRTUAL_MACHINE,
+            })
+        );
     };
 
-export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) =>
-    (dispatch: Dispatch) => {
-        dispatch<any>(openContextMenu(event, {
-            name: '',
+export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) => (dispatch: Dispatch) => {
+    dispatch<any>(
+        openContextMenu(event, {
+            name: "",
             uuid: sshKey.uuid,
             ownerUuid: sshKey.ownerUuid,
             kind: ResourceKind.SSH_KEY,
-            menuKind: ContextMenuKind.SSH_KEY
-        }));
-    };
+            menuKind: ContextMenuKind.SSH_KEY,
+        })
+    );
+};
 
-export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) =>
-    (dispatch: Dispatch) => {
-        dispatch<any>(openContextMenu(event, {
-            name: '',
+export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) => (dispatch: Dispatch) => {
+    dispatch<any>(
+        openContextMenu(event, {
+            name: "",
             uuid: keepService.uuid,
             ownerUuid: keepService.ownerUuid,
             kind: ResourceKind.KEEP_SERVICE,
-            menuKind: ContextMenuKind.KEEP_SERVICE
-        }));
-    };
+            menuKind: ContextMenuKind.KEEP_SERVICE,
+        })
+    );
+};
 
-export const openApiClientAuthorizationContextMenu =
-    (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
-        (dispatch: Dispatch) => {
-            dispatch<any>(openContextMenu(event, {
-                name: '',
-                uuid: resourceUuid,
-                ownerUuid: '',
-                kind: ResourceKind.API_CLIENT_AUTHORIZATION,
-                menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION
-            }));
-        };
+export const openApiClientAuthorizationContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch) => {
+    dispatch<any>(
+        openContextMenu(event, {
+            name: "",
+            uuid: resourceUuid,
+            ownerUuid: "",
+            kind: ResourceKind.API_CLIENT_AUTHORIZATION,
+            menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION,
+        })
+    );
+};
 
-export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+export const openRootProjectContextMenu =
+    (event: React.MouseEvent<HTMLElement>, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
         const res = getResource<UserResource>(projectUuid)(getState().resources);
         if (res) {
-            dispatch<any>(openContextMenu(event, {
-                name: '',
-                uuid: res.uuid,
-                ownerUuid: res.uuid,
-                kind: res.kind,
-                menuKind: ContextMenuKind.ROOT_PROJECT,
-                isTrashed: false
-            }));
+            dispatch<any>(
+                openContextMenu(event, {
+                    name: "",
+                    uuid: res.uuid,
+                    ownerUuid: res.uuid,
+                    kind: res.kind,
+                    menuKind: ContextMenuKind.ROOT_PROJECT,
+                    isTrashed: false,
+                })
+            );
         }
     };
 
-export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+export const openProjectContextMenu =
+    (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
         const res = getResource<GroupContentsResource>(resourceUuid)(getState().resources);
         const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
         if (res && menuKind) {
-            dispatch<any>(openContextMenu(event, {
-                name: res.name,
-                uuid: res.uuid,
-                kind: res.kind,
-                menuKind,
-                description: res.description,
-                ownerUuid: res.ownerUuid,
-                isTrashed: ('isTrashed' in res) ? res.isTrashed : false,
-                isFrozen: !!(res as ProjectResource).frozenByUuid,
-            }));
+            dispatch<any>(
+                openContextMenu(event, {
+                    name: res.name,
+                    uuid: res.uuid,
+                    kind: res.kind,
+                    menuKind,
+                    description: res.description,
+                    ownerUuid: res.ownerUuid,
+                    isTrashed: "isTrashed" in res ? res.isTrashed : false,
+                    isFrozen: !!(res as ProjectResource).frozenByUuid,
+                })
+            );
         }
     };
 
-export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        if (!isSidePanelTreeCategory(id)) {
-            const kind = extractUuidKind(id);
-            if (kind === ResourceKind.USER) {
-                dispatch<any>(openRootProjectContextMenu(event, id));
-            } else if (kind === ResourceKind.PROJECT) {
-                dispatch<any>(openProjectContextMenu(event, id));
-            }
+export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) => (dispatch: Dispatch, getState: () => RootState) => {
+    if (!isSidePanelTreeCategory(id)) {
+        const kind = extractUuidKind(id);
+        if (kind === ResourceKind.USER) {
+            dispatch<any>(openRootProjectContextMenu(event, id));
+        } else if (kind === ResourceKind.PROJECT) {
+            dispatch<any>(openProjectContextMenu(event, id));
         }
-    };
+    }
+};
 
-export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
-        if (res) {
-            dispatch<any>(openContextMenu(event, {
+export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) => (dispatch: Dispatch, getState: () => RootState) => {
+    const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
+    if (res) {
+        dispatch<any>(
+            openContextMenu(event, {
                 uuid: res.uuid,
                 ownerUuid: res.ownerUuid,
                 kind: ResourceKind.PROCESS,
                 name: res.name,
                 description: res.description,
-                outputUuid: res.outputUuid || '',
-                workflowUuid: res.properties.template_uuid || '',
-                menuKind: ContextMenuKind.PROCESS_RESOURCE
-            }));
+                outputUuid: res.outputUuid || "",
+                workflowUuid: res.properties.template_uuid || "",
+                menuKind: ContextMenuKind.PROCESS_RESOURCE,
+            })
+        );
+    }
+};
+
+export const openRunningProcessContextMenu =
+    (event: React.MouseEvent<HTMLElement>, process: Process) => (dispatch: Dispatch, getState: () => RootState) => {
+        const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
+        if (res) {
+            dispatch<any>(
+                openContextMenu(event, {
+                    uuid: res.uuid,
+                    ownerUuid: res.ownerUuid,
+                    kind: ResourceKind.PROCESS,
+                    name: res.name,
+                    description: res.description,
+                    outputUuid: res.outputUuid || "",
+                    workflowUuid: res.properties.template_uuid || "",
+                    menuKind: ContextMenuKind.RUNNING_PROCESS_RESOURCE,
+                })
+            );
         }
     };
 
-export const openPermissionEditContextMenu = (event: React.MouseEvent<HTMLElement>, link: LinkResource) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+export const openPermissionEditContextMenu =
+    (event: React.MouseEvent<HTMLElement>, link: LinkResource) => (dispatch: Dispatch, getState: () => RootState) => {
         if (link) {
-            dispatch<any>(openContextMenu(event, {
-                name: link.name,
-                uuid: link.uuid,
-                kind: link.kind,
-                menuKind: ContextMenuKind.PERMISSION_EDIT,
-                ownerUuid: link.ownerUuid,
-            }));
+            dispatch<any>(
+                openContextMenu(event, {
+                    name: link.name,
+                    uuid: link.uuid,
+                    kind: link.kind,
+                    menuKind: ContextMenuKind.PERMISSION_EDIT,
+                    ownerUuid: link.ownerUuid,
+                })
+            );
         }
     };
 
-export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        dispatch<any>(openContextMenu(event, {
-            name: '',
+export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) => (dispatch: Dispatch, getState: () => RootState) => {
+    dispatch<any>(
+        openContextMenu(event, {
+            name: "",
             uuid: user.uuid,
             ownerUuid: user.ownerUuid,
             kind: user.kind,
-            menuKind: ContextMenuKind.USER
-        }));
-    };
+            menuKind: ContextMenuKind.USER,
+        })
+    );
+};
 
-export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
+export const resourceUuidToContextMenuKind =
+    (uuid: string, readonly = false) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
         const kind = extractUuidKind(uuid);
         const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
         const isFrozen = resourceIsFrozen(resource, getState().resources);
-        const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly && !isFrozen;
+        const isEditable = (isAdminUser || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
 
         switch (kind) {
             case ResourceKind.PROJECT:
@@ -238,35 +273,39 @@ export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
                     return isAdminUser ? ContextMenuKind.FROZEN_PROJECT_ADMIN : ContextMenuKind.FROZEN_PROJECT;
                 }
 
-                return (isAdminUser && !readonly)
-                    ? (resource && resource.groupClass !== GroupClass.FILTER)
+                return isAdminUser && !readonly
+                    ? resource && resource.groupClass !== GroupClass.FILTER
                         ? ContextMenuKind.PROJECT_ADMIN
                         : ContextMenuKind.FILTER_GROUP_ADMIN
                     : isEditable
-                        ? (resource && resource.groupClass !== GroupClass.FILTER)
-                            ? ContextMenuKind.PROJECT
-                            : ContextMenuKind.FILTER_GROUP
-                        : ContextMenuKind.READONLY_PROJECT;
+                    ? resource && resource.groupClass !== GroupClass.FILTER
+                        ? ContextMenuKind.PROJECT
+                        : ContextMenuKind.FILTER_GROUP
+                    : ContextMenuKind.READONLY_PROJECT;
             case ResourceKind.COLLECTION:
                 const c = getResource<CollectionResource>(uuid)(getState().resources);
-                if (c === undefined) { return; }
+                if (c === undefined) {
+                    return;
+                }
                 const isOldVersion = c.uuid !== c.currentVersionUuid;
                 const isTrashed = c.isTrashed;
                 return isOldVersion
                     ? ContextMenuKind.OLD_VERSION_COLLECTION
-                    : (isTrashed && isEditable)
-                        ? ContextMenuKind.TRASHED_COLLECTION
-                        : (isAdminUser && isEditable)
-                            ? ContextMenuKind.COLLECTION_ADMIN
-                            : isEditable
-                                ? ContextMenuKind.COLLECTION
-                                : ContextMenuKind.READONLY_COLLECTION;
+                    : isTrashed && isEditable
+                    ? ContextMenuKind.TRASHED_COLLECTION
+                    : isAdminUser && isEditable
+                    ? ContextMenuKind.COLLECTION_ADMIN
+                    : isEditable
+                    ? ContextMenuKind.COLLECTION
+                    : ContextMenuKind.READONLY_COLLECTION;
             case ResourceKind.PROCESS:
-                return (isAdminUser && isEditable)
+                return isAdminUser && isEditable
                     ? ContextMenuKind.PROCESS_ADMIN
                     : readonly
-                        ? ContextMenuKind.READONLY_PROCESS_RESOURCE
-                        : ContextMenuKind.PROCESS_RESOURCE;
+                    ? ContextMenuKind.READONLY_PROCESS_RESOURCE
+                    : resource && isProcessCancelable(getProcess(resource.uuid)(getState().resources) as Process)
+                    ? ContextMenuKind.RUNNING_PROCESS_RESOURCE
+                    : ContextMenuKind.PROCESS_RESOURCE;
             case ResourceKind.USER:
                 return ContextMenuKind.ROOT_PROJECT;
             case ResourceKind.LINK:
@@ -278,16 +317,18 @@ export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
         }
     };
 
-export const openSearchResultsContextMenu = (event: React.MouseEvent<HTMLElement>, uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+export const openSearchResultsContextMenu =
+    (event: React.MouseEvent<HTMLElement>, uuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
         const res = getResource<Resource>(uuid)(getState().resources);
         if (res) {
-            dispatch<any>(openContextMenu(event, {
-                name: '',
-                uuid: res.uuid,
-                ownerUuid: '',
-                kind: res.kind,
-                menuKind: ContextMenuKind.SEARCH_RESULTS,
-            }));
+            dispatch<any>(
+                openContextMenu(event, {
+                    name: "",
+                    uuid: res.uuid,
+                    ownerUuid: "",
+                    kind: res.kind,
+                    menuKind: ContextMenuKind.SEARCH_RESULTS,
+                })
+            );
         }
     };
diff --git a/src/views-components/context-menu/action-sets/process-resource-action-set.ts b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
index 7d593ee4..579161d6 100644
--- a/src/views-components/context-menu/action-sets/process-resource-action-set.ts
+++ b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
@@ -6,114 +6,147 @@ import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "store/favorites/favorites-actions";
 import {
-    RenameIcon, ShareIcon, MoveToIcon, DetailsIcon,
-    RemoveIcon, ReRunProcessIcon, OutputIcon,
+    RenameIcon,
+    ShareIcon,
+    MoveToIcon,
+    DetailsIcon,
+    RemoveIcon,
+    ReRunProcessIcon,
+    OutputIcon,
     AdvancedIcon,
-    OpenIcon
+    OpenIcon,
+    StopIcon,
 } from "components/icon/icon";
 import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
-import { openMoveProcessDialog } from 'store/processes/process-move-actions';
+import { openMoveProcessDialog } from "store/processes/process-move-actions";
 import { openProcessUpdateDialog } from "store/processes/process-update-actions";
-import { openCopyProcessDialog } from 'store/processes/process-copy-actions';
+import { openCopyProcessDialog } from "store/processes/process-copy-actions";
 import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
 import { openRemoveProcessDialog } from "store/processes/processes-actions";
-import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+import { toggleDetailsPanel } from "store/details-panel/details-panel-action";
 import { navigateToOutput } from "store/process-panel/process-panel-actions";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
 import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
 import { publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
 import { openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+import { cancelRunningWorkflow } from "store/processes/processes-actions";
 
-export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
-    {
-        component: ToggleFavoriteAction,
-        execute: (dispatch, resource) => {
-            dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
-    {
-        icon: OpenIcon,
-        name: "Open in new tab",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openInNewTabAction(resource));
-        }
-    },
-    {
-        icon: ReRunProcessIcon,
-        name: "Copy and re-run process",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCopyProcessDialog(resource));
-        }
-    },
-    {
-        icon: OutputIcon,
-        name: "Outputs",
-        execute: (dispatch, resource) => {
-            if(resource.outputUuid){
-                dispatch<any>(navigateToOutput(resource.outputUuid));
-            }
-        }
-    },
-    {
-        icon: DetailsIcon,
-        name: "View details",
-        execute: dispatch => {
-            dispatch<any>(toggleDetailsPanel());
-        }
-    },
-    {
-        icon: AdvancedIcon,
-        name: "API Details",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openAdvancedTabDialog(resource.uuid));
-        }
-    },
-]];
+export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [
+    [
+        {
+            component: ToggleFavoriteAction,
+            execute: (dispatch, resource) => {
+                dispatch<any>(toggleFavorite(resource)).then(() => {
+                    dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+                });
+            },
+        },
+        {
+            icon: OpenIcon,
+            name: "Open in new tab",
+            execute: (dispatch, resource) => {
+                dispatch<any>(openInNewTabAction(resource));
+            },
+        },
+        {
+            icon: ReRunProcessIcon,
+            name: "Copy and re-run process",
+            execute: (dispatch, resource) => {
+                dispatch<any>(openCopyProcessDialog(resource));
+            },
+        },
+        {
+            icon: OutputIcon,
+            name: "Outputs",
+            execute: (dispatch, resource) => {
+                if (resource.outputUuid) {
+                    dispatch<any>(navigateToOutput(resource.outputUuid));
+                }
+            },
+        },
+        {
+            icon: DetailsIcon,
+            name: "View details",
+            execute: dispatch => {
+                dispatch<any>(toggleDetailsPanel());
+            },
+        },
+        {
+            icon: AdvancedIcon,
+            name: "API Details",
+            execute: (dispatch, resource) => {
+                dispatch<any>(openAdvancedTabDialog(resource.uuid));
+            },
+        },
+    ],
+];
 
-export const processResourceActionSet: ContextMenuActionSet = [[
-    ...readOnlyProcessResourceActionSet.reduce((prev, next) => prev.concat(next), []),
-    {
-        icon: RenameIcon,
-        name: "Edit process",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProcessUpdateDialog(resource));
-        }
-    },
-    {
-        icon: ShareIcon,
-        name: "Share",
-        execute: (dispatch, { uuid }) => {
-            dispatch<any>(openSharingDialog(uuid));
-        }
-    },
-    {
-        icon: MoveToIcon,
-        name: "Move to",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openMoveProcessDialog(resource));
-        }
-    },
-    {
-        name: "Remove",
-        icon: RemoveIcon,
-        execute: (dispatch, resource) => {
-            dispatch<any>(openRemoveProcessDialog(resource.uuid));
-        }
-    }
-]];
+export const processResourceActionSet: ContextMenuActionSet = [
+    [
+        ...readOnlyProcessResourceActionSet.reduce((prev, next) => prev.concat(next), []),
+        {
+            icon: RenameIcon,
+            name: "Edit process",
+            execute: (dispatch, resource) => {
+                dispatch<any>(openProcessUpdateDialog(resource));
+            },
+        },
+        {
+            icon: ShareIcon,
+            name: "Share",
+            execute: (dispatch, { uuid }) => {
+                dispatch<any>(openSharingDialog(uuid));
+            },
+        },
+        {
+            icon: MoveToIcon,
+            name: "Move to",
+            execute: (dispatch, resource) => {
+                dispatch<any>(openMoveProcessDialog(resource));
+            },
+        },
+        {
+            name: "Remove",
+            icon: RemoveIcon,
+            execute: (dispatch, resource) => {
+                dispatch<any>(openRemoveProcessDialog(resource.uuid));
+            },
+        },
+        // {
+        //     name: "Cancel",
+        //     icon: StopIcon,
+        //     execute: (dispatch, resource) => {
+        //         dispatch<any>(cancelRunningWorkflow(resource.uuid));
+        //     },
+        // },
+    ],
+];
 
-export const processResourceAdminActionSet: ContextMenuActionSet = [[
-    ...processResourceActionSet.reduce((prev, next) => prev.concat(next), []),
-    {
-        component: TogglePublicFavoriteAction,
-        name: "Add to public favorites",
-        execute: (dispatch, resource) => {
-            dispatch<any>(togglePublicFavorite(resource)).then(() => {
-                dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
-            });
-        }
-    },
-]];
+export const runningProcessResourceActionSet = [
+    [
+        ...processResourceActionSet.reduce((prev, next) => prev.concat(next), []),
+        {
+            name: "Cancel",
+            icon: StopIcon,
+            execute: (dispatch, resource) => {
+                dispatch(cancelRunningWorkflow(resource.uuid));
+            },
+        },
+    ],
+];
+
+export const processResourceAdminActionSet: ContextMenuActionSet = [
+    [
+        ...processResourceActionSet.reduce((prev, next) => prev.concat(next), []),
+        {
+            component: TogglePublicFavoriteAction,
+            name: "Add to public favorites",
+            execute: (dispatch, resource) => {
+                dispatch<any>(togglePublicFavorite(resource)).then(() => {
+                    dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
+                });
+            },
+        },
+    ],
+];
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index c659b7c5..a61690c6 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -9,26 +9,28 @@ import { ContextMenu as ContextMenuComponent, ContextMenuProps, ContextMenuItem
 import { createAnchorAt } from "components/popover/helpers";
 import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
 import { Dispatch } from "redux";
-import { memoize } from 'lodash';
+import { memoize } from "lodash";
 import { sortByProperty } from "common/array-utils";
 type DataProps = Pick<ContextMenuProps, "anchorEl" | "items" | "open"> & { resource?: ContextMenuResource };
 const mapStateToProps = (state: RootState): DataProps => {
     const { open, position, resource } = state.contextMenu;
 
-    const filteredItems = getMenuActionSet(resource).map((group) => (group.filter((item) => {
-        if (resource && item.filters) {
-            // Execute all filters on this item, every returns true IFF all filters return true
-            return item.filters.every((filter) => filter(state, resource));
-        } else {
-            return true;
-        }
-    })));
+    const filteredItems = getMenuActionSet(resource).map(group =>
+        group.filter(item => {
+            if (resource && item.filters) {
+                // Execute all filters on this item, every returns true IFF all filters return true
+                return item.filters.every(filter => filter(state, resource));
+            } else {
+                return true;
+            }
+        })
+    );
 
     return {
         anchorEl: resource ? createAnchorAt(position) : undefined,
         items: filteredItems,
         open,
-        resource
+        resource,
     };
 };
 
@@ -42,11 +44,11 @@ const mapDispatchToProps = (dispatch: Dispatch): ActionProps => ({
         if (resource) {
             action.execute(dispatch, resource);
         }
-    }
+    },
 });
 
 const handleItemClick = memoize(
-    (resource: DataProps['resource'], onItemClick: ActionProps['onItemClick']): ContextMenuProps['onItemClick'] =>
+    (resource: DataProps["resource"], onItemClick: ActionProps["onItemClick"]): ContextMenuProps["onItemClick"] =>
         item => {
             onItemClick(item, resource);
         }
@@ -55,32 +57,30 @@ const handleItemClick = memoize(
 const mergeProps = ({ resource, ...dataProps }: DataProps, actionProps: ActionProps): ContextMenuProps => ({
     ...dataProps,
     ...actionProps,
-    onItemClick: handleItemClick(resource, actionProps.onItemClick)
+    onItemClick: handleItemClick(resource, actionProps.onItemClick),
 });
 
-
 export const ContextMenu = connect(mapStateToProps, mapDispatchToProps, mergeProps)(ContextMenuComponent);
 
 const menuActionSets = new Map<string, ContextMenuActionSet>();
 
 export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) => {
-    const sorted = itemSet.map(items => items.sort(sortByProperty('name')));
+    const sorted = itemSet.map(items => items.sort(sortByProperty("name")));
     menuActionSets.set(name, sorted);
 };
 
 const emptyActionSet: ContextMenuActionSet = [];
-const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => (
-    resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet
-);
+const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet =>
+    resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet;
 
 export enum ContextMenuKind {
     API_CLIENT_AUTHORIZATION = "ApiClientAuthorization",
     ROOT_PROJECT = "RootProject",
     PROJECT = "Project",
     FILTER_GROUP = "FilterGroup",
-    READONLY_PROJECT = 'ReadOnlyProject',
-    FROZEN_PROJECT = 'FrozenProject',
-    FROZEN_PROJECT_ADMIN = 'FrozenProjectAdmin',
+    READONLY_PROJECT = "ReadOnlyProject",
+    FROZEN_PROJECT = "FrozenProject",
+    FROZEN_PROJECT_ADMIN = "FrozenProjectAdmin",
     PROJECT_ADMIN = "ProjectAdmin",
     FILTER_GROUP_ADMIN = "FilterGroupAdmin",
     RESOURCE = "Resource",
@@ -93,15 +93,16 @@ export enum ContextMenuKind {
     READONLY_COLLECTION_FILE_ITEM = "ReadOnlyCollectionFileItem",
     READONLY_COLLECTION_DIRECTORY_ITEM = "ReadOnlyCollectionDirectoryItem",
     COLLECTION_FILES_NOT_SELECTED = "CollectionFilesNotSelected",
-    COLLECTION = 'Collection',
-    COLLECTION_ADMIN = 'CollectionAdmin',
-    READONLY_COLLECTION = 'ReadOnlyCollection',
-    OLD_VERSION_COLLECTION = 'OldVersionCollection',
-    TRASHED_COLLECTION = 'TrashedCollection',
+    COLLECTION = "Collection",
+    COLLECTION_ADMIN = "CollectionAdmin",
+    READONLY_COLLECTION = "ReadOnlyCollection",
+    OLD_VERSION_COLLECTION = "OldVersionCollection",
+    TRASHED_COLLECTION = "TrashedCollection",
     PROCESS = "Process",
-    PROCESS_ADMIN = 'ProcessAdmin',
-    PROCESS_RESOURCE = 'ProcessResource',
-    READONLY_PROCESS_RESOURCE = 'ReadOnlyProcessResource',
+    PROCESS_ADMIN = "ProcessAdmin",
+    RUNNING_PROCESS_RESOURCE = "RunningProcessResource",
+    PROCESS_RESOURCE = "ProcessResource",
+    READONLY_PROCESS_RESOURCE = "ReadOnlyProcessResource",
     PROCESS_LOGS = "ProcessLogs",
     REPOSITORY = "Repository",
     SSH_KEY = "SshKey",
@@ -113,5 +114,5 @@ export enum ContextMenuKind {
     PERMISSION_EDIT = "PermissionEdit",
     LINK = "Link",
     WORKFLOW = "Workflow",
-    SEARCH_RESULTS = "SearchResults"
+    SEARCH_RESULTS = "SearchResults",
 }
diff --git a/src/views/all-processes-panel/all-processes-panel.tsx b/src/views/all-processes-panel/all-processes-panel.tsx
index 4914da62..6d612e75 100644
--- a/src/views/all-processes-panel/all-processes-panel.tsx
+++ b/src/views/all-processes-panel/all-processes-panel.tsx
@@ -2,49 +2,49 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import React from "react";
+import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
-import { connect, DispatchProp } from 'react-redux';
-import { DataColumns } from 'components/data-table/data-table';
-import { RouteComponentProps } from 'react-router';
-import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
-import { SortDirection } from 'components/data-table/data-column';
-import { ResourceKind } from 'models/resource';
-import { ArvadosTheme } from 'common/custom-theme';
-import { ALL_PROCESSES_PANEL_ID } from 'store/all-processes-panel/all-processes-panel-action';
+import { connect, DispatchProp } from "react-redux";
+import { DataColumns } from "components/data-table/data-table";
+import { RouteComponentProps } from "react-router";
+import { DataTableFilterItem } from "components/data-table-filters/data-table-filters";
+import { SortDirection } from "components/data-table/data-column";
+import { ResourceKind } from "models/resource";
+import { ArvadosTheme } from "common/custom-theme";
+import { ALL_PROCESSES_PANEL_ID } from "store/all-processes-panel/all-processes-panel-action";
 import {
     ProcessStatus,
     ResourceName,
     ResourceOwnerWithName,
     ResourceType,
     ContainerRunTime,
-    ResourceCreatedAtDate
-} from 'views-components/data-explorer/renderers';
-import { ProcessIcon } from 'components/icon/icon';
-import { openProcessContextMenu } from 'store/context-menu/context-menu-actions';
-import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
-import { navigateTo } from 'store/navigation/navigation-action';
+    ResourceCreatedAtDate,
+} from "views-components/data-explorer/renderers";
+import { ProcessIcon } from "components/icon/icon";
+import { openProcessContextMenu, openRunningProcessContextMenu } from "store/context-menu/context-menu-actions";
+import { loadDetailsPanel } from "store/details-panel/details-panel-action";
+import { navigateTo } from "store/navigation/navigation-action";
 import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
-import { RootState } from 'store/store';
-import { createTree } from 'models/tree';
-import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from 'store/resource-type-filters/resource-type-filters';
-import { getProcess } from 'store/processes/process';
-import { ResourcesState } from 'store/resources/resources';
+import { RootState } from "store/store";
+import { createTree } from "models/tree";
+import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from "store/resource-type-filters/resource-type-filters";
+import { getProcess, isProcessCancelable } from "store/processes/process";
+import { ResourcesState } from "store/resources/resources";
 
 type CssRules = "toolbar" | "button" | "root";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toolbar: {
         paddingBottom: theme.spacing.unit * 3,
-        textAlign: "right"
+        textAlign: "right",
     },
     button: {
-        marginLeft: theme.spacing.unit
+        marginLeft: theme.spacing.unit,
     },
     root: {
-        width: '100%',
-    }
+        width: "100%",
+    },
 });
 
 export enum AllProcessesPanelColumnNames {
@@ -53,7 +53,7 @@ export enum AllProcessesPanelColumnNames {
     TYPE = "Type",
     OWNER = "Owner",
     CREATED_AT = "Created at",
-    RUNTIME = "Run Time"
+    RUNTIME = "Run Time",
 }
 
 export interface AllProcessesPanelFilter extends DataTableFilterItem {
@@ -65,9 +65,9 @@ export const allProcessesPanelColumns: DataColumns<string, ContainerRequestResou
         name: AllProcessesPanelColumnNames.NAME,
         selected: true,
         configurable: true,
-        sort: {direction: SortDirection.NONE, field: "name"},
+        sort: { direction: SortDirection.NONE, field: "name" },
         filters: createTree(),
-        render: uuid => <ResourceName uuid={uuid} />
+        render: uuid => <ResourceName uuid={uuid} />,
     },
     {
         name: AllProcessesPanelColumnNames.STATUS,
@@ -75,37 +75,37 @@ export const allProcessesPanelColumns: DataColumns<string, ContainerRequestResou
         configurable: true,
         mutuallyExclusiveFilters: true,
         filters: getInitialProcessStatusFilters(),
-        render: uuid => <ProcessStatus uuid={uuid} />
+        render: uuid => <ProcessStatus uuid={uuid} />,
     },
     {
         name: AllProcessesPanelColumnNames.TYPE,
         selected: true,
         configurable: true,
         filters: getInitialProcessTypeFilters(),
-        render: uuid => <ResourceType uuid={uuid} />
+        render: uuid => <ResourceType uuid={uuid} />,
     },
     {
         name: AllProcessesPanelColumnNames.OWNER,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceOwnerWithName uuid={uuid} />
+        render: uuid => <ResourceOwnerWithName uuid={uuid} />,
     },
     {
         name: AllProcessesPanelColumnNames.CREATED_AT,
         selected: true,
         configurable: true,
-        sort: {direction: SortDirection.DESC, field: "createdAt"},
+        sort: { direction: SortDirection.DESC, field: "createdAt" },
         filters: createTree(),
-        render: uuid => <ResourceCreatedAtDate uuid={uuid} />
+        render: uuid => <ResourceCreatedAtDate uuid={uuid} />,
     },
     {
         name: AllProcessesPanelColumnNames.RUNTIME,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ContainerRunTime uuid={uuid} />
-    }
+        render: uuid => <ContainerRunTime uuid={uuid} />,
+    },
 ];
 
 interface AllProcessesPanelDataProps {
@@ -117,42 +117,51 @@ interface AllProcessesPanelActionProps {
     onDialogOpen: (ownerUuid: string) => void;
     onItemDoubleClick: (item: string) => void;
 }
-const mapStateToProps = (state : RootState): AllProcessesPanelDataProps => ({
-    resources: state.resources
+const mapStateToProps = (state: RootState): AllProcessesPanelDataProps => ({
+    resources: state.resources,
 });
 
-type AllProcessesPanelProps = AllProcessesPanelDataProps & AllProcessesPanelActionProps & DispatchProp
-    & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+type AllProcessesPanelProps = AllProcessesPanelDataProps &
+    AllProcessesPanelActionProps &
+    DispatchProp &
+    WithStyles<CssRules> &
+    RouteComponentProps<{ id: string }>;
 
 export const AllProcessesPanel = withStyles(styles)(
     connect(mapStateToProps)(
         class extends React.Component<AllProcessesPanelProps> {
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
                 const process = getProcess(resourceUuid)(this.props.resources);
-                if (process) {
+                if (process && isProcessCancelable(process)) {
+                    this.props.dispatch<any>(openRunningProcessContextMenu(event, process));
+                } else if (process) {
                     this.props.dispatch<any>(openProcessContextMenu(event, process));
                 }
                 this.props.dispatch<any>(loadDetailsPanel(resourceUuid));
-            }
+            };
 
             handleRowDoubleClick = (uuid: string) => {
                 this.props.dispatch<any>(navigateTo(uuid));
-            }
+            };
 
             handleRowClick = (uuid: string) => {
                 this.props.dispatch<any>(loadDetailsPanel(uuid));
-            }
+            };
 
             render() {
-                return <div className={this.props.classes.root}><DataExplorer
-                    id={ALL_PROCESSES_PANEL_ID}
-                    onRowClick={this.handleRowClick}
-                    onRowDoubleClick={this.handleRowDoubleClick}
-                    onContextMenu={this.handleContextMenu}
-                    contextMenuColumn={true}
-                    defaultViewIcon={ProcessIcon}
-                    defaultViewMessages={['Processes list empty.']} />
-                </div>
+                return (
+                    <div className={this.props.classes.root}>
+                        <DataExplorer
+                            id={ALL_PROCESSES_PANEL_ID}
+                            onRowClick={this.handleRowClick}
+                            onRowDoubleClick={this.handleRowDoubleClick}
+                            onContextMenu={this.handleContextMenu}
+                            contextMenuColumn={true}
+                            defaultViewIcon={ProcessIcon}
+                            defaultViewMessages={["Processes list empty."]}
+                        />
+                    </div>
+                );
             }
         }
     )
diff --git a/src/views/process-panel/process-panel.tsx b/src/views/process-panel/process-panel.tsx
index 9dcb72cf..d3d8caf2 100644
--- a/src/views/process-panel/process-panel.tsx
+++ b/src/views/process-panel/process-panel.tsx
@@ -2,35 +2,29 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { RootState } from 'store/store';
-import { connect } from 'react-redux';
-import { getProcess, getSubprocesses, Process, getProcessStatus } from 'store/processes/process';
-import { Dispatch } from 'redux';
-import { openProcessContextMenu } from 'store/context-menu/context-menu-actions';
-import {
-    ProcessPanelRootDataProps,
-    ProcessPanelRootActionProps,
-    ProcessPanelRoot
-} from './process-panel-root';
-import {
-    getProcessPanelCurrentUuid,
-    ProcessPanel as ProcessPanelState
-} from 'store/process-panel/process-panel';
-import { groupBy } from 'lodash';
+import { RootState } from "store/store";
+import { connect } from "react-redux";
+import { getProcess, getSubprocesses, Process, getProcessStatus } from "store/processes/process";
+import { Dispatch } from "redux";
+import { openProcessContextMenu, openRunningProcessContextMenu } from "store/context-menu/context-menu-actions";
+import { ProcessPanelRootDataProps, ProcessPanelRootActionProps, ProcessPanelRoot } from "./process-panel-root";
+import { getProcessPanelCurrentUuid, ProcessPanel as ProcessPanelState } from "store/process-panel/process-panel";
+import { groupBy } from "lodash";
 import {
     loadInputs,
     loadOutputDefinitions,
     loadOutputs,
     toggleProcessPanelFilter,
     updateOutputParams,
-    loadNodeJson
-} from 'store/process-panel/process-panel-actions';
-import { cancelRunningWorkflow, resumeOnHoldWorkflow, startWorkflow } from 'store/processes/processes-actions';
-import { navigateToLogCollection, setProcessLogsPanelFilter } from 'store/process-logs-panel/process-logs-panel-actions';
-import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+    loadNodeJson,
+} from "store/process-panel/process-panel-actions";
+import { cancelRunningWorkflow, resumeOnHoldWorkflow, startWorkflow } from "store/processes/processes-actions";
+import { navigateToLogCollection, setProcessLogsPanelFilter } from "store/process-logs-panel/process-logs-panel-actions";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { isProcessCancelable } from "store/processes/process";
 
 const mapStateToProps = ({ router, auth, resources, processPanel, processLogsPanel }: RootState): ProcessPanelRootDataProps => {
-    const uuid = getProcessPanelCurrentUuid(router) || '';
+    const uuid = getProcessPanelCurrentUuid(router) || "";
     const subprocesses = getSubprocesses(uuid)(resources);
     return {
         process: getProcess(uuid)(resources),
@@ -49,40 +43,45 @@ const mapStateToProps = ({ router, auth, resources, processPanel, processLogsPan
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
     onCopyToClipboard: (message: string) => {
-        dispatch<any>(snackbarActions.OPEN_SNACKBAR({
-            message,
-            hideDuration: 2000,
-            kind: SnackbarKind.SUCCESS,
-        }));
+        dispatch<any>(
+            snackbarActions.OPEN_SNACKBAR({
+                message,
+                hideDuration: 2000,
+                kind: SnackbarKind.SUCCESS,
+            })
+        );
     },
     onContextMenu: (event, process) => {
-        dispatch<any>(openProcessContextMenu(event, process));
+        // dispatch<any>(openProcessContextMenu(event, process));
+        if (process && isProcessCancelable(process)) {
+            dispatch<any>(openRunningProcessContextMenu(event, process));
+        } else if (process) {
+            dispatch<any>(openProcessContextMenu(event, process));
+        }
     },
     onToggle: status => {
         dispatch<any>(toggleProcessPanelFilter(status));
     },
-    cancelProcess: (uuid) => dispatch<any>(cancelRunningWorkflow(uuid)),
-    startProcess: (uuid) => dispatch<any>(startWorkflow(uuid)),
-    resumeOnHoldWorkflow: (uuid) => dispatch<any>(resumeOnHoldWorkflow(uuid)),
-    onLogFilterChange: (filter) => dispatch(setProcessLogsPanelFilter(filter.value)),
-    navigateToLog: (uuid) => dispatch<any>(navigateToLogCollection(uuid)),
-    loadInputs: (containerRequest) => dispatch<any>(loadInputs(containerRequest)),
-    loadOutputs: (containerRequest) => dispatch<any>(loadOutputs(containerRequest)),
-    loadOutputDefinitions: (containerRequest) => dispatch<any>(loadOutputDefinitions(containerRequest)),
+    cancelProcess: uuid => dispatch<any>(cancelRunningWorkflow(uuid)),
+    startProcess: uuid => dispatch<any>(startWorkflow(uuid)),
+    resumeOnHoldWorkflow: uuid => dispatch<any>(resumeOnHoldWorkflow(uuid)),
+    onLogFilterChange: filter => dispatch(setProcessLogsPanelFilter(filter.value)),
+    navigateToLog: uuid => dispatch<any>(navigateToLogCollection(uuid)),
+    loadInputs: containerRequest => dispatch<any>(loadInputs(containerRequest)),
+    loadOutputs: containerRequest => dispatch<any>(loadOutputs(containerRequest)),
+    loadOutputDefinitions: containerRequest => dispatch<any>(loadOutputDefinitions(containerRequest)),
     updateOutputParams: () => dispatch<any>(updateOutputParams()),
-    loadNodeJson: (containerRequest) => dispatch<any>(loadNodeJson(containerRequest)),
+    loadNodeJson: containerRequest => dispatch<any>(loadNodeJson(containerRequest)),
 });
 
 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,
-        }));
+    return Object.keys(processPanel.filters).map(filter => ({
+        label: filter,
+        value: (grouppedProcesses[filter] || []).length,
+        checked: processPanel.filters[filter],
+        key: filter,
+    }));
 };
 
 export const ProcessPanel = connect(mapStateToProps, mapDispatchToProps)(ProcessPanelRoot);
diff --git a/src/views/subprocess-panel/subprocess-panel.tsx b/src/views/subprocess-panel/subprocess-panel.tsx
index c46a1c52..9ab069b6 100644
--- a/src/views/subprocess-panel/subprocess-panel.tsx
+++ b/src/views/subprocess-panel/subprocess-panel.tsx
@@ -4,17 +4,20 @@
 
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
-import { openProcessContextMenu } from 'store/context-menu/context-menu-actions';
-import { SubprocessPanelRoot, SubprocessPanelActionProps, SubprocessPanelDataProps } from 'views/subprocess-panel/subprocess-panel-root';
+import { openProcessContextMenu, openRunningProcessContextMenu } from "store/context-menu/context-menu-actions";
+import { SubprocessPanelRoot, SubprocessPanelActionProps, SubprocessPanelDataProps } from "views/subprocess-panel/subprocess-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";
+import { isProcessCancelable } from "store/processes/process";
 
 const mapDispatchToProps = (dispatch: Dispatch): SubprocessPanelActionProps => ({
     onContextMenu: (event, resourceUuid, resources) => {
         const process = getProcess(resourceUuid)(resources);
-        if (process) {
+        if (process && isProcessCancelable(process)) {
+            dispatch<any>(openRunningProcessContextMenu(event, process));
+        } else if (process) {
             dispatch<any>(openProcessContextMenu(event, process));
         }
     },
@@ -23,11 +26,11 @@ const mapDispatchToProps = (dispatch: Dispatch): SubprocessPanelActionProps => (
     },
     onItemDoubleClick: uuid => {
         dispatch<any>(navigateTo(uuid));
-    }
+    },
 });
 
 const mapStateToProps = (state: RootState): SubprocessPanelDataProps => ({
-    resources: state.resources
+    resources: state.resources,
 });
 
-export const SubprocessPanel = connect(mapStateToProps, mapDispatchToProps)(SubprocessPanelRoot);
\ No newline at end of file
+export const SubprocessPanel = connect(mapStateToProps, mapDispatchToProps)(SubprocessPanelRoot);

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list