[arvados] created: 2.7.0-5315-g049f1a1e93

git repository hosting git at public.arvados.org
Tue Nov 7 19:37:23 UTC 2023


        at  049f1a1e93539bf6f98d294e1b3365d6ac228598 (commit)


commit 049f1a1e93539bf6f98d294e1b3365d6ac228598
Author: Tom Clegg <tom at curii.com>
Date:   Tue Nov 7 14:36:37 2023 -0500

    18874: Use test auth instead of adding real accounts on test host.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/workbench2/cypress/integration/login.spec.js b/services/workbench2/cypress/integration/login.spec.js
index 2c539e4902..58f8874d55 100644
--- a/services/workbench2/cypress/integration/login.spec.js
+++ b/services/workbench2/cypress/integration/login.spec.js
@@ -27,17 +27,10 @@ describe('Login tests', function() {
             .as('inactiveUser').then(function() {
                 inactiveUser = this.inactiveUser;
             }
-        );
-        randomUser.username = `randomuser${Math.floor(Math.random() * 999999)}`;
-        randomUser.password = {
-            crypt: 'zpAReoZzPnwmQ',
-            clear: 'topsecret',
-        };
-        cy.exec(`useradd ${randomUser.username} -p ${randomUser.password.crypt}`);
-    })
-
-    after(function() {
-        cy.exec(`userdel ${randomUser.username}`);
+                                    );
+        // Username/password match Login.Test section of arvados_config.yml
+        randomUser.username = 'randomuser1234';
+        randomUser.password = 'topsecret';
     })
 
     beforeEach(function() {
@@ -128,14 +121,14 @@ describe('Login tests', function() {
         cy.get('#username').type(randomUser.username);
         cy.get('#password').type('wrong password');
         cy.get("button span:contains('Log in')").click();
-        cy.get('p#password-helper-text').should('contain', 'PAM: Authentication failure');
+        cy.get('p#password-helper-text').should('contain', 'Authentication failed');
         cy.url().should('not.contain', '/projects/');
     })
 
     it('successfully authenticates using the login form', function() {
         cy.visit('/');
         cy.get('#username').type(randomUser.username);
-        cy.get('#password').type(randomUser.password.clear);
+        cy.get('#password').type(randomUser.password);
         cy.get("button span:contains('Log in')").click();
         cy.url().should('contain', '/projects/');
         cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)');
diff --git a/services/workbench2/tools/arvados_config.yml b/services/workbench2/tools/arvados_config.yml
index 1ef77b86ce..ba41c51b63 100644
--- a/services/workbench2/tools/arvados_config.yml
+++ b/services/workbench2/tools/arvados_config.yml
@@ -18,8 +18,12 @@ Clusters:
         original_owner_uuid: {Function: original_owner, Protected: true}
     Login:
       TrustPrivateNetworks: true
-      PAM:
+      Test:
         Enable: true
+        Users:
+          randomuser1234:
+            Email: randomuser1234 at example.invalid
+            Password: topsecret
     StorageClasses:
       default:
         Default: true

commit b34c4b9234777d68b675aebf77680b8dd8708a6d
Author: Tom Clegg <tom at curii.com>
Date:   Tue Nov 7 13:48:42 2023 -0500

    18874: Fix errant it.only().
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>

diff --git a/services/workbench2/cypress/integration/project.spec.js b/services/workbench2/cypress/integration/project.spec.js
index a8663d8622..e61138219d 100644
--- a/services/workbench2/cypress/integration/project.spec.js
+++ b/services/workbench2/cypress/integration/project.spec.js
@@ -564,7 +564,7 @@ describe("Project tests", function () {
         );
     });
 
-    it.only("sorts displayed items correctly", () => {
+    it("sorts displayed items correctly", () => {
         cy.loginAs(activeUser);
 
         cy.get('[data-cy=project-panel] button[title="Select columns"]').click();

commit 4ad6191d53207a8b2d4c0c8a30b18119daaa5fbc
Merge: 56a2e5c346 8e913832ad
Author: Tom Clegg <tom at curii.com>
Date:   Mon Nov 6 12:14:51 2023 -0500

    Merge branch 'main' from workbench2.git
    
    refs #18874
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>


commit 8e913832ad289fd28700f4081046b7de48688130
Merge: 5c43d68767 5b30fab055
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Tue Oct 31 13:53:33 2023 -0400

    Merge branch '21067-process-panel-error' refs #21067
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>


commit 5b30fab0552f623a35ff5b89b90dab82dfcee258
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Mon Oct 30 15:24:39 2023 -0400

    21067: Fix bug deleting projects from shared-with-me
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/cypress/integration/page-not-found.spec.js b/cypress/integration/page-not-found.spec.js
index 4df4135c87..6eab27c827 100644
--- a/cypress/integration/page-not-found.spec.js
+++ b/cypress/integration/page-not-found.spec.js
@@ -45,8 +45,7 @@ describe('Page not found tests', function() {
             cy.goToPath(path);
 
             // then
-            cy.get('[data-cy=not-found-page]').should('not.exist');
-            cy.get('[data-cy=not-found-content]').should('exist');
+            cy.get('[data-cy=not-found-view]').should('exist');
         });
     });
-})
\ No newline at end of file
+})
diff --git a/src/store/trash/trash-actions.ts b/src/store/trash/trash-actions.ts
index 884293a90e..62b669220e 100644
--- a/src/store/trash/trash-actions.ts
+++ b/src/store/trash/trash-actions.ts
@@ -9,104 +9,112 @@ import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 import { trashPanelActions } from "store/trash-panel/trash-panel-action";
 import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "store/side-panel-tree/side-panel-tree-actions";
 import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
+import { sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with-me-panel-actions";
 import { ResourceKind } from "models/resource";
 import { navigateTo, navigateToTrash } from "store/navigation/navigation-action";
-import { matchCollectionRoute } from "routes/routes";
+import { matchCollectionRoute, matchSharedWithMeRoute } from "routes/routes";
 
 export const toggleProjectTrashed =
     (uuid: string, ownerUuid: string, isTrashed: boolean, isMulti: boolean) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
-        let errorMessage = "";
-        let successMessage = "";
-        let untrashedResource;
-        try {
-            if (isTrashed) {
-                errorMessage = "Could not restore project from trash";
-                successMessage = "Restored project from trash";
-                untrashedResource = await services.groupsService.untrash(uuid);
-                dispatch<any>(isMulti || !untrashedResource ? navigateToTrash : navigateTo(uuid));
-                dispatch<any>(activateSidePanelTreeItem(uuid));
-            } else {
-                errorMessage = "Could not move project to trash";
-                successMessage = "Added project to trash";
-                await services.groupsService.trash(uuid);
-                dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
-                dispatch<any>(navigateTo(ownerUuid));
-            }
-            if (untrashedResource) {
-                dispatch(
-                    snackbarActions.OPEN_SNACKBAR({
-                        message: successMessage,
-                        hideDuration: 2000,
-                        kind: SnackbarKind.SUCCESS,
-                    })
-                );
-            }
-        } catch (e) {
-            if (e.status === 422) {
-                dispatch(
-                    snackbarActions.OPEN_SNACKBAR({
-                        message: "Could not restore project from trash: Duplicate name at destination",
-                        kind: SnackbarKind.ERROR,
-                    })
-                );
-            } else {
-                dispatch(
-                    snackbarActions.OPEN_SNACKBAR({
-                        message: errorMessage,
-                        kind: SnackbarKind.ERROR,
-                    })
-                );
+        async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+            let errorMessage = "";
+            let successMessage = "";
+            let untrashedResource;
+            try {
+                if (isTrashed) {
+                    errorMessage = "Could not restore project from trash";
+                    successMessage = "Restored project from trash";
+                    untrashedResource = await services.groupsService.untrash(uuid);
+                    dispatch<any>(isMulti || !untrashedResource ? navigateToTrash : navigateTo(uuid));
+                    dispatch<any>(activateSidePanelTreeItem(uuid));
+                } else {
+                    errorMessage = "Could not move project to trash";
+                    successMessage = "Added project to trash";
+                    await services.groupsService.trash(uuid);
+                    dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
+
+                    const { location } = getState().router;
+                    if (matchSharedWithMeRoute(location ? location.pathname : "")) {
+                        dispatch(sharedWithMePanelActions.REQUEST_ITEMS());
+                    }
+                    else {
+                        dispatch<any>(navigateTo(ownerUuid));
+                    }
+                }
+                if (untrashedResource) {
+                    dispatch(
+                        snackbarActions.OPEN_SNACKBAR({
+                            message: successMessage,
+                            hideDuration: 2000,
+                            kind: SnackbarKind.SUCCESS,
+                        })
+                    );
+                }
+            } catch (e) {
+                if (e.status === 422) {
+                    dispatch(
+                        snackbarActions.OPEN_SNACKBAR({
+                            message: "Could not restore project from trash: Duplicate name at destination",
+                            kind: SnackbarKind.ERROR,
+                        })
+                    );
+                } else {
+                    dispatch(
+                        snackbarActions.OPEN_SNACKBAR({
+                            message: errorMessage,
+                            kind: SnackbarKind.ERROR,
+                        })
+                    );
+                }
             }
-        }
-    };
+        };
 
 export const toggleCollectionTrashed =
     (uuid: string, isTrashed: boolean) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
-        let errorMessage = "";
-        let successMessage = "";
-        try {
-            if (isTrashed) {
-                const { location } = getState().router;
-                errorMessage = "Could not restore collection from trash";
-                successMessage = "Restored from trash";
-                await services.collectionService.untrash(uuid);
-                if (matchCollectionRoute(location ? location.pathname : "")) {
-                    dispatch(navigateToTrash);
+        async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+            let errorMessage = "";
+            let successMessage = "";
+            try {
+                if (isTrashed) {
+                    const { location } = getState().router;
+                    errorMessage = "Could not restore collection from trash";
+                    successMessage = "Restored from trash";
+                    await services.collectionService.untrash(uuid);
+                    if (matchCollectionRoute(location ? location.pathname : "")) {
+                        dispatch(navigateToTrash);
+                    }
+                    dispatch(trashPanelActions.REQUEST_ITEMS());
+                } else {
+                    errorMessage = "Could not move collection to trash";
+                    successMessage = "Added to trash";
+                    await services.collectionService.trash(uuid);
+                    dispatch(projectPanelActions.REQUEST_ITEMS());
                 }
-                dispatch(trashPanelActions.REQUEST_ITEMS());
-            } else {
-                errorMessage = "Could not move collection to trash";
-                successMessage = "Added to trash";
-                await services.collectionService.trash(uuid);
-                dispatch(projectPanelActions.REQUEST_ITEMS());
-            }
-            dispatch(
-                snackbarActions.OPEN_SNACKBAR({
-                    message: successMessage,
-                    hideDuration: 2000,
-                    kind: SnackbarKind.SUCCESS,
-                })
-            );
-        } catch (e) {
-            if (e.status === 422) {
                 dispatch(
                     snackbarActions.OPEN_SNACKBAR({
-                        message: "Could not restore collection from trash: Duplicate name at destination",
-                        kind: SnackbarKind.ERROR,
-                    })
-                );
-            } else {
-                dispatch(
-                    snackbarActions.OPEN_SNACKBAR({
-                        message: errorMessage,
-                        kind: SnackbarKind.ERROR,
+                        message: successMessage,
+                        hideDuration: 2000,
+                        kind: SnackbarKind.SUCCESS,
                     })
                 );
+            } catch (e) {
+                if (e.status === 422) {
+                    dispatch(
+                        snackbarActions.OPEN_SNACKBAR({
+                            message: "Could not restore collection from trash: Duplicate name at destination",
+                            kind: SnackbarKind.ERROR,
+                        })
+                    );
+                } else {
+                    dispatch(
+                        snackbarActions.OPEN_SNACKBAR({
+                            message: errorMessage,
+                            kind: SnackbarKind.ERROR,
+                        })
+                    );
+                }
             }
-        }
-    };
+        };
 
 export const toggleTrashed = (kind: ResourceKind, uuid: string, ownerUuid: string, isTrashed: boolean) => (dispatch: Dispatch) => {
     if (kind === ResourceKind.PROJECT) {
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index eed9c7de71..d93d6e9258 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -37,7 +37,7 @@ import { Link as ButtonLink } from '@material-ui/core';
 import { ResourceWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers';
 import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
 import { resourceIsFrozen } from 'common/frozen-resources';
-import { DefaultView } from "components/default-view/default-view";
+import { NotFoundView } from 'views/not-found-panel/not-found-panel';
 
 type CssRules = 'root'
     | 'button'
@@ -230,16 +230,10 @@ export const CollectionPanel = withStyles(styles)(connect(
                             </Card>
                         </MPVPanelContent>
                     </MPVContainer >
-                    : <Grid
-                        container
-                        alignItems="center"
-                        justify="center"
-                        style={{ minHeight: "100%" }}>
-                        <DefaultView
-                            icon={CollectionIcon}
-                            messages={["Collection not found"]}
-                        />
-                    </Grid>
+                    : <NotFoundView
+                        icon={CollectionIcon}
+                        messages={["Collection not found"]}
+                    />
                     ;
             }
 
diff --git a/src/views/not-found-panel/not-found-panel.tsx b/src/views/not-found-panel/not-found-panel.tsx
index 148c331e29..f54c00c32a 100644
--- a/src/views/not-found-panel/not-found-panel.tsx
+++ b/src/views/not-found-panel/not-found-panel.tsx
@@ -3,8 +3,12 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { RootState } from 'store/store';
+import React from 'react';
 import { connect } from 'react-redux';
 import { NotFoundPanelRoot, NotFoundPanelRootDataProps } from 'views/not-found-panel/not-found-panel-root';
+import { Grid } from '@material-ui/core';
+import { DefaultView } from "components/default-view/default-view";
+import { IconType } from 'components/icon/icon';
 
 const mapStateToProps = (state: RootState): NotFoundPanelRootDataProps => {
     return {
@@ -17,3 +21,26 @@ const mapDispatchToProps = null;
 
 export const NotFoundPanel = connect(mapStateToProps, mapDispatchToProps)
     (NotFoundPanelRoot) as any;
+
+export interface NotFoundViewDataProps {
+    messages: string[];
+    icon?: IconType;
+}
+
+// TODO: optionally pass in the UUID and check if the
+// reason the item is not found is because
+// it or a parent project is actually in the trash.
+// If so, offer to untrash the item or the parent project.
+export const NotFoundView =
+    ({ messages, icon: Icon }: NotFoundViewDataProps) =>
+        <Grid
+            container
+            alignItems="center"
+            justify="center"
+            style={{ minHeight: "100%" }}
+            data-cy="not-found-view">
+            <DefaultView
+                icon={Icon}
+                messages={messages}
+            />
+        </Grid>;
diff --git a/src/views/process-panel/process-panel-root.tsx b/src/views/process-panel/process-panel-root.tsx
index d019d1418f..7a24089901 100644
--- a/src/views/process-panel/process-panel-root.tsx
+++ b/src/views/process-panel/process-panel-root.tsx
@@ -3,8 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from "react";
-import { Grid, StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
-import { DefaultView } from "components/default-view/default-view";
+import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
 import { ProcessIcon } from "components/icon/icon";
 import { Process } from "store/processes/process";
 import { SubprocessPanel } from "views/subprocess-panel/subprocess-panel";
@@ -24,6 +23,7 @@ import { AuthState } from "store/auth/auth-reducer";
 import { ProcessCmdCard } from "./process-cmd-card";
 import { ContainerRequestResource } from "models/container-request";
 import { OutputDetails, NodeInstanceType } from "store/process-panel/process-panel";
+import { NotFoundView } from 'views/not-found-panel/not-found-panel';
 
 type CssRules = "root";
 
@@ -209,16 +209,10 @@ export const ProcessPanelRoot = withStyles(styles)(
                 </MPVPanelContent>
             </MPVContainer>
         ) : (
-            <Grid
-                container
-                alignItems="center"
-                justify="center"
-                style={{ minHeight: "100%" }}>
-                <DefaultView
-                    icon={ProcessIcon}
-                    messages={["Process not found"]}
-                />
-            </Grid>
+            <NotFoundView
+                icon={ProcessIcon}
+                messages={["Process not found"]}
+            />
         );
     }
 );
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index 2f274c97d5..2cc751bffd 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 import withStyles from '@material-ui/core/styles/withStyles';
 import { DispatchProp, connect } from 'react-redux';
 import { RouteComponentProps } from 'react-router';
-import { StyleRulesCallback, WithStyles, Grid } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles } from '@material-ui/core';
 
 import { DataExplorer } from 'views-components/data-explorer/data-explorer';
 import { DataColumns } from 'components/data-table/data-table';
@@ -51,7 +51,7 @@ import { GroupClass, GroupResource } from 'models/group';
 import { CollectionResource } from 'models/collection';
 import { resourceIsFrozen } from 'common/frozen-resources';
 import { ProjectResource } from 'models/project';
-import { DefaultView } from "components/default-view/default-view";
+import { NotFoundView } from 'views/not-found-panel/not-found-panel';
 
 type CssRules = 'root' | 'button';
 
@@ -276,16 +276,11 @@ export const ProjectPanel = withStyles(styles)(
                             defaultViewMessages={DEFAULT_VIEW_MESSAGES}
                         />
                     </div>
-                    : <Grid
-                        container
-                        alignItems="center"
-                        justify="center"
-                        style={{ minHeight: "100%" }}>
-                        <DefaultView
-                            icon={ProjectIcon}
-                            messages={["Project not found"]}
-                        />
-                    </Grid>;
+                    :
+                    <NotFoundView
+                        icon={ProjectIcon}
+                        messages={["Project not found"]}
+                    />
             }
 
             isCurrentItemChild = (resource: Resource) => {
diff --git a/src/views/workflow-panel/registered-workflow-panel.tsx b/src/views/workflow-panel/registered-workflow-panel.tsx
index da273719bf..50192e543d 100644
--- a/src/views/workflow-panel/registered-workflow-panel.tsx
+++ b/src/views/workflow-panel/registered-workflow-panel.tsx
@@ -12,8 +12,7 @@ import {
     Card,
     CardHeader,
     CardContent,
-    IconButton,
-    Grid
+    IconButton
 } from '@material-ui/core';
 import { connect, DispatchProp } from "react-redux";
 import { RouteComponentProps } from 'react-router';
@@ -27,7 +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 { DefaultView } from "components/default-view/default-view";
+import { NotFoundView } from 'views/not-found-panel/not-found-panel';
 
 type CssRules = 'root'
     | 'button'
@@ -202,16 +201,11 @@ export const RegisteredWorkflowPanel = withStyles(styles)(connect(
                             </Card>
                         </MPVPanelContent>
                     </MPVContainer>
-                    : <Grid
-                        container
-                        alignItems="center"
-                        justify="center"
-                        style={{ minHeight: "100%" }}>
-                        <DefaultView
-                            icon={WorkflowIcon}
-                            messages={["Workflow not found"]}
-                        />
-                    </Grid>;
+                    :
+                    <NotFoundView
+                        icon={WorkflowIcon}
+                        messages={["Workflow not found"]}
+                    />
             }
 
             handleContextMenu = (event: React.MouseEvent<any>) => {

commit 26e2f985f4c2b511c59977e4c63158649f296624
Merge: e11c6c520d 5c43d68767
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Fri Oct 27 13:52:35 2023 -0400

    Merge branch 'main' into 21067-process-panel-error
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>


commit e11c6c520d8d01397f58a5cda1fe7e5e5e06acab
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Oct 26 17:15:51 2023 -0400

    21067: Remove unused imports
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/index.tsx b/src/index.tsx
index b0bda23ba8..ede257dc5d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -64,7 +64,6 @@ import {
     runningProcessResourceAdminActionSet,
     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";
@@ -89,8 +88,6 @@ import {
 } 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, readOnlyWorkflowActionSet } 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";
 
diff --git a/src/store/process-panel/process-panel-actions.ts b/src/store/process-panel/process-panel-actions.ts
index 66de2f69f3..81f8dd6ba0 100644
--- a/src/store/process-panel/process-panel-actions.ts
+++ b/src/store/process-panel/process-panel-actions.ts
@@ -8,10 +8,9 @@ import { Dispatch } from "redux";
 import { ProcessStatus } from "store/processes/process";
 import { RootState } from "store/store";
 import { ServiceRepository } from "services/services";
-import { navigateTo, navigateToWorkflows } from "store/navigation/navigation-action";
+import { navigateTo } from "store/navigation/navigation-action";
 import { snackbarActions } from "store/snackbar/snackbar-actions";
 import { SnackbarKind } from "../snackbar/snackbar-actions";
-import { showWorkflowDetails } from "store/workflow-panel/workflow-panel-actions";
 import { loadSubprocessPanel, subprocessPanelActions } from "../subprocess-panel/subprocess-panel-actions";
 import { initProcessLogsPanel, processLogsPanelActions } from "store/process-logs-panel/process-logs-panel-actions";
 import { CollectionFile } from "models/collection-file";
diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts
index b2ddb46c66..b72058d56e 100644
--- a/src/store/project-panel/project-panel-middleware-service.ts
+++ b/src/store/project-panel/project-panel-middleware-service.ts
@@ -35,7 +35,6 @@ import { updatePublicFavorites } from "store/public-favorites/public-favorites-a
 import { selectedFieldsOfGroup } from "models/group";
 import { defaultCollectionSelectedFields } from "models/collection";
 import { containerRequestFieldsNoMounts } from "models/container-request";
-import { openNotFoundDialog } from "store/not-found-panel/not-found-panel-action";
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {

commit 75b017ae9d566d523e448aaeb863c4d89b3548fe
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Oct 26 16:56:28 2023 -0400

    21067: Better handling of missing output/logs on process panel.
    
    A few other important changes:
    
    * Similar to the change in #21077, this removes the default error
    snackbar popups any time a 400 error happens.  This reduces user
    confusion, particularly when "harmless" errors would occur.
    
    * The collection files component will no longer give an "unhandled
    rejection" React failure when it can't load the file list (this took
    forever to track down.)
    
    * Collections, projects and workflows will now default to a "not
    found" panel if they fail to load.  Previously, collections and
    workflows would show nothing at all, and projects would show an empty
    list.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx
index 83de48dec8..f1e50e0f0b 100644
--- a/src/components/collection-panel-files/collection-panel-files.tsx
+++ b/src/components/collection-panel-files/collection-panel-files.tsx
@@ -311,6 +311,8 @@ export const CollectionPanelFiles = withStyles(styles)(
                             return { ...next, ...prev };
                         }, {});
                     setPathData(state => ({ ...state, ...newState }));
+                }, () => {
+                    // Nothing to do
                 })
                 .finally(() => {
                     setIsLoading(false);
diff --git a/src/index.tsx b/src/index.tsx
index a3f6c1ee79..b0bda23ba8 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -153,25 +153,13 @@ fetchConfig().then(({ config, apiHost }) => {
 
     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) {
+                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,
-                        })
-                    );
                 }
             }
         },
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index de8f258708..e50e5ed350 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -53,11 +53,14 @@ export class CollectionService extends TrashableResourceService<CollectionResour
     }
 
     async files(uuid: string) {
-        const request = await this.keepWebdavClient.propfind(`c=${uuid}`);
-        if (request.responseXML != null) {
-            return extractFilesData(request.responseXML);
+        try {
+            const request = await this.keepWebdavClient.propfind(`c=${uuid}`);
+            if (request.responseXML != null) {
+                return extractFilesData(request.responseXML);
+            }
+        } catch (e) {
+            return Promise.reject(e);
         }
-
         return Promise.reject();
     }
 
diff --git a/src/store/process-logs-panel/process-logs-panel-actions.ts b/src/store/process-logs-panel/process-logs-panel-actions.ts
index 87a2fa12aa..4e52431eeb 100644
--- a/src/store/process-logs-panel/process-logs-panel-actions.ts
+++ b/src/store/process-logs-panel/process-logs-panel-actions.ts
@@ -13,7 +13,7 @@ import { Process, getProcess } from 'store/processes/process';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { CollectionFile, CollectionFileType } from "models/collection-file";
-import { ContainerRequestResource } from "models/container-request";
+import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
 
 const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
 const LOG_TIMESTAMP_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{9}Z/;
@@ -40,15 +40,16 @@ export const setProcessLogsPanelFilter = (filter: string) =>
 
 export const initProcessLogsPanel = (processUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => {
+        let process: Process | undefined;
         try {
             dispatch(processLogsPanelActions.RESET_PROCESS_LOGS_PANEL());
-            const process = getProcess(processUuid)(getState().resources);
+            process = getProcess(processUuid)(getState().resources);
             if (process?.containerRequest?.uuid) {
                 // Get log file size info
                 const logFiles = await loadContainerLogFileList(process.containerRequest, logService);
 
                 // Populate lastbyte 0 for each file
-                const filesWithProgress = logFiles.map((file) => ({file, lastByte: 0}));
+                const filesWithProgress = logFiles.map((file) => ({ file, lastByte: 0 }));
 
                 // Fetch array of LogFragments
                 const logLines = await loadContainerLogFileContents(filesWithProgress, logService, process);
@@ -57,13 +58,16 @@ export const initProcessLogsPanel = (processUuid: string) =>
                 const initialState = createInitialLogPanelState(logFiles, logLines);
                 dispatch(processLogsPanelActions.INIT_PROCESS_LOGS_PANEL(initialState));
             }
-        } catch(e) {
+        } catch (e) {
             // On error, populate empty state to allow polling to start
             const initialState = createInitialLogPanelState([], []);
             dispatch(processLogsPanelActions.INIT_PROCESS_LOGS_PANEL(initialState));
             // Only show toast on errors other than 404 since 404 is expected when logs do not exist yet
             if (e.status !== 404) {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not load process logs', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Error loading process logs', hideDuration: 4000, kind: SnackbarKind.ERROR }));
+            }
+            if (e.status === 404 && process?.containerRequest.state === ContainerRequestState.FINAL) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Log collection was trashed or deleted.', hideDuration: 4000, kind: SnackbarKind.WARNING }));
             }
         }
     };
@@ -88,7 +92,7 @@ export const pollProcessLogs = (processUuid: string) =>
                     const isChanged = !isNew && currentStateLogLastByte < updatedFile.size;
 
                     if (isNew || isChanged) {
-                        return acc.concat({file: updatedFile, lastByte: currentStateLogLastByte});
+                        return acc.concat({ file: updatedFile, lastByte: currentStateLogLastByte });
                     } else {
                         return acc;
                     }
@@ -132,17 +136,17 @@ const loadContainerLogFileList = async (containerRequest: ContainerRequestResour
  * @returns LogFragment[] containing a single LogFragment corresponding to each input file
  */
 const loadContainerLogFileContents = async (logFilesWithProgress: FileWithProgress[], logService: LogService, process: Process) => (
-    (await Promise.allSettled(logFilesWithProgress.filter(({file}) => file.size > 0).map(({file, lastByte}) => {
+    (await Promise.allSettled(logFilesWithProgress.filter(({ file }) => file.size > 0).map(({ file, lastByte }) => {
         const requestSize = file.size - lastByte;
         if (requestSize > maxLogFetchSize) {
             const chunkSize = Math.floor(maxLogFetchSize / 2);
-            const firstChunkEnd = lastByte+chunkSize-1;
+            const firstChunkEnd = lastByte + chunkSize - 1;
             return Promise.all([
                 logService.getLogFileContents(process.containerRequest, file, lastByte, firstChunkEnd),
-                logService.getLogFileContents(process.containerRequest, file, file.size-chunkSize, file.size-1)
+                logService.getLogFileContents(process.containerRequest, file, file.size - chunkSize, file.size - 1)
             ] as Promise<(LogFragment)>[]);
         } else {
-            return Promise.all([logService.getLogFileContents(process.containerRequest, file, lastByte, file.size-1)]);
+            return Promise.all([logService.getLogFileContents(process.containerRequest, file, lastByte, file.size - 1)]);
         }
     })).then((res) => {
         if (res.length && res.every(promiseResult => (promiseResult.status === 'rejected'))) {
@@ -150,7 +154,7 @@ const loadContainerLogFileContents = async (logFilesWithProgress: FileWithProgre
             //   error if every request failed
             const error = res.find(
                 (promiseResult): promiseResult is PromiseRejectedResult => promiseResult.status === 'rejected'
-              )?.reason;
+            )?.reason;
             return Promise.reject(error);
         }
         return res.filter((promiseResult): promiseResult is PromiseFulfilledResult<LogFragment[]> => (
@@ -161,16 +165,16 @@ const loadContainerLogFileContents = async (logFilesWithProgress: FileWithProgre
             //   (prevent incorrect snipline generation or an un-resumable situation)
             !!promiseResult.value.every(logFragment => logFragment.contents.length)
         )).map(one => one.value)
-    })).map((logResponseSet)=> {
+    })).map((logResponseSet) => {
         // For any multi fragment response set, modify the last line of non-final chunks to include a line break and snip line
         //   Don't add snip line as a separate line so that sorting won't reorder it
         for (let i = 1; i < logResponseSet.length; i++) {
-            const fragment = logResponseSet[i-1];
-            const lastLineIndex = fragment.contents.length-1;
+            const fragment = logResponseSet[i - 1];
+            const lastLineIndex = fragment.contents.length - 1;
             const lastLineContents = fragment.contents[lastLineIndex];
             const newLastLine = `${lastLineContents}\n${SNIPLINE}`;
 
-            logResponseSet[i-1].contents[lastLineIndex] = newLastLine;
+            logResponseSet[i - 1].contents[lastLineIndex] = newLastLine;
         }
 
         // Merge LogFragment Array (representing multiple log line arrays) into single LogLine[] / LogFragment
@@ -181,7 +185,7 @@ const loadContainerLogFileContents = async (logFilesWithProgress: FileWithProgre
     })
 );
 
-const createInitialLogPanelState = (logFiles: CollectionFile[], logFragments: LogFragment[]): {filters: string[], logs: ProcessLogs} => {
+const createInitialLogPanelState = (logFiles: CollectionFile[], logFragments: LogFragment[]): { filters: string[], logs: ProcessLogs } => {
     const logs = groupLogs(logFiles, logFragments);
     const filters = Object.keys(logs);
     return { filters, logs };
@@ -201,12 +205,12 @@ const groupLogs = (logFiles: CollectionFile[], logFragments: LogFragment[]): Pro
 
     const groupedLogs = logFragments.reduce((grouped, fragment) => ({
         ...grouped,
-        [fragment.logType as string]: {lastByte: fetchLastByteNumber(logFiles, fragment.logType), contents: fragment.contents}
+        [fragment.logType as string]: { lastByte: fetchLastByteNumber(logFiles, fragment.logType), contents: fragment.contents }
     }), {});
 
     return {
-        [MAIN_FILTER_TYPE]: {lastByte: undefined, contents: mainLogs},
-        [ALL_FILTER_TYPE]: {lastByte: undefined, contents: allLogs},
+        [MAIN_FILTER_TYPE]: { lastByte: undefined, contents: mainLogs },
+        [ALL_FILTER_TYPE]: { lastByte: undefined, contents: allLogs },
         ...groupedLogs,
     }
 };
@@ -233,9 +237,9 @@ const mergeMultilineLoglines = (logFragments: LogFragment[]) => (
                     // Partial line without timestamp detected
                     if (i > 0) {
                         // If not first line, copy line to previous line
-                        const previousLineContents = fragmentCopy.contents[i-1];
+                        const previousLineContents = fragmentCopy.contents[i - 1];
                         const newPreviousLineContents = `${previousLineContents}\n${lineContents}`;
-                        fragmentCopy.contents[i-1] = newPreviousLineContents;
+                        fragmentCopy.contents[i - 1] = newPreviousLineContents;
                     }
                     // Delete the current line and prevent iterating
                     fragmentCopy.contents.splice(i, 1);
@@ -283,7 +287,7 @@ export const navigateToLogCollection = (uuid: string) =>
             await services.collectionService.get(uuid);
             dispatch<any>(navigateTo(uuid));
         } catch {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not request collection', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Log collection was trashed or deleted.', hideDuration: 4000, kind: SnackbarKind.WARNING }));
         }
     };
 
diff --git a/src/store/process-panel/process-panel-actions.ts b/src/store/process-panel/process-panel-actions.ts
index 03e36aac98..66de2f69f3 100644
--- a/src/store/process-panel/process-panel-actions.ts
+++ b/src/store/process-panel/process-panel-actions.ts
@@ -59,7 +59,7 @@ export const navigateToOutput = (uuid: string) => async (dispatch: Dispatch<any>
         await services.collectionService.get(uuid);
         dispatch<any>(navigateTo(uuid));
     } catch {
-        dispatch(snackbarActions.OPEN_SNACKBAR({ message: "This collection does not exists!", hideDuration: 2000, kind: SnackbarKind.ERROR }));
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Output collection was trashed or deleted.", hideDuration: 4000, kind: SnackbarKind.WARNING }));
     }
 };
 
@@ -159,8 +159,7 @@ export const updateOutputParams = () => async (dispatch: Dispatch<any>, getState
 };
 
 export const openWorkflow = (uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    dispatch<any>(navigateToWorkflows);
-    dispatch<any>(showWorkflowDetails(uuid));
+    dispatch<any>(navigateTo(uuid));
 };
 
 export const initProcessPanelFilters = processPanelActions.SET_PROCESS_PANEL_FILTERS([
diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts
index c0c0cd1873..b2ddb46c66 100644
--- a/src/store/project-panel/project-panel-middleware-service.ts
+++ b/src/store/project-panel/project-panel-middleware-service.ts
@@ -35,6 +35,7 @@ import { updatePublicFavorites } from "store/public-favorites/public-favorites-a
 import { selectedFieldsOfGroup } from "models/group";
 import { defaultCollectionSelectedFields } from "models/collection";
 import { containerRequestFieldsNoMounts } from "models/container-request";
+import { openNotFoundDialog } from "store/not-found-panel/not-found-panel-action";
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -69,7 +70,12 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
                         rowsPerPage: dataExplorer.rowsPerPage,
                     })
                 );
-                api.dispatch(couldNotFetchProjectContents());
+                if (e.status === 404) {
+                    // It'll just show up as not found
+                }
+                else {
+                    api.dispatch(couldNotFetchProjectContents());
+                }
             } finally {
                 if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
             }
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index b03400d5ae..f2dae2c524 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -109,6 +109,12 @@ export const isWorkbenchLoading = (state: RootState) => {
 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));
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index 8cf19c03fe..eed9c7de71 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -37,6 +37,7 @@ import { Link as ButtonLink } from '@material-ui/core';
 import { ResourceWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers';
 import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
 import { resourceIsFrozen } from 'common/frozen-resources';
+import { DefaultView } from "components/default-view/default-view";
 
 type CssRules = 'root'
     | 'button'
@@ -229,7 +230,17 @@ export const CollectionPanel = withStyles(styles)(connect(
                             </Card>
                         </MPVPanelContent>
                     </MPVContainer >
-                    : null;
+                    : <Grid
+                        container
+                        alignItems="center"
+                        justify="center"
+                        style={{ minHeight: "100%" }}>
+                        <DefaultView
+                            icon={CollectionIcon}
+                            messages={["Collection not found"]}
+                        />
+                    </Grid>
+                    ;
             }
 
             handleContextMenu = (event: React.MouseEvent<any>) => {
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index 4c94ab8d2d..2f274c97d5 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 import withStyles from '@material-ui/core/styles/withStyles';
 import { DispatchProp, connect } from 'react-redux';
 import { RouteComponentProps } from 'react-router';
-import { StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, Grid } from '@material-ui/core';
 
 import { DataExplorer } from 'views-components/data-explorer/data-explorer';
 import { DataColumns } from 'components/data-table/data-table';
@@ -51,6 +51,7 @@ import { GroupClass, GroupResource } from 'models/group';
 import { CollectionResource } from 'models/collection';
 import { resourceIsFrozen } from 'common/frozen-resources';
 import { ProjectResource } from 'models/project';
+import { DefaultView } from "components/default-view/default-view";
 
 type CssRules = 'root' | 'button';
 
@@ -238,6 +239,7 @@ const DEFAULT_VIEW_MESSAGES = ['Your project is empty.', 'Please create a projec
 interface ProjectPanelDataProps {
     currentItemId: string;
     resources: ResourcesState;
+    project: GroupResource;
     isAdmin: boolean;
     userUuid: string;
     dataExplorerItems: any;
@@ -245,17 +247,24 @@ interface ProjectPanelDataProps {
 
 type ProjectPanelProps = ProjectPanelDataProps & DispatchProp & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
-export const ProjectPanel = withStyles(styles)(
-    connect((state: RootState) => ({
-        currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+const mapStateToProps = (state: RootState) => {
+    const currentItemId = getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+    const project = getResource<GroupResource>(currentItemId || "")(state.resources);
+    return {
+        currentItemId,
+        project,
         resources: state.resources,
         userUuid: state.auth.user!.uuid,
-    }))(
+    };
+}
+
+export const ProjectPanel = withStyles(styles)(
+    connect(mapStateToProps)(
         class extends React.Component<ProjectPanelProps> {
             render() {
                 const { classes } = this.props;
 
-                return (
+                return this.props.project ?
                     <div data-cy='project-panel' className={classes.root}>
                         <DataExplorer
                             id={PROJECT_PANEL_ID}
@@ -267,7 +276,16 @@ export const ProjectPanel = withStyles(styles)(
                             defaultViewMessages={DEFAULT_VIEW_MESSAGES}
                         />
                     </div>
-                );
+                    : <Grid
+                        container
+                        alignItems="center"
+                        justify="center"
+                        style={{ minHeight: "100%" }}>
+                        <DefaultView
+                            icon={ProjectIcon}
+                            messages={["Project not found"]}
+                        />
+                    </Grid>;
             }
 
             isCurrentItemChild = (resource: Resource) => {
diff --git a/src/views/workflow-panel/registered-workflow-panel.tsx b/src/views/workflow-panel/registered-workflow-panel.tsx
index 5973efedc8..da273719bf 100644
--- a/src/views/workflow-panel/registered-workflow-panel.tsx
+++ b/src/views/workflow-panel/registered-workflow-panel.tsx
@@ -13,6 +13,7 @@ import {
     CardHeader,
     CardContent,
     IconButton,
+    Grid
 } from '@material-ui/core';
 import { connect, DispatchProp } from "react-redux";
 import { RouteComponentProps } from 'react-router';
@@ -26,6 +27,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 { DefaultView } from "components/default-view/default-view";
 
 type CssRules = 'root'
     | 'button'
@@ -200,7 +202,16 @@ export const RegisteredWorkflowPanel = withStyles(styles)(connect(
                             </Card>
                         </MPVPanelContent>
                     </MPVContainer>
-                    : null;
+                    : <Grid
+                        container
+                        alignItems="center"
+                        justify="center"
+                        style={{ minHeight: "100%" }}>
+                        <DefaultView
+                            icon={WorkflowIcon}
+                            messages={["Workflow not found"]}
+                        />
+                    </Grid>;
             }
 
             handleContextMenu = (event: React.MouseEvent<any>) => {

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list