Merge branch 'main' into 19462-colorscheme refs #19462
authorPeter Amstutz <peter.amstutz@curii.com>
Mon, 19 Dec 2022 20:34:14 +0000 (15:34 -0500)
committerPeter Amstutz <peter.amstutz@curii.com>
Mon, 19 Dec 2022 20:34:14 +0000 (15:34 -0500)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

72 files changed:
.gitignore
cypress/integration/collection.spec.js
cypress/integration/create-workflow.spec.js
cypress/integration/favorites.spec.js
cypress/integration/project.spec.js
src/common/config.ts
src/common/formatters.ts
src/components/breadcrumbs/breadcrumbs.test.tsx
src/components/breadcrumbs/breadcrumbs.tsx
src/components/column-selector/column-selector.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.tsx
src/components/file-upload/file-upload.tsx
src/components/form-dialog/form-dialog.tsx
src/components/icon/icon.tsx
src/components/multi-panel-view/multi-panel-view.tsx
src/models/container-request.ts
src/models/log.ts
src/models/search-bar.ts
src/services/api/filter-builder.ts
src/services/collection-service/collection-service.ts
src/services/common-service/common-resource-service.ts
src/services/common-service/common-service.ts
src/services/groups-service/groups-service.ts
src/services/project-service/project-service.ts
src/store/all-processes-panel/all-processes-panel-middleware-service.ts
src/store/auth/auth-action.ts
src/store/breadcrumbs/breadcrumbs-actions.ts
src/store/collections/collection-create-actions.ts
src/store/collections/collection-update-actions.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/data-explorer/data-explorer-middleware.ts
src/store/data-explorer/data-explorer-reducer.ts
src/store/process-logs-panel/process-logs-panel-actions.ts
src/store/projects/project-create-actions.ts
src/store/projects/project-update-actions.ts
src/store/search-bar/search-bar-actions.ts
src/store/search-bar/search-bar-tree-actions.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/store.ts
src/store/subprocess-panel/subprocess-panel-middleware-service.ts
src/store/tree-picker/tree-picker-actions.ts
src/store/tree-picker/tree-picker-middleware.ts [new file with mode: 0644]
src/store/tree-picker/tree-picker-reducer.ts
src/store/workbench/workbench-actions.ts
src/validators/validators.tsx
src/views-components/breadcrumbs/breadcrumbs.ts
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
src/views-components/dialog-copy/dialog-copy.tsx
src/views-components/dialog-copy/dialog-partial-copy-to-collection.tsx
src/views-components/file-uploader/file-uploader.tsx
src/views-components/form-fields/search-bar-form-fields.tsx
src/views-components/main-content-bar/main-content-bar.tsx
src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
src/views-components/projects-tree-picker/home-tree-picker.tsx
src/views-components/projects-tree-picker/projects-tree-picker.tsx
src/views-components/projects-tree-picker/search-projects-picker.tsx [new file with mode: 0644]
src/views-components/projects-tree-picker/shared-tree-picker.tsx
src/views-components/projects-tree-picker/tree-picker-field.tsx
src/views-components/resource-properties-form/property-value-field.tsx
src/views-components/resource-properties-form/resource-properties-form.tsx
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views/inactive-panel/inactive-panel.tsx
src/views/process-panel/process-io-card.tsx
src/views/project-panel/project-panel.tsx
src/views/run-process-panel/inputs/directory-array-input.tsx
src/views/run-process-panel/inputs/directory-input.tsx
src/views/run-process-panel/inputs/file-array-input.tsx
src/views/run-process-panel/inputs/file-input.tsx
src/views/run-process-panel/inputs/project-input.tsx

index 6a564a2bc75617053c888e76a6dce6ba769cb44b..ead5e35a7e8f3cc6b0c006cb153f9fb95b4abd03 100644 (file)
@@ -33,6 +33,7 @@ yarn-error.log*
 
 .idea
 .vscode
+/public/config.json
 
 # see https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
 .pnp.*
index 74506aea21b67533e8318bbae402be3b0497a9ee..01d7001f2fe55ca0975578379905a012b6ffc0e1 100644 (file)
@@ -75,6 +75,55 @@ describe('Collection panel tests', function () {
         });
     });
 
+    it('attempts to use a preexisting name creating or updating a collection', function() {
+        const name = `Test collection ${Math.floor(Math.random() * 999999)}`;
+        cy.createCollection(adminUser.token, {
+            name: name,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+        });
+        cy.loginAs(activeUser);
+        cy.goToPath(`/projects/${activeUser.user.uuid}`);
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('not.exist');
+        // Attempt to create new collection with a duplicate name
+        cy.get('[data-cy=side-panel-button]').click();
+        cy.get('[data-cy=side-panel-new-collection]').click();
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'New collection')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(name);
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        // Error message should display, allowing editing the name
+        cy.get('[data-cy=form-dialog]').should('exist')
+            .and('contain', 'Collection with the same name already exists')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(' renamed');
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+        // Attempt to rename the collection with the duplicate name
+        cy.get('[data-cy=collection-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Edit collection').click();
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'Edit Collection')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input')
+                        .type('{selectall}{backspace}')
+                        .type(name);
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        cy.get('[data-cy=form-dialog]').should('exist')
+            .and('contain', 'Collection with the same name already exists');
+    });
+
     it('uses the property editor (from edit dialog) with vocabulary terms', function () {
         cy.createCollection(adminUser.token, {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
@@ -628,9 +677,10 @@ describe('Collection panel tests', function () {
                 cy.get('[data-cy=form-dialog]')
                     .should('contain', 'Move to')
                     .within(() => {
+                        // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
                         cy.get('[data-cy=projects-tree-home-tree-picker]')
                             .find('i')
-                            .click();
+                            .then(el => el.click());
                         cy.get('[data-cy=projects-tree-home-tree-picker]')
                             .contains(projName)
                             .click();
@@ -911,6 +961,10 @@ describe('Collection panel tests', function () {
         // Confirm proper vocabulary labels are displayed on the UI.
         cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
 
+        // Value field should not complain about being required just after
+        // adding a new property. See #19732
+        cy.get('[data-cy=form-dialog]').should('not.contain', 'This field is required');
+
         cy.get('[data-cy=form-submit-btn]').click();
         // Confirm that the user was taken to the newly created collection
         cy.get('[data-cy=form-dialog]').should('not.exist');
index 8df8389ffd1a6fbc4683a1fc2405b1d146713ce6..ca56e404ad61c60400a1cebb8e8dcf32e7c81f6c 100644 (file)
@@ -82,7 +82,7 @@ describe('Multi-file deletion tests', function () {
                 });
 
             cy.get('[data-cy=choose-a-file-dialog]').as('chooseFileDialog');
-            cy.get('@chooseFileDialog').contains('Projects').closest('ul').find('i').click();
+            cy.get('@chooseFileDialog').contains('Home Projects').closest('ul').find('i').click();
 
             cy.get('@project1').then((project1) => {
                 cy.get('@chooseFileDialog').find(`[data-id=${project1.uuid}]`).find('i').click();
@@ -158,11 +158,10 @@ describe('Multi-file deletion tests', function () {
                         cy.get('label').contains('foo').parent('div').find('input').click();
                         cy.get('div[role=dialog]')
                             .within(() => {
-                                cy.get('p').contains('Projects').closest('div[role=button]')
-                                    .within(() => {
-                                        cy.get('svg[role=presentation]')
-                                            .click({ multiple: true });
-                                    });
+                                // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+                                cy.get('p').contains('Home Projects').closest('ul')
+                                    .find('i')
+                                    .then(el => el.click());
 
                                 cy.get(`[data-id=${testCollection.uuid}]`)
                                     .find('i').click();
@@ -177,11 +176,10 @@ describe('Multi-file deletion tests', function () {
                         cy.get('label').contains('bar').parent('div').find('input').click();
                         cy.get('div[role=dialog]')
                             .within(() => {
-                                cy.get('p').contains('Projects').closest('div[role=button]')
-                                    .within(() => {
-                                        cy.get('svg[role=presentation]')
-                                            .click({ multiple: true });
-                                    });
+                                // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+                                cy.get('p').contains('Home Projects').closest('ul')
+                                    .find('i')
+                                    .then(el => el.click());
 
                                 cy.get(`[data-id=${testCollection.uuid}]`)
                                     .find('input[type=checkbox]').click();
index 7fd091245f770015a7c86c12ae938d0ace54db86..db9a0d5f394072736dbff8c1af730182eacc6ee4 100644 (file)
@@ -119,7 +119,10 @@ describe('Favorites tests', function () {
                 });
 
                 cy.get('[data-cy=form-dialog]').within(function () {
-                    cy.get('[data-cy=projects-tree-favourites-tree-picker]').find('i').click();
+                    // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+                    cy.get('[data-cy=projects-tree-favourites-tree-picker]')
+                        .find('i')
+                        .then(el => el.click());
                     cy.contains(myProject1.name);
                     cy.contains(mySharedWritableProject.name);
                     cy.get('[data-cy=projects-tree-favourites-tree-picker]')
index 93e4257b5b55f137eb626f9669e784b2ba7a5172..bbb3571af1e3ed93b8f17e2307ea35133da4f4b8 100644 (file)
@@ -158,6 +158,38 @@ describe('Project tests', function() {
         cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
     });
 
+    it('attempts to use a preexisting name creating a project', function() {
+        const name = `Test project ${Math.floor(Math.random() * 999999)}`;
+        cy.createGroup(activeUser.token, {
+            name: name,
+            group_class: 'project',
+        });
+        cy.loginAs(activeUser);
+        cy.goToPath(`/projects/${activeUser.user.uuid}`);
+
+        // Attempt to create new collection with a duplicate name
+        cy.get('[data-cy=side-panel-button]').click();
+        cy.get('[data-cy=side-panel-new-project]').click();
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'New Project')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(name);
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        // Error message should display, allowing editing the name
+        cy.get('[data-cy=form-dialog]').should('exist')
+            .and('contain', 'Project with the same name already exists')
+            .within(() => {
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(' renamed');
+                });
+                cy.get('[data-cy=form-submit-btn]').click();
+            });
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+    });
+
     it('navigates to the parent project after trashing the one being displayed', function() {
         cy.createGroup(activeUser.token, {
             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
@@ -313,12 +345,12 @@ describe('Project tests', function() {
     });
 
     describe('Frozen projects', () => {
-        beforeEach(() => {  
+        beforeEach(() => {
             cy.createGroup(activeUser.token, {
                 name: `Main project ${Math.floor(Math.random() * 999999)}`,
                 group_class: 'project',
             }).as('mainProject');
-    
+
             cy.createGroup(adminUser.token, {
                 name: `Admin project ${Math.floor(Math.random() * 999999)}`,
                 group_class: 'project',
@@ -337,7 +369,7 @@ describe('Project tests', function() {
                     name: `Main collection ${Math.floor(Math.random() * 999999)}`,
                     owner_uuid: mainProject.uuid,
                     manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-                }).as('mainCollection');        
+                }).as('mainCollection');
             });
         });
 
@@ -398,7 +430,7 @@ describe('Project tests', function() {
                 cy.get('[data-cy=context-menu]').contains('Unfreeze').click();
 
                 cy.get('main').contains(adminProject.name).rightclick();
-                
+
                 cy.get('[data-cy=context-menu]').contains('Freeze').should('exist');
             });
         });
index 574445df09b05f5f47a8b0b57fc190352a8e1237..9319736784b81676c6c15fe931e5136b806d985a 100644 (file)
@@ -2,19 +2,21 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import Axios from "axios";
+import Axios from 'axios';
 
-export const WORKBENCH_CONFIG_URL = process.env.REACT_APP_ARVADOS_CONFIG_URL || "/config.json";
+export const WORKBENCH_CONFIG_URL =
+  process.env.REACT_APP_ARVADOS_CONFIG_URL || '/config.json';
 
 interface WorkbenchConfig {
-    API_HOST: string;
-    VOCABULARY_URL?: string;
-    FILE_VIEWERS_CONFIG_URL?: string;
+  API_HOST: string;
+  VOCABULARY_URL?: string;
+  FILE_VIEWERS_CONFIG_URL?: string;
 }
 
 export interface ClusterConfigJSON {
     API: {
         UnfreezeProjectRequiresAdmin: boolean
+        MaxItemsPerResponse: number
     },
     ClusterID: string;
     RemoteClusters: {
@@ -26,297 +28,328 @@ export interface ClusterConfigJSON {
             Scheme: string
         }
     };
-    Mail?: {
-        SupportEmailAddress: string;
+  Mail?: {
+    SupportEmailAddress: string;
+  };
+  Services: {
+    Controller: {
+      ExternalURL: string;
     };
-    Services: {
-        Controller: {
-            ExternalURL: string
-        }
-        Workbench1: {
-            ExternalURL: string
-        }
-        Workbench2: {
-            ExternalURL: string
-        }
-        Websocket: {
-            ExternalURL: string
-        }
-        WebDAV: {
-            ExternalURL: string
-        },
-        WebDAVDownload: {
-            ExternalURL: string
-        },
-        WebShell: {
-            ExternalURL: string
-        }
+    Workbench1: {
+      ExternalURL: string;
     };
-    Workbench: {
-        DisableSharingURLsUI: boolean;
-        ArvadosDocsite: string;
-        FileViewersConfigURL: string;
-        WelcomePageHTML: string;
-        InactivePageHTML: string;
-        SSHHelpPageHTML: string;
-        SSHHelpHostSuffix: string;
-        SiteName: string;
-        IdleTimeout: string;
+    Workbench2: {
+      ExternalURL: string;
     };
-    Login: {
-        LoginCluster: string;
-        Google: {
-            Enable: boolean;
-        }
-        LDAP: {
-            Enable: boolean;
-        }
-        OpenIDConnect: {
-            Enable: boolean;
-        }
-        PAM: {
-            Enable: boolean;
-        }
-        SSO: {
-            Enable: boolean;
-        }
-        Test: {
-            Enable: boolean;
-        }
+    Websocket: {
+      ExternalURL: string;
     };
-    Collections: {
-        ForwardSlashNameSubstitution: string;
-        ManagedProperties?: {
-            [key: string]: {
-                Function: string,
-                Value: string,
-                Protected?: boolean,
-            }
-        },
-        TrustAllContent: boolean
+    WebDAV: {
+      ExternalURL: string;
     };
-    Volumes: {
-        [key: string]: {
-            StorageClasses: {
-                [key: string]: boolean;
-            }
-        }
+    WebDAVDownload: {
+      ExternalURL: string;
+    };
+    WebShell: {
+      ExternalURL: string;
+    };
+  };
+  Workbench: {
+    DisableSharingURLsUI: boolean;
+    ArvadosDocsite: string;
+    FileViewersConfigURL: string;
+    WelcomePageHTML: string;
+    InactivePageHTML: string;
+    SSHHelpPageHTML: string;
+    SSHHelpHostSuffix: string;
+    SiteName: string;
+    IdleTimeout: string;
+  };
+  Login: {
+    LoginCluster: string;
+    Google: {
+      Enable: boolean;
+    };
+    LDAP: {
+      Enable: boolean;
+    };
+    OpenIDConnect: {
+      Enable: boolean;
+    };
+    PAM: {
+      Enable: boolean;
     };
+    SSO: {
+      Enable: boolean;
+    };
+    Test: {
+      Enable: boolean;
+    };
+  };
+  Collections: {
+    ForwardSlashNameSubstitution: string;
+    ManagedProperties?: {
+      [key: string]: {
+        Function: string;
+        Value: string;
+        Protected?: boolean;
+      };
+    };
+    TrustAllContent: boolean;
+  };
+  Volumes: {
+    [key: string]: {
+      StorageClasses: {
+        [key: string]: boolean;
+      };
+    };
+  };
 }
 
 export class Config {
-    baseUrl!: string;
-    keepWebServiceUrl!: string;
-    keepWebInlineServiceUrl!: string;
-    remoteHosts!: {
-        [key: string]: string
-    };
-    rootUrl!: string;
-    uuidPrefix!: string;
-    websocketUrl!: string;
-    workbenchUrl!: string;
-    workbench2Url!: string;
-    vocabularyUrl!: string;
-    fileViewersConfigUrl!: string;
-    loginCluster!: string;
-    clusterConfig!: ClusterConfigJSON;
-    apiRevision!: number;
+  baseUrl!: string;
+  keepWebServiceUrl!: string;
+  keepWebInlineServiceUrl!: string;
+  remoteHosts!: {
+    [key: string]: string;
+  };
+  rootUrl!: string;
+  uuidPrefix!: string;
+  websocketUrl!: string;
+  workbenchUrl!: string;
+  workbench2Url!: string;
+  vocabularyUrl!: string;
+  fileViewersConfigUrl!: string;
+  loginCluster!: string;
+  clusterConfig!: ClusterConfigJSON;
+  apiRevision!: number;
 }
 
 export const buildConfig = (clusterConfig: ClusterConfigJSON): Config => {
-    const clusterConfigJSON = removeTrailingSlashes(clusterConfig);
-    const config = new Config();
-    config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
-    config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
-    config.uuidPrefix = clusterConfigJSON.ClusterID;
-    config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
-    config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
-    config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
-    config.keepWebServiceUrl = clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
-    config.keepWebInlineServiceUrl = clusterConfigJSON.Services.WebDAV.ExternalURL;
-    config.loginCluster = clusterConfigJSON.Login.LoginCluster;
-    config.clusterConfig = clusterConfigJSON;
-    config.apiRevision = 0;
-    mapRemoteHosts(clusterConfigJSON, config);
-    return config;
+  const clusterConfigJSON = removeTrailingSlashes(clusterConfig);
+  const config = new Config();
+  config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
+  config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
+  config.uuidPrefix = clusterConfigJSON.ClusterID;
+  config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
+  config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
+  config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
+  config.keepWebServiceUrl =
+    clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
+  config.keepWebInlineServiceUrl =
+    clusterConfigJSON.Services.WebDAV.ExternalURL;
+  config.loginCluster = clusterConfigJSON.Login.LoginCluster;
+  config.clusterConfig = clusterConfigJSON;
+  config.apiRevision = 0;
+  mapRemoteHosts(clusterConfigJSON, config);
+  return config;
 };
 
 export const getStorageClasses = (config: Config): string[] => {
-    const classes: Set<string> = new Set(['default']);
-    const volumes = config.clusterConfig.Volumes;
-    Object.keys(volumes).forEach(v => {
-        Object.keys(volumes[v].StorageClasses || {}).forEach(sc => {
-            if (volumes[v].StorageClasses[sc]) {
-                classes.add(sc);
-            }
-        });
+  const classes: Set<string> = new Set(['default']);
+  const volumes = config.clusterConfig.Volumes;
+  Object.keys(volumes).forEach((v) => {
+    Object.keys(volumes[v].StorageClasses || {}).forEach((sc) => {
+      if (volumes[v].StorageClasses[sc]) {
+        classes.add(sc);
+      }
     });
-    return Array.from(classes);
+  });
+  return Array.from(classes);
 };
 
 const getApiRevision = async (apiUrl: string) => {
-    try {
-        const dd = (await Axios.get<any>(`${apiUrl}/${DISCOVERY_DOC_PATH}`)).data;
-        return parseInt(dd.revision, 10) || 0;
-    } catch {
-        console.warn("Unable to get API Revision number, defaulting to zero. Some features may not work properly.");
-        return 0;
-    }
+  try {
+    const dd = (await Axios.get<any>(`${apiUrl}/${DISCOVERY_DOC_PATH}`)).data;
+    return parseInt(dd.revision, 10) || 0;
+  } catch {
+    console.warn(
+      'Unable to get API Revision number, defaulting to zero. Some features may not work properly.'
+    );
+    return 0;
+  }
 };
 
-const removeTrailingSlashes = (config: ClusterConfigJSON): ClusterConfigJSON => {
-    const svcs: any = {};
-    Object.keys(config.Services).forEach((s) => {
-        svcs[s] = config.Services[s];
-        if (svcs[s].hasOwnProperty('ExternalURL')) {
-            svcs[s].ExternalURL = svcs[s].ExternalURL.replace(/\/+$/, '');
-        }
-    });
-    return { ...config, Services: svcs };
+const removeTrailingSlashes = (
+  config: ClusterConfigJSON
+): ClusterConfigJSON => {
+  const svcs: any = {};
+  Object.keys(config.Services).forEach((s) => {
+    svcs[s] = config.Services[s];
+    if (svcs[s].hasOwnProperty('ExternalURL')) {
+      svcs[s].ExternalURL = svcs[s].ExternalURL.replace(/\/+$/, '');
+    }
+  });
+  return { ...config, Services: svcs };
 };
 
 export const fetchConfig = () => {
-    return Axios
-        .get<WorkbenchConfig>(WORKBENCH_CONFIG_URL + "?nocache=" + (new Date()).getTime())
-        .then(response => response.data)
-        .catch(() => {
-            console.warn(`There was an exception getting the Workbench config file at ${WORKBENCH_CONFIG_URL}. Using defaults instead.`);
-            return Promise.resolve(getDefaultConfig());
-        })
-        .then(workbenchConfig => {
-            if (workbenchConfig.API_HOST === undefined) {
-                throw new Error(`Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`);
-            }
-            return Axios.get<ClusterConfigJSON>(getClusterConfigURL(workbenchConfig.API_HOST)).then(async response => {
-                const apiRevision = await getApiRevision(response.data.Services.Controller.ExternalURL.replace(/\/+$/, ''));
-                const config = { ...buildConfig(response.data), apiRevision };
-                const warnLocalConfig = (varName: string) => console.warn(
-                    `A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
-remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`);
+  return Axios.get<WorkbenchConfig>(
+    WORKBENCH_CONFIG_URL + '?nocache=' + new Date().getTime()
+  )
+    .then((response) => response.data)
+    .catch(() => {
+      console.warn(
+        `There was an exception getting the Workbench config file at ${WORKBENCH_CONFIG_URL}. Using defaults instead.`
+      );
+      return Promise.resolve(getDefaultConfig());
+    })
+    .then((workbenchConfig) => {
+      if (workbenchConfig.API_HOST === undefined) {
+        throw new Error(
+          `Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`
+        );
+      }
+      return Axios.get<ClusterConfigJSON>(
+        getClusterConfigURL(workbenchConfig.API_HOST)
+      ).then(async (response) => {
+        const apiRevision = await getApiRevision(
+          response.data.Services.Controller.ExternalURL.replace(/\/+$/, '')
+        );
+        const config = { ...buildConfig(response.data), apiRevision };
+        const warnLocalConfig = (varName: string) =>
+          console.warn(
+            `A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
+remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`
+          );
 
-                // Check if the workbench config has an entry for vocabulary and file viewer URLs
-                // If so, use these values (even if it is an empty string), but print a console warning.
-                // Otherwise, use the cluster config.
-                let fileViewerConfigUrl;
-                if (workbenchConfig.FILE_VIEWERS_CONFIG_URL !== undefined) {
-                    warnLocalConfig("FILE_VIEWERS_CONFIG_URL");
-                    fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
-                }
-                else {
-                    fileViewerConfigUrl = config.clusterConfig.Workbench.FileViewersConfigURL || "/file-viewers-example.json";
-                }
-                config.fileViewersConfigUrl = fileViewerConfigUrl;
+        // Check if the workbench config has an entry for vocabulary and file viewer URLs
+        // If so, use these values (even if it is an empty string), but print a console warning.
+        // Otherwise, use the cluster config.
+        let fileViewerConfigUrl;
+        if (workbenchConfig.FILE_VIEWERS_CONFIG_URL !== undefined) {
+          warnLocalConfig('FILE_VIEWERS_CONFIG_URL');
+          fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
+        } else {
+          fileViewerConfigUrl =
+            config.clusterConfig.Workbench.FileViewersConfigURL ||
+            '/file-viewers-example.json';
+        }
+        config.fileViewersConfigUrl = fileViewerConfigUrl;
 
-                if (workbenchConfig.VOCABULARY_URL !== undefined) {
-                    console.warn(`A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`)
-                }
-                config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
+        if (workbenchConfig.VOCABULARY_URL !== undefined) {
+          console.warn(
+            `A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`
+          );
+        }
+        config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
 
-                return { config, apiHost: workbenchConfig.API_HOST };
-            });
-        });
+        return { config, apiHost: workbenchConfig.API_HOST };
+      });
+    });
 };
 
 // Maps remote cluster hosts and removes the default RemoteCluster entry
-export const mapRemoteHosts = (clusterConfigJSON: ClusterConfigJSON, config: Config) => {
-    config.remoteHosts = {};
-    Object.keys(clusterConfigJSON.RemoteClusters).forEach(k => { config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host; });
-    delete config.remoteHosts["*"];
+export const mapRemoteHosts = (
+  clusterConfigJSON: ClusterConfigJSON,
+  config: Config
+) => {
+  config.remoteHosts = {};
+  Object.keys(clusterConfigJSON.RemoteClusters).forEach((k) => {
+    config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host;
+  });
+  delete config.remoteHosts['*'];
 };
 
-export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): ClusterConfigJSON => ({
-    API: {
-        UnfreezeProjectRequiresAdmin: false,
+export const mockClusterConfigJSON = (
+  config: Partial<ClusterConfigJSON>
+): ClusterConfigJSON => ({
+  API: {
+    UnfreezeProjectRequiresAdmin: false,
+    MaxItemsPerResponse: 1000,
+  },
+  ClusterID: '',
+  RemoteClusters: {},
+  Services: {
+    Controller: { ExternalURL: '' },
+    Workbench1: { ExternalURL: '' },
+    Workbench2: { ExternalURL: '' },
+    Websocket: { ExternalURL: '' },
+    WebDAV: { ExternalURL: '' },
+    WebDAVDownload: { ExternalURL: '' },
+    WebShell: { ExternalURL: '' },
+  },
+  Workbench: {
+    DisableSharingURLsUI: false,
+    ArvadosDocsite: '',
+    FileViewersConfigURL: '',
+    WelcomePageHTML: '',
+    InactivePageHTML: '',
+    SSHHelpPageHTML: '',
+    SSHHelpHostSuffix: '',
+    SiteName: '',
+    IdleTimeout: '0s',
+  },
+  Login: {
+    LoginCluster: '',
+    Google: {
+      Enable: false,
     },
-    ClusterID: "",
-    RemoteClusters: {},
-    Services: {
-        Controller: { ExternalURL: "" },
-        Workbench1: { ExternalURL: "" },
-        Workbench2: { ExternalURL: "" },
-        Websocket: { ExternalURL: "" },
-        WebDAV: { ExternalURL: "" },
-        WebDAVDownload: { ExternalURL: "" },
-        WebShell: { ExternalURL: "" },
+    LDAP: {
+      Enable: false,
     },
-    Workbench: {
-        DisableSharingURLsUI: false,
-        ArvadosDocsite: "",
-        FileViewersConfigURL: "",
-        WelcomePageHTML: "",
-        InactivePageHTML: "",
-        SSHHelpPageHTML: "",
-        SSHHelpHostSuffix: "",
-        SiteName: "",
-        IdleTimeout: "0s",
+    OpenIDConnect: {
+      Enable: false,
     },
-    Login: {
-        LoginCluster: "",
-        Google: {
-            Enable: false,
-        },
-        LDAP: {
-            Enable: false,
-        },
-        OpenIDConnect: {
-            Enable: false,
-        },
-        PAM: {
-            Enable: false,
-        },
-        SSO: {
-            Enable: false,
-        },
-        Test: {
-            Enable: false,
-        },
+    PAM: {
+      Enable: false,
     },
-    Collections: {
-        ForwardSlashNameSubstitution: "",
-        TrustAllContent: false,
+    SSO: {
+      Enable: false,
     },
-    Volumes: {},
-    ...config
+    Test: {
+      Enable: false,
+    },
+  },
+  Collections: {
+    ForwardSlashNameSubstitution: '',
+    TrustAllContent: false,
+  },
+  Volumes: {},
+  ...config,
 });
 
 export const mockConfig = (config: Partial<Config>): Config => ({
-    baseUrl: "",
-    keepWebServiceUrl: "",
-    keepWebInlineServiceUrl: "",
-    remoteHosts: {},
-    rootUrl: "",
-    uuidPrefix: "",
-    websocketUrl: "",
-    workbenchUrl: "",
-    workbench2Url: "",
-    vocabularyUrl: "",
-    fileViewersConfigUrl: "",
-    loginCluster: "",
-    clusterConfig: mockClusterConfigJSON({}),
-    apiRevision: 0,
-    ...config
+  baseUrl: '',
+  keepWebServiceUrl: '',
+  keepWebInlineServiceUrl: '',
+  remoteHosts: {},
+  rootUrl: '',
+  uuidPrefix: '',
+  websocketUrl: '',
+  workbenchUrl: '',
+  workbench2Url: '',
+  vocabularyUrl: '',
+  fileViewersConfigUrl: '',
+  loginCluster: '',
+  clusterConfig: mockClusterConfigJSON({}),
+  apiRevision: 0,
+  ...config,
 });
 
 const getDefaultConfig = (): WorkbenchConfig => {
-    let apiHost = "";
-    const envHost = process.env.REACT_APP_ARVADOS_API_HOST;
-    if (envHost !== undefined) {
-        console.warn(`Using default API host ${envHost}.`);
-        apiHost = envHost;
-    }
-    else {
-        console.warn(`No API host was found in the environment. Workbench may not be able to communicate with Arvados components.`);
-    }
-    return {
-        API_HOST: apiHost,
-        VOCABULARY_URL: undefined,
-        FILE_VIEWERS_CONFIG_URL: undefined,
-    };
+  let apiHost = '';
+  const envHost = process.env.REACT_APP_ARVADOS_API_HOST;
+  if (envHost !== undefined) {
+    console.warn(`Using default API host ${envHost}.`);
+    apiHost = envHost;
+  } else {
+    console.warn(
+      `No API host was found in the environment. Workbench may not be able to communicate with Arvados components.`
+    );
+  }
+  return {
+    API_HOST: apiHost,
+    VOCABULARY_URL: undefined,
+    FILE_VIEWERS_CONFIG_URL: undefined,
+  };
 };
 
-export const ARVADOS_API_PATH = "arvados/v1";
-export const CLUSTER_CONFIG_PATH = "arvados/v1/config";
-export const VOCABULARY_PATH = "arvados/v1/vocabulary";
-export const DISCOVERY_DOC_PATH = "discovery/v1/apis/arvados/v1/rest";
-export const getClusterConfigURL = (apiHost: string) => `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`;
-export const getVocabularyURL = (apiHost: string) => `https://${apiHost}/${VOCABULARY_PATH}?nocache=${(new Date()).getTime()}`;
+export const ARVADOS_API_PATH = 'arvados/v1';
+export const CLUSTER_CONFIG_PATH = 'arvados/v1/config';
+export const VOCABULARY_PATH = 'arvados/v1/vocabulary';
+export const DISCOVERY_DOC_PATH = 'discovery/v1/apis/arvados/v1/rest';
+export const getClusterConfigURL = (apiHost: string) =>
+  `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${new Date().getTime()}`;
+export const getVocabularyURL = (apiHost: string) =>
+  `https://${apiHost}/${VOCABULARY_PATH}?nocache=${new Date().getTime()}`;
index 1fbf17103941f2d7bf819a5a66940a51f832c515..3cacc6c83bd18519eaa7f1af01d63bd56ef44eb3 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { PropertyValue } from "models/search-bar";
-import { Vocabulary, getTagKeyLabel, getTagValueLabel } from "models/vocabulary";
+import { PropertyValue } from 'models/search-bar';
+import {
+  Vocabulary,
+  getTagKeyLabel,
+  getTagValueLabel,
+} from 'models/vocabulary';
 
 export const formatDate = (isoDate?: string | null, utc: boolean = false) => {
-    if (isoDate) {
-        const date = new Date(isoDate);
-        let text: string;
-        if (utc) {
-            text = date.toUTCString();
-        }
-        else {
-            text = date.toLocaleString();
-        }
-        return text === 'Invalid Date' ? "(none)" : text;
+  if (isoDate) {
+    const date = new Date(isoDate);
+    let text: string;
+    if (utc) {
+      text = date.toUTCString();
+    } else {
+      text = date.toLocaleString();
     }
-    return "(none)";
+    return text === 'Invalid Date' ? '(none)' : text;
+  }
+  return '-';
 };
 
 export const formatFileSize = (size?: number | string) => {
-    if (typeof size === "number") {
-        if (size === 0) { return "0 B"; }
-
-        for (const { base, unit } of FILE_SIZES) {
-            if (size >= base) {
-                return `${(size / base).toFixed()} ${unit}`;
-            }
-        }
+  if (typeof size === 'number') {
+    if (size === 0) {
+      return '0 B';
     }
-    if ((typeof size === "string" && size === '') || size === undefined) {
-        return '';
+
+    for (const { base, unit } of FILE_SIZES) {
+      if (size >= base) {
+        return `${(size / base).toFixed()} ${unit}`;
+      }
     }
-    return "0 B";
+  }
+  if ((typeof size === 'string' && size === '') || size === undefined) {
+    return '-';
+  }
+  return '0 B';
 };
 
 export const formatTime = (time: number, seconds?: boolean) => {
-    const minutes = Math.floor(time / (1000 * 60) % 60).toFixed(0);
-    const hours = Math.floor(time / (1000 * 60 * 60)).toFixed(0);
+  const minutes = Math.floor((time / (1000 * 60)) % 60).toFixed(0);
+  const hours = Math.floor(time / (1000 * 60 * 60)).toFixed(0);
 
-    if (seconds) {
-        const seconds = Math.floor(time / (1000) % 60).toFixed(0);
-        return hours + "h " + minutes + "m " + seconds + "s";
-    }
+  if (seconds) {
+    const seconds = Math.floor((time / 1000) % 60).toFixed(0);
+    return hours + 'h ' + minutes + 'm ' + seconds + 's';
+  }
 
-    return hours + "h " + minutes + "m";
+  return hours + 'h ' + minutes + 'm';
 };
 
 export const getTimeDiff = (endTime: string, startTime: string) => {
-    return new Date(endTime).getTime() - new Date(startTime).getTime();
+  return new Date(endTime).getTime() - new Date(startTime).getTime();
 };
 
 export const formatProgress = (loaded: number, total: number) => {
-    const progress = loaded >= 0 && total > 0 ? loaded * 100 / total : 0;
-    return `${progress.toFixed(2)}%`;
+  const progress = loaded >= 0 && total > 0 ? (loaded * 100) / total : 0;
+  return `${progress.toFixed(2)}%`;
 };
 
-export function formatUploadSpeed(prevLoaded: number, loaded: number, prevTime: number, currentTime: number) {
-    const speed = loaded > prevLoaded && currentTime > prevTime
-        ? (loaded - prevLoaded) / (currentTime - prevTime)
-        : 0;
+export function formatUploadSpeed(
+  prevLoaded: number,
+  loaded: number,
+  prevTime: number,
+  currentTime: number
+) {
+  const speed =
+    loaded > prevLoaded && currentTime > prevTime
+      ? (loaded - prevLoaded) / (currentTime - prevTime)
+      : 0;
 
-    return `${(speed / 1000).toFixed(2)} MB/s`;
+  return `${(speed / 1000).toFixed(2)} MB/s`;
 }
 
 const FILE_SIZES = [
-    {
-        base: 1099511627776,
-        unit: "TB"
-    },
-    {
-        base: 1073741824,
-        unit: "GB"
-    },
-    {
-        base: 1048576,
-        unit: "MB"
-    },
-    {
-        base: 1024,
-        unit: "KB"
-    },
-    {
-        base: 1,
-        unit: "B"
-    }
+  {
+    base: 1099511627776,
+    unit: 'TB',
+  },
+  {
+    base: 1073741824,
+    unit: 'GB',
+  },
+  {
+    base: 1048576,
+    unit: 'MB',
+  },
+  {
+    base: 1024,
+    unit: 'KB',
+  },
+  {
+    base: 1,
+    unit: 'B',
+  },
 ];
 
-export const formatPropertyValue = (pv: PropertyValue, vocabulary?: Vocabulary) => {
-    if (vocabulary && pv.keyID && pv.valueID) {
-        return `${getTagKeyLabel(pv.keyID, vocabulary)}: ${getTagValueLabel(pv.keyID, pv.valueID!, vocabulary)}`;
-    }
-    if (pv.key) {
-        return pv.value
-            ? `${pv.key}: ${pv.value}`
-            : pv.key;
-    }
-    return "";
+export const formatPropertyValue = (
+  pv: PropertyValue,
+  vocabulary?: Vocabulary
+) => {
+  if (vocabulary && pv.keyID && pv.valueID) {
+    return `${getTagKeyLabel(pv.keyID, vocabulary)}: ${getTagValueLabel(
+      pv.keyID,
+      pv.valueID!,
+      vocabulary
+    )}`;
+  }
+  if (pv.key) {
+    return pv.value ? `${pv.key}: ${pv.value}` : pv.key;
+  }
+  return '';
 };
 
 export const formatContainerCost = (cost: number): string => {
-    const decimalPlaces = 3;
+  const decimalPlaces = 3;
 
-    const factor = Math.pow(10, decimalPlaces);
-    const rounded = Math.round(cost*factor)/factor;
-    if (cost > 0 && rounded === 0) {
-        // Display min value of 0.001
-        return `$${1/factor}`;
-    } else {
-        // Otherwise use rounded value to proper decimal places
-        return `$${rounded}`;
-    }
+  const factor = Math.pow(10, decimalPlaces);
+  const rounded = Math.round(cost * factor) / factor;
+  if (cost > 0 && rounded === 0) {
+    // Display min value of 0.001
+    return `$${1 / factor}`;
+  } else {
+    // Otherwise use rounded value to proper decimal places
+    return `$${rounded}`;
+  }
 };
index 1594c0366c6d11f8ffb238ce1f107de4c6d15c72..f17ce3936dc7d0c5604047322df99daf107423ba 100644 (file)
@@ -3,12 +3,15 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from "react";
-import { configure, shallow } from "enzyme";
+import { configure, mount } from "enzyme";
 
 import Adapter from "enzyme-adapter-react-16";
 import { Breadcrumbs } from "./breadcrumbs";
-import { Button } from "@material-ui/core";
+import { Button, MuiThemeProvider } from "@material-ui/core";
 import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import { CustomTheme } from 'common/custom-theme';
+import { Provider } from "react-redux";
+import { combineReducers, createStore } from "redux";
 
 configure({ adapter: new Adapter() });
 
@@ -16,36 +19,63 @@ describe("<Breadcrumbs />", () => {
 
     let onClick: () => void;
     let resources = {};
-
+    let store;
     beforeEach(() => {
         onClick = jest.fn();
+        const initialAuthState = {
+            config: {
+                clusterConfig: {
+                    Collections: {
+                        ForwardSlashNameSubstitution: "/"
+                    }
+                }
+            }
+        }
+        store = createStore(combineReducers({
+            auth: (state: any = initialAuthState, action: any) => state,
+        }));
     });
 
     it("renders one item", () => {
         const items = [
-            { label: 'breadcrumb 1' }
+            { label: 'breadcrumb 1', uuid: '1' }
         ];
-        const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+        const breadcrumbs = mount(
+            <Provider store={store}>
+                <MuiThemeProvider theme={CustomTheme}>
+                    <Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />
+                </MuiThemeProvider>
+            </Provider>);
         expect(breadcrumbs.find(Button)).toHaveLength(1);
         expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(0);
     });
 
     it("renders multiple items", () => {
         const items = [
-            { label: 'breadcrumb 1' },
-            { label: 'breadcrumb 2' }
+            { label: 'breadcrumb 1', uuid: '1' },
+            { label: 'breadcrumb 2', uuid: '2' }
         ];
-        const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+        const breadcrumbs = mount(
+            <Provider store={store}>
+                <MuiThemeProvider theme={CustomTheme}>
+                    <Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />
+                </MuiThemeProvider>
+            </Provider>);
         expect(breadcrumbs.find(Button)).toHaveLength(2);
         expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(1);
     });
 
     it("calls onClick with clicked item", () => {
         const items = [
-            { label: 'breadcrumb 1' },
-            { label: 'breadcrumb 2' }
+            { label: 'breadcrumb 1', uuid: '1' },
+            { label: 'breadcrumb 2', uuid: '2' }
         ];
-        const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+        const breadcrumbs = mount(
+            <Provider store={store}>
+                <MuiThemeProvider theme={CustomTheme}>
+                    <Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />
+                </MuiThemeProvider>
+            </Provider>);
         breadcrumbs.find(Button).at(1).simulate('click');
         expect(onClick).toBeCalledWith(items[1]);
     });
index 717966611949c8858548f9206663296139b959f3..baf84d1da253fd2ac266eac6ba7b2215d004a1a2 100644 (file)
@@ -9,43 +9,58 @@ import { withStyles } from '@material-ui/core';
 import { IllegalNamingWarning } from '../warning/warning';
 import { IconType, FreezeIcon } from 'components/icon/icon';
 import grey from '@material-ui/core/colors/grey';
-import { ResourceBreadcrumb } from 'store/breadcrumbs/breadcrumbs-actions';
 import { ResourcesState } from 'store/resources/resources';
+import classNames from 'classnames';
+import { ArvadosTheme } from 'common/custom-theme';
 
 export interface Breadcrumb {
     label: string;
     icon?: IconType;
+    uuid: string;
 }
 
-type CssRules = "item" | "currentItem" | "label" | "icon" | "frozenIcon";
+type CssRules = "item" | "chevron" | "label" | "buttonLabel" | "icon" | "frozenIcon";
 
-const styles: StyleRulesCallback<CssRules> = theme => ({
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     item: {
-        opacity: 0.6
+        borderRadius: '16px',
+        height: '32px',
+        minWidth: '36px',
+        color: theme.customs.colors.grey700,
+        '&.parentItem': {
+            color: `${theme.palette.primary.main}`,
+        },
     },
-    currentItem: {
-        opacity: 1
+    chevron: {
+        color: grey["600"],
     },
     label: {
-        textTransform: "none"
+        textTransform: "none",
+        paddingRight: '3px',
+        paddingLeft: '3px',
+        lineHeight: '1.4',
+    },
+    buttonLabel: {
+        overflow: 'hidden',
+        justifyContent: 'flex-start',
     },
     icon: {
         fontSize: 20,
         color: grey["600"],
-        marginRight: '10px',
+        marginRight: '5px',
     },
     frozenIcon: {
         fontSize: 20,
         color: grey["600"],
-        marginLeft: '10px',
+        marginLeft: '3px',
     },
 });
 
 export interface BreadcrumbsProps {
-    items: ResourceBreadcrumb[];
+    items: Breadcrumb[];
     resources: ResourcesState;
-    onClick: (breadcrumb: ResourceBreadcrumb) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: ResourceBreadcrumb) => void;
+    onClick: (breadcrumb: Breadcrumb) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
 }
 
 export const Breadcrumbs = withStyles(styles)(
@@ -67,8 +82,14 @@ export const Breadcrumbs = withStyles(styles)(
                                 : isLastItem
                                     ? 'breadcrumb-last'
                                     : false}
+                            className={classNames(
+                                isLastItem ? null : 'parentItem',
+                                classes.item
+                            )}
+                            classes={{
+                                label: classes.buttonLabel
+                            }}
                             color="inherit"
-                            className={isLastItem ? classes.currentItem : classes.item}
                             onClick={() => onClick(item)}
                             onContextMenu={event => onContextMenu(event, item)}>
                             <Icon className={classes.icon} />
@@ -83,7 +104,7 @@ export const Breadcrumbs = withStyles(styles)(
                             }
                         </Button>
                     </Tooltip>
-                    {!isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />}
+                    {!isLastItem && <ChevronRightIcon color="inherit" className={classNames('parentItem', classes.chevron)} />}
                 </React.Fragment>
             );
         })
index 5fbef6b62c1e37d2c882c5968f3983d3f5f2fcf6..2323987b7d51f38f52e16e41038e10ec6805d140 100644 (file)
@@ -17,12 +17,18 @@ interface ColumnSelectorDataProps {
     className?: string;
 }
 
-type CssRules = "checkbox";
+type CssRules = "checkbox" | "listItem" | "listItemText";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     checkbox: {
         width: 24,
         height: 24
+    },
+    listItem: {
+        padding: 0
+    },
+    listItemText: {
+        paddingTop: '0.2rem'
     }
 });
 
@@ -39,13 +45,15 @@ export const ColumnSelector = withStyles(styles)(
                             <ListItem
                                 button
                                 key={index}
+                                className={classes.listItem}
                                 onClick={() => onColumnToggle(column)}>
                                 <Checkbox
                                     disableRipple
                                     color="primary"
                                     checked={column.selected}
                                     className={classes.checkbox} />
-                                <ListItemText>
+                                <ListItemText 
+                                    className={classes.listItemText}>
                                     {column.name}
                                 </ListItemText>
                             </ListItem>
@@ -61,3 +69,5 @@ export const ColumnSelectorTrigger = (props: IconButtonProps) =>
             <MenuIcon aria-label="Select columns" />
         </IconButton>
     </Tooltip>;
+
+
index c7a296a60f4aebe5d6ad946265484be9ae148620..e4eef5935d9d68de8722caf832dc07a09eddc624 100644 (file)
@@ -160,7 +160,6 @@ export const DataExplorer = withStyles(styles)(
                 paperKey, fetchMode, currentItemUuid, title,
                 doHidePanel, doMaximizePanel, doUnMaximizePanel, panelName, panelMaximized, elementPath
             } = this.props;
-
             return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
                 <Grid container direction="column" wrap="nowrap" className={classes.container}>
                     <div>
index f2b6f36daf412cd1a62a0a4898d7c56867aed916..b3b2f32ea4d8df5c1866ad2ffa0b15ec33774ffb 100644 (file)
@@ -169,11 +169,10 @@ export const DataTable = withStyles(styles)(
                 onContextMenu={this.handleRowContextMenu(item)}
                 onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)}
                 selected={item === currentItemUuid}>
-                {this.mapVisibleColumns((column, index) => (
-                    <TableCell key={column.key || index} className={currentRoute === '/workflows' ? classes.tableCellWorkflows : classes.tableCell}>
+                {this.mapVisibleColumns((column, index) => <TableCell key={column.key || index} className={currentRoute === '/workflows' ? classes.tableCellWorkflows : classes.tableCell}>
                         {column.render(item)}
                     </TableCell>
-                ))}
+                )}
             </TableRow>;
         }
 
index 54d5b5db2b9513a34579d1d5465a89587c3fb8eb..e6c15144522fa8467ed43296d312a507cf4eba00 100644 (file)
@@ -34,7 +34,8 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
         width: "100%",
         height: "200px",
         position: "relative",
-        border: "1px solid rgba(0, 0, 0, 0.42)"
+        border: "1px solid rgba(0, 0, 0, 0.42)",
+        boxSizing: 'border-box',
     },
     dropzoneBorder: {
         content: "",
index 0fc799dee92b62eb0ab58a6ce100403dc7477193..b50504a6c0b7a7ef4bd8da5bb330a73a18e0406c 100644 (file)
@@ -8,7 +8,7 @@ import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 import { WithDialogProps } from 'store/dialog/with-dialog';
 
-type CssRules = "button" | "lastButton" | "formContainer" | "dialogTitle" | "progressIndicator" | "dialogActions";
+type CssRules = "button" | "lastButton" | "form" | "formContainer" | "dialogTitle" | "progressIndicator" | "dialogActions";
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     button: {
@@ -18,6 +18,12 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
         marginLeft: theme.spacing.unit,
         marginRight: "0",
     },
+    form: {
+        display: 'flex',
+        overflowY: 'auto',
+        flexDirection: 'column',
+        flex: '0 1 auto',
+    },
     formContainer: {
         display: "flex",
         flexDirection: "column",
@@ -57,7 +63,7 @@ export const FormDialog = withStyles(styles)((props: DialogProjectProps) =>
         disableEscapeKeyDown={props.submitting}
         fullWidth
         maxWidth='md'>
-        <form data-cy='form-dialog'>
+        <form data-cy='form-dialog' className={props.classes.form}>
             <DialogTitle className={props.classes.dialogTitle}>
                 {props.dialogTitle}
             </DialogTitle>
index a6c118fc57bb07a3c21eb1d72948a441a5623d93..b559a2ab9003bd530a81cb559ddc11d95dade568 100644 (file)
@@ -7,7 +7,6 @@ import { Badge, SvgIcon, Tooltip } from '@material-ui/core';
 import Add from '@material-ui/icons/Add';
 import ArrowBack from '@material-ui/icons/ArrowBack';
 import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
-import BubbleChart from '@material-ui/icons/BubbleChart';
 import Build from '@material-ui/icons/Build';
 import Cached from '@material-ui/icons/Cached';
 import DescriptionIcon from '@material-ui/icons/Description';
@@ -56,6 +55,7 @@ import RestoreFromTrash from '@material-ui/icons/History';
 import Search from '@material-ui/icons/Search';
 import SettingsApplications from '@material-ui/icons/SettingsApplications';
 import SettingsEthernet from '@material-ui/icons/SettingsEthernet';
+import Settings from '@material-ui/icons/Settings';
 import Star from '@material-ui/icons/Star';
 import StarBorder from '@material-ui/icons/StarBorder';
 import Warning from '@material-ui/icons/Warning';
@@ -86,18 +86,14 @@ library.add(
 );
 
 export const FreezeIcon = (props: any) =>
-    <span {...props}>
-        <span className='fas fa-snowflake' />
-    </span>
+    <SvgIcon {...props}>
+        <path d="M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z" />
+    </SvgIcon>
 
 export const UnfreezeIcon = (props: any) =>
-    <div {...props}>
-        <div className="fa-layers fa-1x fa-fw">
-            <span className="fas fa-slash"
-                data-fa-mask="fas fa-snowflake" data-fa-transform="down-1.5" />
-            <span className="fas fa-slash" />
-        </div>
-    </div>;
+    <SvgIcon {...props}>
+        <path d="M11 5.12L9.29 3.41L10.71 2L12 3.29L13.29 2L14.71 3.41L13 5.12V7.38L15.45 8.82L17.45 7.69L18.07 5.36L20 5.88L19.54 7.65L21.31 8.12L20.79 10.05L18.46 9.43L16.46 10.56V13.26L14.5 11.3V10.56L12.74 9.54L10.73 7.53L11 7.38V5.12M18.46 14.57L16.87 13.67L19.55 16.35L21.3 15.88L20.79 13.95L18.46 14.57M13 16.62V18.88L14.7 20.59L13.29 22L12 20.71L10.71 22L9.29 20.59L11 18.88V16.62L8.55 15.18L6.55 16.31L5.93 18.64L4 18.12L4.47 16.36L2.7 15.89L3.22 13.96L5.55 14.58L7.55 13.45V10.56L5.55 9.43L3.22 10.05L2.7 8.12L4.47 7.65L4 5.89L1.11 3L2.39 1.73L22.11 21.46L20.84 22.73L14.1 16L13 16.62M12 14.89L12.63 14.5L9.5 11.39V13.44L12 14.89Z" />
+    </SvgIcon>
 
 export const PendingIcon = (props: any) =>
     <span {...props}>
@@ -179,7 +175,7 @@ export const InputIcon: IconType = (props) => <MoveToInbox {...props} />;
 export const PaginationDownIcon: IconType = (props) => <ArrowDropDown {...props} />;
 export const PaginationLeftArrowIcon: IconType = (props) => <ChevronLeft {...props} />;
 export const PaginationRightArrowIcon: IconType = (props) => <ChevronRight {...props} />;
-export const ProcessIcon: IconType = (props) => <BubbleChart {...props} />;
+export const ProcessIcon: IconType = (props) => <Settings {...props} />;
 export const ProjectIcon: IconType = (props) => <Folder {...props} />;
 export const FilterGroupIcon: IconType = (props) => <Pageview {...props} />;
 export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
index 877061de37f567bc6a3824451cb2f6951d8a8e99..203748d5e0b2c73ff6241b100718f2c01f5e68b2 100644 (file)
@@ -19,9 +19,12 @@ import { InfoIcon } from 'components/icon/icon';
 import { ReactNodeArray } from 'prop-types';
 import classNames from 'classnames';
 
-type CssRules = 'button' | 'buttonIcon' | 'content';
+type CssRules = 'root' | 'button' | 'buttonIcon' | 'content';
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
+    root: {
+        marginTop: '10px',
+    },
     button: {
         padding: '2px 5px',
         marginRight: '5px',
@@ -206,7 +209,7 @@ const MPVContainerComponent = ({children, panelStates, classes, ...props}: MPVCo
         };
     };
 
-    return <Grid container {...props}>
+    return <Grid container {...props} className={classes.root}>
         <Grid container item direction="row">
             { buttons.map((tgl, idx) => <Grid item key={idx}>{tgl}</Grid>) }
         </Grid>
@@ -221,4 +224,4 @@ const MPVContainerComponent = ({children, panelStates, classes, ...props}: MPVCo
     </Grid>;
 };
 
-export const MPVContainer = withStyles(styles)(MPVContainerComponent);
\ No newline at end of file
+export const MPVContainer = withStyles(styles)(MPVContainerComponent);
index dc6bd84fb01690e857e740f3e0775c02fc0ed6b9..e6e12da841ce06178df785bfce0e7b5a7db029a1 100644 (file)
@@ -2,41 +2,81 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Resource, ResourceKind, ResourceWithProperties } from "./resource";
-import { MountType } from "models/mount-types";
+import { Resource, ResourceKind, ResourceWithProperties } from './resource';
+import { MountType } from 'models/mount-types';
 import { RuntimeConstraints } from './runtime-constraints';
 import { SchedulingParameters } from './scheduling-parameters';
 
 export enum ContainerRequestState {
-    UNCOMMITTED = "Uncommitted",
-    COMMITTED = "Committed",
-    FINAL = "Final"
+  UNCOMMITTED = 'Uncommitted',
+  COMMITTED = 'Committed',
+  FINAL = 'Final',
 }
 
-export interface ContainerRequestResource extends Resource, ResourceWithProperties {
-    kind: ResourceKind.CONTAINER_REQUEST;
-    name: string;
-    description: string;
-    state: ContainerRequestState;
-    requestingContainerUuid: string | null;
-    cumulativeCost: number;
-    containerUuid: string | null;
-    containerCountMax: number;
-    mounts: {[path: string]: MountType};
-    runtimeConstraints: RuntimeConstraints;
-    schedulingParameters: SchedulingParameters;
-    containerImage: string;
-    environment: any;
-    cwd: string;
-    command: string[];
-    outputPath: string;
-    outputName: string;
-    outputTtl: number;
-    priority: number | null;
-    expiresAt: string;
-    useExisting: boolean;
-    logUuid: string | null;
-    outputUuid: string | null;
-    filters: string;
-    containerCount: number;
+export interface ContainerRequestResource
+  extends Resource,
+    ResourceWithProperties {
+  command: string[];
+  containerCountMax: number;
+  containerCount: number;
+  containerImage: string;
+  containerUuid: string | null;
+  cumulativeCost: number;
+  cwd: string;
+  description: string;
+  environment: any;
+  expiresAt: string;
+  filters: string;
+  kind: ResourceKind.CONTAINER_REQUEST;
+  logUuid: string | null;
+  mounts: { [path: string]: MountType };
+  name: string;
+  outputName: string;
+  outputPath: string;
+  outputTtl: number;
+  outputUuid: string | null;
+  priority: number | null;
+  requestingContainerUuid: string | null;
+  runtimeConstraints: RuntimeConstraints;
+  schedulingParameters: SchedulingParameters;
+  state: ContainerRequestState;
+  useExisting: boolean;
 }
+
+// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+export const containerRequestFieldsNoMounts = [
+    "command",
+    "container_count_max",
+    "container_count",
+    "container_image",
+    "container_uuid",
+    "created_at",
+    "cwd",
+    "description",
+    "environment",
+    "etag",
+    "expires_at",
+    "filters",
+    "href",
+    "kind",
+    "log_uuid",
+    "modified_at",
+    "modified_by_client_uuid",
+    "modified_by_user_uuid",
+    "name",
+    "output_name",
+    "output_path",
+    "output_properties",
+    "output_storage_classes",
+    "output_ttl",
+    "output_uuid",
+    "owner_uuid",
+    "priority",
+    "properties",
+    "requesting_container_uuid",
+    "runtime_constraints",
+    "scheduling_parameters",
+    "state",
+    "use_existing",
+    "uuid",
+];
index f5d351acb1176578a3f60a6115a9cb3aeafe66d9..0109ad61459a549fc6d58e18aadd92d5c2137528 100644 (file)
@@ -18,6 +18,7 @@ export enum LogEventType {
     STDERR = 'stderr',
     CONTAINER = 'container',
     KEEPSTORE = 'keepstore',
+    SNIP = 'snip-line', // This type is for internal use only. See #19851
 }
 
 export interface LogResource extends Resource, ResourceWithProperties {
index b404496f9381c0d76ac0ea2c16f6d09bf378795c..f9320a26d700f2582afea62cf512a0fb0ba2b085 100644 (file)
@@ -3,11 +3,13 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ResourceKind } from 'models/resource';
+import { GroupResource } from './group';
 
 export type SearchBarAdvancedFormData = {
     type?: ResourceKind;
     cluster?: string;
     projectUuid?: string;
+    projectObject?: GroupResource;
     inTrash: boolean;
     pastVersions: boolean;
     dateFrom: string;
index da67935a1e5d8a44c5bf2c601c3fc639204ca8ab..bb97665a8c6652a7b89324872a416e3b39c343a5 100644 (file)
@@ -64,7 +64,7 @@ export class FilterBuilder {
         return this.addCondition("properties." + field, "exists", false, "", "", resourcePrefix);
     }
 
-    public addFullTextSearch(value: string) {
+    public addFullTextSearch(value: string, table?: string) {
         const regex = /"[^"]*"/;
         const matches: any[] = [];
 
@@ -76,10 +76,15 @@ export class FilterBuilder {
             match = value.match(regex);
         }
 
+        let searchIn = 'any';
+        if (table) {
+            searchIn = table + ".any";
+        }
+
         const terms = value.trim().split(/(\s+)/).concat(matches);
         terms.forEach(term => {
             if (term !== " ") {
-                this.addCondition("any", "ilike", term, "%", "%");
+                this.addCondition(searchIn, "ilike", term, "%", "%");
             }
         });
         return this;
index e2c420d8d8c6e9f4ebbf112b41daa85dbc559662..d08e7899568ea857807c8d302b39980bc2082098 100644 (file)
@@ -35,13 +35,13 @@ export class CollectionService extends TrashableResourceService<CollectionResour
         return super.get(uuid, showErrors, selectParam, session);
     }
 
-    create(data?: Partial<CollectionResource>) {
-        return super.create({ ...data, preserveVersion: true });
+    create(data?: Partial<CollectionResource>, showErrors?: boolean) {
+        return super.create({ ...data, preserveVersion: true }, showErrors);
     }
 
-    update(uuid: string, data: Partial<CollectionResource>) {
+    update(uuid: string, data: Partial<CollectionResource>, showErrors?: boolean) {
         const select = [...Object.keys(data), 'version', 'modifiedAt'];
-        return super.update(uuid, { ...data, preserveVersion: true }, select);
+        return super.update(uuid, { ...data, preserveVersion: true }, showErrors, select);
     }
 
     async files(uuid: string) {
index c6306779a9ee8cef4bb6eff287c485462f5e898a..d9be8dae9f2a402268217cd8704c0e1d5f538a48 100644 (file)
@@ -26,7 +26,7 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
         ]));
     }
 
-    create(data?: Partial<T>) {
+    create(data?: Partial<T>, showErrors?: boolean) {
         let payload: any;
         if (data !== undefined) {
             this.readOnlyFields.forEach( field => delete data[field] );
@@ -34,10 +34,10 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
                 [this.resourceType.slice(0, -1)]: CommonService.mapKeys(snakeCase)(data),
             };
         }
-        return super.create(payload);
+        return super.create(payload, showErrors);
     }
 
-    update(uuid: string, data: Partial<T>, select?: string[]) {
+    update(uuid: string, data: Partial<T>, showErrors?: boolean, select?: string[]) {
         let payload: any;
         if (data !== undefined) {
             this.readOnlyFields.forEach( field => delete data[field] );
@@ -48,12 +48,12 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
                 payload.select = ['uuid', ...select.map(field => snakeCase(field))];
             };
         }
-        return super.update(uuid, payload);
+        return super.update(uuid, payload, showErrors);
     }
 }
 
 export const getCommonResourceServiceError = (errorResponse: any) => {
-    if ('errors' in errorResponse && 'errorToken' in errorResponse) {
+    if ('errors' in errorResponse) {
         const error = errorResponse.errors.join('');
         switch (true) {
             case /UniqueViolation/.test(error):
index b8e7dc679c21222badea95db2b1be7a9bb4e2138..9a5b767306023ce21673d26427a23d721bd785b6 100644 (file)
@@ -175,12 +175,14 @@ export class CommonService<T> {
         }
     }
 
-    update(uuid: string, data: Partial<T>) {
+    update(uuid: string, data: Partial<T>, showErrors?: boolean) {
         this.validateUuid(uuid);
         return CommonService.defaultResponse(
             this.serverApi
                 .put<T>(`/${this.resourceType}/${uuid}`, data && CommonService.mapKeys(snakeCase)(data)),
-            this.actions
+            this.actions,
+            undefined, // mapKeys
+            showErrors
         );
     }
 }
index b69483cb3b2a45e6f74d6b2e0befcfc6753537e4..025314eb77aac8b261da58dcef4c061700c8da1b 100644 (file)
@@ -5,81 +5,93 @@
 import { CancelToken } from 'axios';
 import { snakeCase, camelCase } from "lodash";
 import { CommonResourceService } from 'services/common-service/common-resource-service';
-import { ListResults, ListArguments } from 'services/common-service/common-service';
-import { AxiosInstance, AxiosRequestConfig } from "axios";
-import { CollectionResource } from "models/collection";
-import { ProjectResource } from "models/project";
-import { ProcessResource } from "models/process";
-import { WorkflowResource } from "models/workflow";
-import { TrashableResourceService } from "services/common-service/trashable-resource-service";
-import { ApiActions } from "services/api/api-actions";
-import { GroupResource } from "models/group";
-import { Session } from "models/session";
+import {
+  ListResults,
+  ListArguments,
+} from 'services/common-service/common-service';
+import { AxiosInstance, AxiosRequestConfig } from 'axios';
+import { CollectionResource } from 'models/collection';
+import { ProjectResource } from 'models/project';
+import { ProcessResource } from 'models/process';
+import { WorkflowResource } from 'models/workflow';
+import { TrashableResourceService } from 'services/common-service/trashable-resource-service';
+import { ApiActions } from 'services/api/api-actions';
+import { GroupResource } from 'models/group';
+import { Session } from 'models/session';
 
 export interface ContentsArguments {
-    limit?: number;
-    offset?: number;
-    order?: string;
-    filters?: string;
-    recursive?: boolean;
-    includeTrash?: boolean;
-    excludeHomeProject?: boolean;
+  limit?: number;
+  offset?: number;
+  order?: string;
+  filters?: string;
+  recursive?: boolean;
+  includeTrash?: boolean;
+  excludeHomeProject?: boolean;
 }
 
 export interface SharedArguments extends ListArguments {
-    include?: string;
+  include?: string;
 }
 
 export type GroupContentsResource =
-    CollectionResource |
-    ProjectResource |
-    ProcessResource |
-    WorkflowResource;
+  | CollectionResource
+  | ProjectResource
+  | ProcessResource
+  | WorkflowResource;
 
-export class GroupsService<T extends GroupResource = GroupResource> extends TrashableResourceService<T> {
+export class GroupsService<
+  T extends GroupResource = GroupResource
+> extends TrashableResourceService<T> {
+  constructor(serverApi: AxiosInstance, actions: ApiActions) {
+    super(serverApi, 'groups', actions);
+  }
 
-    constructor(serverApi: AxiosInstance, actions: ApiActions) {
-        super(serverApi, "groups", actions);
-    }
-
-    async contents(uuid: string, args: ContentsArguments = {}, session?: Session, cancelToken?: CancelToken): Promise<ListResults<GroupContentsResource>> {
-        const { filters, order, ...other } = args;
-        const params = {
-            ...other,
-            filters: filters ? `[${filters}]` : undefined,
-            order: order ? order : undefined
-        };
-        const pathUrl = uuid ? `/${uuid}/contents` : '/contents';
-
-        const cfg: AxiosRequestConfig = { params: CommonResourceService.mapKeys(snakeCase)(params) };
-        if (session) {
-            cfg.baseURL = session.baseUrl;
-            cfg.headers = { 'Authorization': 'Bearer ' + session.token };
-        }
-
-        if (cancelToken) {
-            cfg.cancelToken = cancelToken;
-        }
+async contents(uuid: string, args: ContentsArguments = {}, session?: Session, cancelToken?: CancelToken): Promise<ListResults<GroupContentsResource>> {
+    const { filters, order, ...other } = args;
+    const params = {
+        ...other,
+        filters: filters ? `[${filters}]` : undefined,
+        order: order ? order : undefined
+    };
+    const pathUrl = uuid ? `/${uuid}/contents` : '/contents';
+    const cfg: AxiosRequestConfig = {
+      params: CommonResourceService.mapKeys(snakeCase)(params),
+    };
 
-        const response = await CommonResourceService.defaultResponse(
-            this.serverApi.get(this.resourceType + pathUrl, cfg), this.actions, false
-        );
-
-        return { ...TrashableResourceService.mapKeys(camelCase)(response), clusterId: session && session.clusterId };
+    if (session) {
+      cfg.baseURL = session.baseUrl;
+      cfg.headers = { Authorization: 'Bearer ' + session.token };
     }
 
-    shared(params: SharedArguments = {}): Promise<ListResults<GroupContentsResource>> {
-        return CommonResourceService.defaultResponse(
-            this.serverApi
-                .get(this.resourceType + '/shared', { params }),
-            this.actions
-        );
+    if (cancelToken) {
+      cfg.cancelToken = cancelToken;
     }
+    
+    const response = await CommonResourceService.defaultResponse(
+      this.serverApi.get(this.resourceType + pathUrl, cfg),
+      this.actions,
+      false
+    );
+
+    return {
+      ...TrashableResourceService.mapKeys(camelCase)(response),
+      clusterId: session && session.clusterId,
+    };
+  }
+
+  shared(
+    params: SharedArguments = {}
+  ): Promise<ListResults<GroupContentsResource>> {
+    return CommonResourceService.defaultResponse(
+      this.serverApi.get(this.resourceType + '/shared', { params }),
+      this.actions
+    );
+  }
 }
 
 export enum GroupContentsResourcePrefix {
-    COLLECTION = "collections",
-    PROJECT = "groups",
-    PROCESS = "container_requests",
-    WORKFLOW = "workflows"
+  COLLECTION = 'collections',
+  PROJECT = 'groups',
+  PROCESS = 'container_requests',
+  WORKFLOW = 'workflows',
 }
index 07b083fdab34a06ed7890fa001172886ab960451..442a6ab94fc78dae1974459e6fc20c8013af405f 100644 (file)
@@ -9,9 +9,9 @@ import { ListArguments } from "services/common-service/common-service";
 import { FilterBuilder, joinFilters } from "services/api/filter-builder";
 export class ProjectService extends GroupsService<ProjectResource> {
 
-    create(data: Partial<ProjectResource>) {
+    create(data: Partial<ProjectResource>, showErrors?: boolean) {
         const projectData = { ...data, groupClass: GroupClass.PROJECT };
-        return super.create(projectData);
+        return super.create(projectData, showErrors);
     }
 
     list(args: ListArguments = {}) {
index 5d5e77d6c64396af8628fb9d1ccb9e3aef3cea83..f50c99e79940ab714bff977f6b4801464b909b74 100644 (file)
@@ -23,6 +23,7 @@ import { AllProcessesPanelColumnNames } from "views/all-processes-panel/all-proc
 import { OrderBuilder, OrderDirection } from "services/api/order-builder";
 import { ProcessResource } from "models/process";
 import { SortDirection } from "components/data-table/data-column";
+import { containerRequestFieldsNoMounts } from "models/container-request";
 
 export class AllProcessesPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -66,44 +67,6 @@ export class AllProcessesPanelMiddlewareService extends DataExplorerMiddlewareSe
     }
 }
 
-// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
-export const containerRequestFieldsNoMounts = [
-    "command",
-    "container_count_max",
-    "container_count",
-    "container_image",
-    "container_uuid",
-    "created_at",
-    "cwd",
-    "description",
-    "environment",
-    "etag",
-    "expires_at",
-    "filters",
-    "href",
-    "kind",
-    "log_uuid",
-    "modified_at",
-    "modified_by_client_uuid",
-    "modified_by_user_uuid",
-    "name",
-    "output_name",
-    "output_path",
-    "output_properties",
-    "output_storage_classes",
-    "output_ttl",
-    "output_uuid",
-    "owner_uuid",
-    "priority",
-    "properties",
-    "requesting_container_uuid",
-    "runtime_constraints",
-    "scheduling_parameters",
-    "state",
-    "use_existing",
-    "uuid",
-];
-
 const getParams = ( dataExplorer: DataExplorer ) => ({
     ...dataExplorerToListParams(dataExplorer),
     order: getOrder(dataExplorer),
index 7fc9df774358a73d3d9448e604f83bf8693242c0..6eb8356f5e403541547f1d91bddec9d1aad035ca 100644 (file)
@@ -92,6 +92,9 @@ export const saveApiToken = (token: string) => async (dispatch: Dispatch, getSta
 
     // If the token is from a LoginCluster federation, get user & token data
     // from the token issuing cluster.
+    if (!config) {
+        return;
+    }
     const lc = (config as Config).loginCluster
     const tokenCluster = tokenParts.length === 3
         ? tokenParts[1].substring(0, 5)
@@ -127,7 +130,7 @@ export const getNewExtraToken = (reuseStored: boolean = false) =>
                 const client = await svc.apiClientAuthorizationService.get('current');
                 dispatch(authActions.SET_EXTRA_TOKEN({
                     extraApiToken: extraToken,
-                    extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt): undefined,
+                    extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt) : undefined,
                 }));
                 return extraToken;
             } catch (e) {
@@ -145,7 +148,7 @@ export const getNewExtraToken = (reuseStored: boolean = false) =>
             const newExtraToken = getTokenV2(client);
             dispatch(authActions.SET_EXTRA_TOKEN({
                 extraApiToken: newExtraToken,
-                extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt): undefined,
+                extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt) : undefined,
             }));
             return newExtraToken;
         } catch {
index 08e1a132fd12e23f722cb2bc4e88f6c0374caab0..74cfde00300d545e80db987aab4e7fe0e5d7696c 100644 (file)
@@ -5,7 +5,6 @@
 import { Dispatch } from 'redux';
 import { RootState } from 'store/store';
 import { getUserUuid } from "common/getuser";
-import { Breadcrumb } from 'components/breadcrumbs/breadcrumbs';
 import { getResource } from 'store/resources/resources';
 import { TreePicker } from '../tree-picker/tree-picker';
 import { getSidePanelTreeBranch, getSidePanelTreeNodeAncestorsIds } from '../side-panel-tree/side-panel-tree-actions';
@@ -18,28 +17,53 @@ import { ResourceKind } from 'models/resource';
 import { GroupResource } from 'models/group';
 import { extractUuidKind } from 'models/resource';
 import { UserResource } from 'models/user';
+import { FilterBuilder } from 'services/api/filter-builder';
+import { ProcessResource } from 'models/process';
+import { OrderBuilder } from 'services/api/order-builder';
+import { Breadcrumb } from 'components/breadcrumbs/breadcrumbs';
+import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
+import { CollectionIcon, IconType, ProcessIcon, ProjectIcon } from 'components/icon/icon';
+import { CollectionResource } from 'models/collection';
+import { getSidePanelIcon } from 'views-components/side-panel-tree/side-panel-tree';
 
 export const BREADCRUMBS = 'breadcrumbs';
 
-export interface ResourceBreadcrumb extends Breadcrumb {
-    uuid: string;
-}
-
-export const setBreadcrumbs = (breadcrumbs: any, currentItem?: any) => {
+export const setBreadcrumbs = (breadcrumbs: any, currentItem?: CollectionResource | ContainerRequestResource | GroupResource) => {
     if (currentItem) {
-        const addLastItem = { label: currentItem.name, uuid: currentItem.uuid };
-        breadcrumbs.push(addLastItem);
+        breadcrumbs.push(resourceToBreadcrumb(currentItem));
     }
     return propertiesActions.SET_PROPERTY({ key: BREADCRUMBS, value: breadcrumbs });
 };
 
+const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerRequestResource | GroupResource): IconType | undefined => {
+    switch (resource.kind) {
+        case ResourceKind.PROJECT:
+            return ProjectIcon;
+        case ResourceKind.PROCESS:
+            return ProcessIcon;
+        case ResourceKind.COLLECTION:
+            return CollectionIcon;
+        default:
+            return undefined;
+    }
+}
+
+const resourceToBreadcrumb = (resource: CollectionResource | ContainerRequestResource | GroupResource): Breadcrumb => ({
+    label: resource.name,
+    uuid: resource.uuid,
+    icon: resourceToBreadcrumbIcon(resource),
+})
 
-const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker): ResourceBreadcrumb[] => {
+const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker): Breadcrumb[] => {
     const nodes = getSidePanelTreeBranch(uuid)(treePicker);
     return nodes.map(node =>
         typeof node.value === 'string'
-            ? { label: node.value, uuid: node.id }
-            : { label: node.value.name, uuid: node.value.uuid });
+            ? {
+                label: node.value,
+                uuid: node.id,
+                icon: getSidePanelIcon(node.value)
+            }
+            : resourceToBreadcrumb(node.value));
 };
 
 export const setSidePanelBreadcrumbs = (uuid: string) =>
@@ -52,9 +76,19 @@ export const setSidePanelBreadcrumbs = (uuid: string) =>
 
         if (uuidKind === ResourceKind.COLLECTION) {
             const collectionItem = item ? item : await services.collectionService.get(currentUuid);
+            const parentProcessItem = await getCollectionParent(collectionItem)(services);
+            if (parentProcessItem) {
+                const mainProcessItem = await getProcessParent(parentProcessItem)(services);
+                mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
+                breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+            }
             dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
         } else if (uuidKind === ResourceKind.PROCESS) {
             const processItem = await services.containerRequestService.get(currentUuid);
+            const parentProcessItem = await getProcessParent(processItem)(services);
+            if (parentProcessItem) {
+                breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+            }
             dispatch(setBreadcrumbs(breadcrumbs, processItem));
         }
         dispatch(setBreadcrumbs(breadcrumbs));
@@ -70,28 +104,81 @@ export const setCategoryBreadcrumbs = (uuid: string, category: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const ancestors = await services.ancestorsService.ancestors(uuid, '');
         dispatch(updateResources(ancestors));
-        const initialBreadcrumbs: ResourceBreadcrumb[] = [
-            { label: category, uuid: category }
+        const initialBreadcrumbs: Breadcrumb[] = [
+            {
+                label: category,
+                uuid: category,
+                icon: getSidePanelIcon(category)
+            }
         ];
         const { collectionPanel: { item } } = getState();
         const path = getState().router.location!.pathname;
         const currentUuid = path.split('/')[2];
         const uuidKind = extractUuidKind(currentUuid);
-        const breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
+        let breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
             ancestor.kind === ResourceKind.GROUP
-                ? [...breadcrumbs, { label: ancestor.name, uuid: ancestor.uuid }]
+                ? [...breadcrumbs, resourceToBreadcrumb(ancestor)]
                 : breadcrumbs,
             initialBreadcrumbs);
         if (uuidKind === ResourceKind.COLLECTION) {
             const collectionItem = item ? item : await services.collectionService.get(currentUuid);
+            const parentProcessItem = await getCollectionParent(collectionItem)(services);
+            if (parentProcessItem) {
+                const mainProcessItem = await getProcessParent(parentProcessItem)(services);
+                mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
+                breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+            }
             dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
         } else if (uuidKind === ResourceKind.PROCESS) {
             const processItem = await services.containerRequestService.get(currentUuid);
+            const parentProcessItem = await getProcessParent(processItem)(services);
+            if (parentProcessItem) {
+                breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+            }
             dispatch(setBreadcrumbs(breadcrumbs, processItem));
         }
         dispatch(setBreadcrumbs(breadcrumbs));
     };
 
+const getProcessParent = (childProcess: ContainerRequestResource) =>
+    async (services: ServiceRepository): Promise<ContainerRequestResource | undefined> => {
+        if (childProcess.requestingContainerUuid) {
+            const parentProcesses = await services.containerRequestService.list({
+                order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
+                filters: new FilterBuilder().addEqual('container_uuid', childProcess.requestingContainerUuid).getFilters(),
+                select: containerRequestFieldsNoMounts,
+            });
+            if (parentProcesses.items.length > 0) {
+                return parentProcesses.items[0];
+            } else {
+                return undefined;
+            }
+        } else {
+            return undefined;
+        }
+    }
+
+const getCollectionParent = (collection: CollectionResource) =>
+    async (services: ServiceRepository): Promise<ContainerRequestResource | undefined> => {
+        const parentOutputPromise = services.containerRequestService.list({
+            order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
+            filters: new FilterBuilder().addEqual('output_uuid', collection.uuid).getFilters(),
+            select: containerRequestFieldsNoMounts,
+        });
+        const parentLogPromise = services.containerRequestService.list({
+            order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
+            filters: new FilterBuilder().addEqual('log_uuid', collection.uuid).getFilters(),
+            select: containerRequestFieldsNoMounts,
+        });
+        const [parentOutput, parentLog] = await Promise.all([parentOutputPromise, parentLogPromise]);
+        return parentOutput.items.length > 0 ?
+                parentOutput.items[0] :
+                parentLog.items.length > 0 ?
+                    parentLog.items[0] :
+                    undefined;
+    }
+
+
 export const setProjectBreadcrumbs = (uuid: string) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const ancestors = getSidePanelTreeNodeAncestorsIds(uuid)(getState().treePicker);
@@ -114,15 +201,23 @@ export const setProcessBreadcrumbs = (processUuid: string) =>
     };
 
 export const setGroupsBreadcrumbs = () =>
-    setBreadcrumbs([{ label: SidePanelTreeCategory.GROUPS }]);
+    setBreadcrumbs([{
+        label: SidePanelTreeCategory.GROUPS,
+        uuid: SidePanelTreeCategory.GROUPS,
+        icon: getSidePanelIcon(SidePanelTreeCategory.GROUPS)
+    }]);
 
 export const setGroupDetailsBreadcrumbs = (groupUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 
         const group = getResource<GroupResource>(groupUuid)(getState().resources);
 
-        const breadcrumbs: ResourceBreadcrumb[] = [
-            { label: SidePanelTreeCategory.GROUPS, uuid: SidePanelTreeCategory.GROUPS },
+        const breadcrumbs: Breadcrumb[] = [
+            {
+                label: SidePanelTreeCategory.GROUPS,
+                uuid: SidePanelTreeCategory.GROUPS,
+                icon: getSidePanelIcon(SidePanelTreeCategory.GROUPS)
+            },
             { label: group ? group.name : (await services.groupsService.get(groupUuid)).name, uuid: groupUuid },
         ];
 
@@ -140,13 +235,13 @@ export const setUserProfileBreadcrumbs = (userUuid: string) =>
         try {
             const user = getResource<UserResource>(userUuid)(getState().resources)
                         || await services.userService.get(userUuid, false);
-            const breadcrumbs: ResourceBreadcrumb[] = [
+            const breadcrumbs: Breadcrumb[] = [
                 { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
                 { label: user ? user.username : userUuid, uuid: userUuid },
             ];
             dispatch(setBreadcrumbs(breadcrumbs));
         } catch (e) {
-            const breadcrumbs: ResourceBreadcrumb[] = [
+            const breadcrumbs: Breadcrumb[] = [
                 { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
                 { label: userUuid, uuid: userUuid },
             ];
index 17fecc1e15b740681b8759d84dd7e01dc5bb0a0f..f3d1fd3b77ebcc71df07e169f8dfdc4c79b1d36a 100644 (file)
@@ -59,7 +59,7 @@ export const createCollection = (data: CollectionCreateFormDialogData) =>
         let newCollection: CollectionResource | undefined;
         try {
             dispatch(progressIndicatorActions.START_WORKING(COLLECTION_CREATE_FORM_NAME));
-            newCollection = await services.collectionService.create(data);
+            newCollection = await services.collectionService.create(data, false);
             await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
             dispatch(reset(COLLECTION_CREATE_FORM_NAME));
@@ -68,11 +68,14 @@ export const createCollection = (data: CollectionCreateFormDialogData) =>
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
-            } else if (error === CommonResourceServiceError.NONE) {
+            } else {
                 dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME));
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
+                const errMsg = e.errors
+                    ? e.errors.join('')
+                    : 'There was an error while creating the collection';
                 dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: 'Collection has not been created.',
+                    message: errMsg,
                     hideDuration: 2000,
                     kind: SnackbarKind.ERROR
                 }));
index bf9c64492d79cef6a5f6a75708436866a9700e0b..d955c9478d3d6d3198a2a249e94a142cd8c5a77a 100644 (file)
@@ -52,7 +52,7 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
             name: collection.name,
             storageClassesDesired: collection.storageClassesDesired,
             description: collection.description,
-            properties: collection.properties }
+            properties: collection.properties }, false
         ).then(updatedCollection => {
             updatedCollection = {...cachedCollection, ...updatedCollection};
             dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection));
@@ -72,8 +72,11 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
                 dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' } as FormErrors));
             } else {
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
+                const errMsg = e.errors
+                    ? e.errors.join('')
+                    : 'There was an error while updating the collection';
                 dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: e.errors.join(''),
+                    message: errMsg,
                     hideDuration: 2000,
                     kind: SnackbarKind.ERROR }));
                 }
index 71a6ee6a9577341a639e5df492fc78cf7441459e..0b8d6debe7ca44d9134905e94ea0e803d69b896a 100644 (file)
@@ -2,44 +2,57 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Dispatch, MiddlewareAPI } from "redux";
-import { RootState } from "../store";
-import { DataColumns } from "components/data-table/data-table";
+import { Dispatch, MiddlewareAPI } from 'redux';
+import { RootState } from '../store';
+import { DataColumns } from 'components/data-table/data-table';
 import { DataExplorer } from './data-explorer-reducer';
 import { ListResults } from 'services/common-service/common-service';
-import { createTree } from "models/tree";
-import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
+import { createTree } from 'models/tree';
+import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
 
 export abstract class DataExplorerMiddlewareService {
-    protected readonly id: string;
-
-    protected constructor(id: string) {
-        this.id = id;
-    }
-
-    public getId() {
-        return this.id;
-    }
-
-    public getColumnFilters<T>(columns: DataColumns<T>, columnName: string): DataTableFilters {
-        return getDataExplorerColumnFilters(columns, columnName);
-    }
-
-    abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean): Promise<void>;
+  protected readonly id: string;
+
+  protected constructor(id: string) {
+    this.id = id;
+  }
+
+  public getId() {
+    return this.id;
+  }
+
+  public getColumnFilters<T>(
+    columns: DataColumns<T>,
+    columnName: string
+  ): DataTableFilters {
+    return getDataExplorerColumnFilters(columns, columnName);
+  }
+
+  abstract requestItems(
+    api: MiddlewareAPI<Dispatch, RootState>,
+    criteriaChanged?: boolean
+  ): Promise<void>;
 }
 
-export const getDataExplorerColumnFilters = <T>(columns: DataColumns<T>, columnName: string): DataTableFilters => {
-    const column = columns.find(c => c.name === columnName);
-    return column ? column.filters : createTree();
+export const getDataExplorerColumnFilters = <T>(
+  columns: DataColumns<T>,
+  columnName: string
+): DataTableFilters => {
+  const column = columns.find((c) => c.name === columnName);
+  return column ? column.filters : createTree();
 };
 
 export const dataExplorerToListParams = (dataExplorer: DataExplorer) => ({
-    limit: dataExplorer.rowsPerPage,
-    offset: dataExplorer.page * dataExplorer.rowsPerPage
+  limit: dataExplorer.rowsPerPage,
+  offset: dataExplorer.page * dataExplorer.rowsPerPage,
 });
 
-export const listResultsToDataExplorerItemsMeta = <R>({ itemsAvailable, offset, limit }: ListResults<R>) => ({
-    itemsAvailable,
-    page: Math.floor(offset / limit),
-    rowsPerPage: limit
+export const listResultsToDataExplorerItemsMeta = <R>({
+  itemsAvailable,
+  offset,
+  limit,
+}: ListResults<R>) => ({
+  itemsAvailable,
+  page: Math.floor(offset / limit),
+  rowsPerPage: limit,
 });
index efe51fe3740e73c368e077ee2d0ca0db707b6433..7d0655e4968ee36392f812eb6da354db77467a5d 100644 (file)
@@ -1,4 +1,3 @@
-
 // Copyright (C) The Arvados Authors. All rights reserved.
 //
 // SPDX-License-Identifier: AGPL-3.0
 import { Dispatch } from 'redux';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
-import { Middleware } from "redux";
-import { dataExplorerActions, bindDataExplorerActions, DataTableRequestState } from "./data-explorer-action";
-import { getDataExplorer } from "./data-explorer-reducer";
-import { DataExplorerMiddlewareService } from "./data-explorer-middleware-service";
+import { Middleware } from 'redux';
+import {
+  dataExplorerActions,
+  bindDataExplorerActions,
+  DataTableRequestState,
+} from './data-explorer-action';
+import { getDataExplorer } from './data-explorer-reducer';
+import { DataExplorerMiddlewareService } from './data-explorer-middleware-service';
 
-export const dataExplorerMiddleware = (service: DataExplorerMiddlewareService): Middleware => api => next => {
+export const dataExplorerMiddleware =
+  (service: DataExplorerMiddlewareService): Middleware =>
+  (api) =>
+  (next) => {
     const actions = bindDataExplorerActions(service.getId());
 
-    return action => {
-        const handleAction = <T extends { id: string }>(handler: (data: T) => void) =>
-            (data: T) => {
-                next(action);
-                if (data.id === service.getId()) {
-                    handler(data);
-                }
-            };
-        dataExplorerActions.match(action, {
-            SET_PAGE: handleAction(() => {
-                api.dispatch(actions.REQUEST_ITEMS(false));
-            }),
-            SET_ROWS_PER_PAGE: handleAction(() => {
-                api.dispatch(actions.REQUEST_ITEMS(true));
-            }),
-            SET_FILTERS: handleAction(() => {
-                api.dispatch(actions.RESET_PAGINATION());
-                api.dispatch(actions.REQUEST_ITEMS(true));
-            }),
-            TOGGLE_SORT: handleAction(() => {
-                api.dispatch(actions.REQUEST_ITEMS(true));
-            }),
-            SET_EXPLORER_SEARCH_VALUE: handleAction(() => {
-                api.dispatch(actions.RESET_PAGINATION());
-                api.dispatch(actions.REQUEST_ITEMS(true));
-            }),
-            REQUEST_ITEMS: handleAction(({ criteriaChanged }) => {
-                api.dispatch<any>(async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-                    while (true) {
-                        let de = getDataExplorer(getState().dataExplorer, service.getId());
-                        switch (de.requestState) {
-                            case DataTableRequestState.IDLE:
-                                // Start a new request.
-                                try {
-                                    dispatch(actions.SET_REQUEST_STATE({ requestState: DataTableRequestState.PENDING }));
-                                    await service.requestItems(api, criteriaChanged);
-                                } catch {
-                                    dispatch(actions.SET_REQUEST_STATE({ requestState: DataTableRequestState.NEED_REFRESH }));
-                                }
-                                // Now check if the state is still PENDING, if it moved to NEED_REFRESH
-                                // then we need to reissue requestItems
-                                de = getDataExplorer(getState().dataExplorer, service.getId());
-                                const complete = (de.requestState === DataTableRequestState.PENDING);
-                                dispatch(actions.SET_REQUEST_STATE({ requestState: DataTableRequestState.IDLE }));
-                                if (complete) {
-                                    return;
-                                }
-                                break;
-                            case DataTableRequestState.PENDING:
-                                // State is PENDING, move it to NEED_REFRESH so that when the current request finishes it starts a new one.
-                                dispatch(actions.SET_REQUEST_STATE({ requestState: DataTableRequestState.NEED_REFRESH }));
-                                return;
-                            case DataTableRequestState.NEED_REFRESH:
-                                // Nothing to do right now.
-                                return;
-                        }
+    return (action) => {
+      const handleAction =
+        <T extends { id: string }>(handler: (data: T) => void) =>
+        (data: T) => {
+          next(action);
+          if (data.id === service.getId()) {
+            handler(data);
+          }
+        };
+      dataExplorerActions.match(action, {
+        SET_PAGE: handleAction(() => {
+          api.dispatch(actions.REQUEST_ITEMS(false));
+        }),
+        SET_ROWS_PER_PAGE: handleAction(() => {
+          api.dispatch(actions.REQUEST_ITEMS(true));
+        }),
+        SET_FILTERS: handleAction(() => {
+          api.dispatch(actions.RESET_PAGINATION());
+          api.dispatch(actions.REQUEST_ITEMS(true));
+        }),
+        TOGGLE_SORT: handleAction(() => {
+          api.dispatch(actions.REQUEST_ITEMS(true));
+        }),
+        SET_EXPLORER_SEARCH_VALUE: handleAction(() => {
+          api.dispatch(actions.RESET_PAGINATION());
+          api.dispatch(actions.REQUEST_ITEMS(true));
+        }),
+        REQUEST_ITEMS: handleAction(({ criteriaChanged }) => {
+          api.dispatch<any>(
+            async (
+              dispatch: Dispatch,
+              getState: () => RootState,
+              services: ServiceRepository
+            ) => {
+              while (true) {
+                let de = getDataExplorer(
+                  getState().dataExplorer,
+                  service.getId()
+                );
+                switch (de.requestState) {
+                  case DataTableRequestState.IDLE:
+                    // Start a new request.
+                    try {
+                      dispatch(
+                        actions.SET_REQUEST_STATE({
+                          requestState: DataTableRequestState.PENDING,
+                        })
+                      );
+                      await service.requestItems(api, criteriaChanged);
+                    } catch {
+                      dispatch(
+                        actions.SET_REQUEST_STATE({
+                          requestState: DataTableRequestState.NEED_REFRESH,
+                        })
+                      );
                     }
-                });
-            }),
-            default: () => next(action)
-        });
+                    // Now check if the state is still PENDING, if it moved to NEED_REFRESH
+                    // then we need to reissue requestItems
+                    de = getDataExplorer(
+                      getState().dataExplorer,
+                      service.getId()
+                    );
+                    const complete =
+                      de.requestState === DataTableRequestState.PENDING;
+                    dispatch(
+                      actions.SET_REQUEST_STATE({
+                        requestState: DataTableRequestState.IDLE,
+                      })
+                    );
+                    if (complete) {
+                      return;
+                    }
+                    break;
+                  case DataTableRequestState.PENDING:
+                    // State is PENDING, move it to NEED_REFRESH so that when the current request finishes it starts a new one.
+                    dispatch(
+                      actions.SET_REQUEST_STATE({
+                        requestState: DataTableRequestState.NEED_REFRESH,
+                      })
+                    );
+                    return;
+                  case DataTableRequestState.NEED_REFRESH:
+                    // Nothing to do right now.
+                    return;
+                }
+              }
+            }
+          );
+        }),
+        default: () => next(action),
+      });
     };
-};
+  };
index 1e5cd88fa1299c2eea4f42fed52ad46a4c8445e3..997b6011d5779e90b8900e190f26b3b1b249b431 100644 (file)
 // SPDX-License-Identifier: AGPL-3.0
 
 import {
-    DataColumn,
-    resetSortDirection,
-    SortDirection,
-    toggleSortDirection
-} from "components/data-table/data-column";
-import { DataExplorerAction, dataExplorerActions, DataTableRequestState } from "./data-explorer-action";
-import { DataColumns, DataTableFetchMode } from "components/data-table/data-table";
-import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
+  DataColumn,
+  resetSortDirection,
+  SortDirection,
+  toggleSortDirection,
+} from 'components/data-table/data-column';
+import {
+  DataExplorerAction,
+  dataExplorerActions,
+  DataTableRequestState,
+} from './data-explorer-action';
+import {
+  DataColumns,
+  DataTableFetchMode,
+} from 'components/data-table/data-table';
+import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
 
 export interface DataExplorer {
-    fetchMode: DataTableFetchMode;
-    columns: DataColumns<any>;
-    items: any[];
-    itemsAvailable: number;
-    page: number;
-    rowsPerPage: number;
-    rowsPerPageOptions: number[];
-    searchValue: string;
-    working?: boolean;
-    requestState: DataTableRequestState;
+  fetchMode: DataTableFetchMode;
+  columns: DataColumns<any>;
+  items: any[];
+  itemsAvailable: number;
+  page: number;
+  rowsPerPage: number;
+  rowsPerPageOptions: number[];
+  searchValue: string;
+  working?: boolean;
+  requestState: DataTableRequestState;
 }
 
 export const initialDataExplorer: DataExplorer = {
-    fetchMode: DataTableFetchMode.PAGINATED,
-    columns: [],
-    items: [],
-    itemsAvailable: 0,
-    page: 0,
-    rowsPerPage: 50,
-    rowsPerPageOptions: [10, 20, 50, 100, 200, 500],
-    searchValue: "",
-    requestState: DataTableRequestState.IDLE
+  fetchMode: DataTableFetchMode.PAGINATED,
+  columns: [],
+  items: [],
+  itemsAvailable: 0,
+  page: 0,
+  rowsPerPage: 50,
+  rowsPerPageOptions: [10, 20, 50, 100, 200, 500],
+  searchValue: '',
+  requestState: DataTableRequestState.IDLE,
 };
 
 export type DataExplorerState = Record<string, DataExplorer>;
 
-export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
-    dataExplorerActions.match(action, {
-        CLEAR: ({ id }) =>
-            update(state, id, explorer => ({ ...explorer, page: 0, itemsAvailable: 0, items: [] })),
-
-        RESET_PAGINATION: ({ id }) =>
-            update(state, id, explorer => ({ ...explorer, page: 0 })),
-
-        SET_FETCH_MODE: ({ id, fetchMode }) =>
-            update(state, id, explorer => ({ ...explorer, fetchMode })),
-
-        SET_COLUMNS: ({ id, columns }) =>
-            update(state, id, setColumns(columns)),
-
-        SET_FILTERS: ({ id, columnName, filters }) =>
-            update(state, id, mapColumns(setFilters(columnName, filters))),
-
-        SET_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
-            update(state, id, explorer => ({ ...explorer, items, itemsAvailable, page: page || 0, rowsPerPage })),
-
-        APPEND_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
-            update(state, id, explorer => ({
-                ...explorer,
-                items: state[id].items.concat(items),
-                itemsAvailable: state[id].itemsAvailable + itemsAvailable,
-                page,
-                rowsPerPage
-            })),
-
-        SET_PAGE: ({ id, page }) =>
-            update(state, id, explorer => ({ ...explorer, page })),
-
-        SET_ROWS_PER_PAGE: ({ id, rowsPerPage }) =>
-            update(state, id, explorer => ({ ...explorer, rowsPerPage })),
-
-        SET_EXPLORER_SEARCH_VALUE: ({ id, searchValue }) =>
-            update(state, id, explorer => ({ ...explorer, searchValue })),
-
-        SET_REQUEST_STATE: ({ id, requestState }) =>
-            update(state, id, explorer => ({ ...explorer, requestState })),
-
-        TOGGLE_SORT: ({ id, columnName }) =>
-            update(state, id, mapColumns(toggleSort(columnName))),
-
-        TOGGLE_COLUMN: ({ id, columnName }) =>
-            update(state, id, mapColumns(toggleColumn(columnName))),
-
-        default: () => state
-    });
-
-export const getDataExplorer = (state: DataExplorerState, id: string) =>
-    state[id] || initialDataExplorer;
-
-export const getSortColumn = (dataExplorer: DataExplorer) => dataExplorer.columns.find((c: any) =>
-    !!c.sortDirection && c.sortDirection !== SortDirection.NONE);
-
-const update = (state: DataExplorerState, id: string, updateFn: (dataExplorer: DataExplorer) => DataExplorer) =>
-    ({ ...state, [id]: updateFn(getDataExplorer(state, id)) });
+export const dataExplorerReducer = (
+  state: DataExplorerState = {},
+  action: DataExplorerAction
+) => {
+  return dataExplorerActions.match(action, {
+    CLEAR: ({ id }) =>
+      update(state, id, (explorer) => ({
+        ...explorer,
+        page: 0,
+        itemsAvailable: 0,
+        items: [],
+      })),
+
+    RESET_PAGINATION: ({ id }) =>
+      update(state, id, (explorer) => ({ ...explorer, page: 0 })),
+
+    SET_FETCH_MODE: ({ id, fetchMode }) =>
+      update(state, id, (explorer) => ({ ...explorer, fetchMode })),
+
+    SET_COLUMNS: ({ id, columns }) => update(state, id, setColumns(columns)),
+
+    SET_FILTERS: ({ id, columnName, filters }) =>
+      update(state, id, mapColumns(setFilters(columnName, filters))),
+
+    SET_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
+      update(state, id, (explorer) => ({
+        ...explorer,
+        items,
+        itemsAvailable,
+        page: page || 0,
+        rowsPerPage,
+      })),
+
+    APPEND_ITEMS: ({ id, items, itemsAvailable, page, rowsPerPage }) =>
+      update(state, id, (explorer) => ({
+        ...explorer,
+        items: state[id].items.concat(items),
+        itemsAvailable: state[id].itemsAvailable + itemsAvailable,
+        page,
+        rowsPerPage,
+      })),
+
+    SET_PAGE: ({ id, page }) =>
+      update(state, id, (explorer) => ({ ...explorer, page })),
+
+    SET_ROWS_PER_PAGE: ({ id, rowsPerPage }) =>
+      update(state, id, (explorer) => ({ ...explorer, rowsPerPage })),
+
+    SET_EXPLORER_SEARCH_VALUE: ({ id, searchValue }) =>
+      update(state, id, (explorer) => ({ ...explorer, searchValue })),
+
+    SET_REQUEST_STATE: ({ id, requestState }) =>
+      update(state, id, (explorer) => ({ ...explorer, requestState })),
+
+    TOGGLE_SORT: ({ id, columnName }) =>
+      update(state, id, mapColumns(toggleSort(columnName))),
+
+    TOGGLE_COLUMN: ({ id, columnName }) =>
+      update(state, id, mapColumns(toggleColumn(columnName))),
+
+    default: () => state,
+  });
+};
+export const getDataExplorer = (state: DataExplorerState, id: string) => {
+  const returnValue = state[id] || initialDataExplorer;
+  return returnValue;
+};
 
-const canUpdateColumns = (prevColumns: DataColumns<any>, nextColumns: DataColumns<any>) => {
-    if (prevColumns.length !== nextColumns.length) {
-        return true;
-    }
-    for (let i = 0; i < nextColumns.length; i++) {
-        const pc = prevColumns[i];
-        const nc = nextColumns[i];
-        if (pc.key !== nc.key || pc.name !== nc.name) {
-            return true;
-        }
+export const getSortColumn = (dataExplorer: DataExplorer) =>
+  dataExplorer.columns.find(
+    (c: any) => !!c.sortDirection && c.sortDirection !== SortDirection.NONE
+  );
+
+const update = (
+  state: DataExplorerState,
+  id: string,
+  updateFn: (dataExplorer: DataExplorer) => DataExplorer
+) => ({ ...state, [id]: updateFn(getDataExplorer(state, id)) });
+
+const canUpdateColumns = (
+  prevColumns: DataColumns<any>,
+  nextColumns: DataColumns<any>
+) => {
+  if (prevColumns.length !== nextColumns.length) {
+    return true;
+  }
+  for (let i = 0; i < nextColumns.length; i++) {
+    const pc = prevColumns[i];
+    const nc = nextColumns[i];
+    if (pc.key !== nc.key || pc.name !== nc.name) {
+      return true;
     }
-    return false;
+  }
+  return false;
 };
 
-const setColumns = (columns: DataColumns<any>) =>
-    (dataExplorer: DataExplorer) =>
-        ({ ...dataExplorer, columns: canUpdateColumns(dataExplorer.columns, columns) ? columns : dataExplorer.columns });
-
-const mapColumns = (mapFn: (column: DataColumn<any>) => DataColumn<any>) =>
-    (dataExplorer: DataExplorer) =>
-        ({ ...dataExplorer, columns: dataExplorer.columns.map(mapFn) });
-
-const toggleSort = (columnName: string) =>
-    (column: DataColumn<any>) => column.name === columnName
-        ? toggleSortDirection(column)
-        : resetSortDirection(column);
-
-const toggleColumn = (columnName: string) =>
-    (column: DataColumn<any>) => column.name === columnName
-        ? { ...column, selected: !column.selected }
-        : column;
-
-const setFilters = (columnName: string, filters: DataTableFilters) =>
-    (column: DataColumn<any>) => column.name === columnName
-        ? { ...column, filters }
-        : column;
+const setColumns =
+  (columns: DataColumns<any>) => (dataExplorer: DataExplorer) => ({
+    ...dataExplorer,
+    columns: canUpdateColumns(dataExplorer.columns, columns)
+      ? columns
+      : dataExplorer.columns,
+  });
+
+const mapColumns =
+  (mapFn: (column: DataColumn<any>) => DataColumn<any>) =>
+  (dataExplorer: DataExplorer) => ({
+    ...dataExplorer,
+    columns: dataExplorer.columns.map(mapFn),
+  });
+
+const toggleSort = (columnName: string) => (column: DataColumn<any>) =>
+  column.name === columnName
+    ? toggleSortDirection(column)
+    : resetSortDirection(column);
+
+const toggleColumn = (columnName: string) => (column: DataColumn<any>) =>
+  column.name === columnName
+    ? { ...column, selected: !column.selected }
+    : column;
+
+const setFilters =
+  (columnName: string, filters: DataTableFilters) =>
+  (column: DataColumn<any>) =>
+    column.name === columnName ? { ...column, filters } : column;
index d4f5ab59244cabccea950b3bbcaf119feb63e83b..16177f18a62edaba2db7ee12f75e921dd6ebe946 100644 (file)
@@ -8,7 +8,7 @@ import { LogEventType } from 'models/log';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { Dispatch } from 'redux';
-import { groupBy } from 'lodash';
+import { groupBy, min, reverse } from 'lodash';
 import { LogResource } from 'models/log';
 import { LogService } from 'services/log-service/log-service';
 import { ResourceEventMessage } from 'websocket/resource-event-message';
@@ -34,8 +34,9 @@ export const initProcessLogsPanel = (processUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => {
         dispatch(processLogsPanelActions.RESET_PROCESS_LOGS_PANEL());
         const process = getProcess(processUuid)(getState().resources);
+        const maxPageSize = getState().auth.config.clusterConfig.API.MaxItemsPerResponse;
         if (process && process.container) {
-            const logResources = await loadContainerLogs(process.container.uuid, logService);
+            const logResources = await loadContainerLogs(process.container.uuid, logService, maxPageSize);
             const initialState = createInitialLogPanelState(logResources);
             dispatch(processLogsPanelActions.INIT_PROCESS_LOGS_PANEL(initialState));
         }
@@ -69,20 +70,44 @@ export const addProcessLogsPanelItem = (message: ResourceEventMessage<{ text: st
         }
     };
 
-const loadContainerLogs = async (containerUuid: string, logService: LogService) => {
+const loadContainerLogs = async (containerUuid: string, logService: LogService, maxPageSize: number) => {
     const requestFilters = new FilterBuilder()
         .addEqual('object_uuid', containerUuid)
         .addIn('event_type', PROCESS_PANEL_LOG_EVENT_TYPES)
         .getFilters();
-    const requestOrder = new OrderBuilder<LogResource>()
+    const requestOrderAsc = new OrderBuilder<LogResource>()
         .addAsc('eventAt')
         .getOrder();
-    const requestParams = {
-        limit: MAX_AMOUNT_OF_LOGS,
+    const requestOrderDesc = new OrderBuilder<LogResource>()
+        .addDesc('eventAt')
+        .getOrder();
+    const { items, itemsAvailable } = await logService.list({
+        limit: maxPageSize,
         filters: requestFilters,
-        order: requestOrder,
-    };
-    const { items } = await logService.list(requestParams);
+        order: requestOrderAsc,
+    });
+
+    // Request additional logs if necessary
+    const remainingLogs = itemsAvailable - items.length;
+    if (remainingLogs > 0) {
+        const { items: itemsLast } = await logService.list({
+            limit: min([maxPageSize, remainingLogs]),
+            filters: requestFilters,
+            order: requestOrderDesc,
+            count: 'none',
+        })
+        if (remainingLogs - itemsLast.length > 0) {
+            const snipLine = {
+                ...items[items.length - 1],
+                eventType: LogEventType.SNIP,
+                properties: {
+                    text: `================ 8< ================ 8< ========= Some log(s) were skipped ========= 8< ================ 8< ================`
+                },
+            }
+            return [...items, snipLine, ...reverse(itemsLast)];
+        }
+        return [...items, ...reverse(itemsLast)];
+    }
     return items;
 };
 
@@ -98,7 +123,11 @@ const createInitialLogPanelState = (logResources: LogResource[]) => {
             ...grouped,
             [key]: logsToLines(groupedLogResources[key])
         }), {});
-    const filters = [MAIN_FILTER_TYPE, ALL_FILTER_TYPE, ...Object.keys(groupedLogs)];
+    const filters = [
+        MAIN_FILTER_TYPE,
+        ALL_FILTER_TYPE,
+        ...Object.keys(groupedLogs)
+    ].filter(e => e !== LogEventType.SNIP);
     const logs = {
         [MAIN_FILTER_TYPE]: mainLogs,
         [ALL_FILTER_TYPE]: allLogs,
@@ -120,8 +149,6 @@ export const navigateToLogCollection = (uuid: string) =>
         }
     };
 
-const MAX_AMOUNT_OF_LOGS = 10000;
-
 const ALL_FILTER_TYPE = 'All logs';
 
 const MAIN_FILTER_TYPE = 'Main logs';
@@ -129,6 +156,7 @@ const MAIN_EVENT_TYPES = [
     LogEventType.CRUNCH_RUN,
     LogEventType.STDERR,
     LogEventType.STDOUT,
+    LogEventType.SNIP,
 ];
 
 const PROCESS_PANEL_LOG_EVENT_TYPES = [
@@ -142,4 +170,5 @@ const PROCESS_PANEL_LOG_EVENT_TYPES = [
     LogEventType.STDOUT,
     LogEventType.CONTAINER,
     LogEventType.KEEPSTORE,
+    LogEventType.SNIP,
 ];
index 23eaf7a4a56aaa077083f0f738c37ad5f81ebb6e..c15c37483ed88e9444618ae4ef13cebf5cb91b67 100644 (file)
@@ -20,6 +20,8 @@ import { ServiceRepository } from 'services/services';
 import { matchProjectRoute, matchRunProcessRoute } from 'routes/routes';
 import { RouterState } from "react-router-redux";
 import { GroupClass } from "models/group";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 
 export interface ProjectCreateFormDialogData {
     ownerUuid: string;
@@ -65,7 +67,8 @@ export const createProject = (project: Partial<ProjectResource>) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(PROJECT_CREATE_FORM_NAME));
         try {
-            const newProject = await services.projectService.create(project);
+            dispatch(progressIndicatorActions.START_WORKING(PROJECT_CREATE_FORM_NAME));
+            const newProject = await services.projectService.create(project, false);
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME }));
             dispatch(reset(PROJECT_CREATE_FORM_NAME));
             return newProject;
@@ -73,7 +76,20 @@ export const createProject = (project: Partial<ProjectResource>) =>
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
+            } else {
+                dispatch(stopSubmit(PROJECT_CREATE_FORM_NAME));
+                dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_CREATE_FORM_NAME }));
+                const errMsg = e.errors
+                    ? e.errors.join('')
+                    : 'There was an error while creating the collection';
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: errMsg,
+                    hideDuration: 2000,
+                    kind: SnackbarKind.ERROR
+                }));
             }
             return undefined;
+        } finally {
+            dispatch(progressIndicatorActions.STOP_WORKING(PROJECT_CREATE_FORM_NAME));
         }
     };
index a6e6748535596e26d09c9b3f948ebf1cabe186dd..057c7cfac59794b95dc7259b66c965551df5c28e 100644 (file)
@@ -24,6 +24,7 @@ import { Participant } from "views-components/sharing-dialog/participant-select"
 import { ProjectProperties } from "./project-create-actions";
 import { getResource } from "store/resources/resources";
 import { ProjectResource } from "models/project";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
@@ -61,7 +62,8 @@ export const updateProject = (project: ProjectUpdateFormDialogData) =>
                     name: project.name,
                     description: project.description,
                     properties: project.properties,
-                });
+                },
+                false);
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(reset(PROJECT_UPDATE_FORM_NAME));
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
@@ -70,7 +72,16 @@ export const updateProject = (project: ProjectUpdateFormDialogData) =>
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
+            } else {
+                dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
+                const errMsg = e.errors
+                    ? e.errors.join('')
+                    : 'There was an error while updating the project';
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: errMsg,
+                    hideDuration: 2000,
+                    kind: SnackbarKind.ERROR }));
             }
-            return ;
+            return;
         }
     };
index 8b03ddd765e3cfe7b9002f97f0e4fd8b371a6be7..af40e86ade09bfc30698eded0bb1b8802dfac577 100644 (file)
@@ -89,6 +89,9 @@ export const searchAdvancedData = (data: SearchBarAdvancedFormData) =>
 
 export const setSearchValueFromAdvancedData = (data: SearchBarAdvancedFormData, prevData?: SearchBarAdvancedFormData) =>
     (dispatch: Dispatch, getState: () => RootState) => {
+        if (data.projectObject) {
+            data.projectUuid = data.projectObject.uuid;
+        }
         const searchValue = getState().searchBar.searchValue;
         const value = getQueryFromAdvancedData({
             ...data,
@@ -98,8 +101,11 @@ export const setSearchValueFromAdvancedData = (data: SearchBarAdvancedFormData,
     };
 
 export const setAdvancedDataFromSearchValue = (search: string, vocabulary: Vocabulary) =>
-    async (dispatch: Dispatch) => {
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const data = getAdvancedDataFromQuery(search, vocabulary);
+        if (data.projectUuid) {
+            data.projectObject = await services.projectService.get(data.projectUuid);
+        }
         dispatch<any>(initialize(SEARCH_BAR_ADVANCED_FORM_NAME, data));
         if (data.projectUuid) {
             await dispatch<any>(activateSearchBarProject(data.projectUuid));
@@ -227,11 +233,11 @@ const searchGroups = (searchValue: string, limit: number, useCancel = false) =>
                         recursive: true
                     }, session, cancelTokens[index].token);
                 }));
-    
+
                 cancelTokens = [];
-    
+
                 const items = lists.reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
-    
+
                 if (lists.filter(list => !!(list as any).items).length !== lists.length) {
                     dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
                 } else {
@@ -290,7 +296,7 @@ export const getQueryFromAdvancedData = (data: SearchBarAdvancedFormData, prevDa
         };
         (data.properties || []).forEach(p =>
             fo[`prop-"${p.keyID || p.key}":"${p.valueID || p.value}"`] = `"${p.valueID || p.value}"`
-            );
+        );
         return fo;
     };
 
@@ -308,9 +314,9 @@ export const getQueryFromAdvancedData = (data: SearchBarAdvancedFormData, prevDa
             [`has:"${p.keyID || p.key}"`, `prop-"${p.keyID || p.key}":"${p.valueID || p.value}"`]
         ));
 
-    const modified = getModifiedKeysValues(flatData(data), prevData ? flatData(prevData):{});
+    const modified = getModifiedKeysValues(flatData(data), prevData ? flatData(prevData) : {});
     value = buildQueryFromKeyMap(
-        {searchValue: data.searchValue, ...modified} as SearchBarAdvancedFormData, keyMap);
+        { searchValue: data.searchValue, ...modified } as SearchBarAdvancedFormData, keyMap);
 
     value = value.trim();
     return value;
index 6ab25d6ea5cc3f83c6be151669e57a51bbba6d56..b0bad2f3c294285e82e9d18030b79421bef41376 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { getTreePicker, TreePicker } from "store/tree-picker/tree-picker";
-import { getNode, getNodeAncestorsIds, initTreeNode, TreeNodeStatus } from "models/tree";
+import { getNode, getNodeAncestorsIds, initTreeNode } from "models/tree";
 import { Dispatch } from "redux";
 import { RootState } from "store/store";
 import { getUserUuid } from "common/getuser";
@@ -66,8 +66,10 @@ export const expandSearchBarTreeItem = (id: string) =>
     };
 
 export const activateSearchBarProject = (id: string) =>
-    async (dispatch: Dispatch, getState: () => RootState) => {
-        const { treePicker } = getState();
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+
+
+        /*const { treePicker } = getState();
         const node = getSearchBarTreeNode(id)(treePicker);
         if (node && node.status !== TreeNodeStatus.LOADED) {
             await dispatch<any>(loadSearchBarTreeProjects(id));
@@ -78,7 +80,7 @@ export const activateSearchBarProject = (id: string) =>
             ids: getSearchBarTreeNodeAncestorsIds(id)(treePicker),
             pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID
         }));
-        dispatch<any>(expandSearchBarTreeItem(id));
+        dispatch<any>(expandSearchBarTreeItem(id));*/
     };
 
 
index dd56b42870d2486a5d887caee3f54a3b134eb447..f6015fbfa0c9d4f6201373eb4bed1f5c71708a05 100644 (file)
@@ -20,7 +20,7 @@ import { CategoriesListReducer } from 'common/plugintypes';
 import { pluginConfig } from 'plugins';
 
 export enum SidePanelTreeCategory {
-    PROJECTS = 'Projects',
+    PROJECTS = 'Home Projects',
     SHARED_WITH_ME = 'Shared with me',
     PUBLIC_FAVORITES = 'Public Favorites',
     FAVORITES = 'My Favorites',
index 94f110a09563ab17537b44445b964f650fef2ce5..1b9e05a904714cf6a84c8b2ae8eeecb5c4fba4fc 100644 (file)
@@ -26,7 +26,8 @@ import { AllProcessesPanelMiddlewareService } from "./all-processes-panel/all-pr
 import { collectionPanelReducer } from './collection-panel/collection-panel-reducer';
 import { dialogReducer } from './dialog/dialog-reducer';
 import { ServiceRepository } from "services/services";
-import { treePickerReducer } from './tree-picker/tree-picker-reducer';
+import { treePickerReducer, treePickerSearchReducer } from './tree-picker/tree-picker-reducer';
+import { treePickerSearchMiddleware } from './tree-picker/tree-picker-middleware';
 import { resourcesReducer } from 'store/resources/resources-reducer';
 import { propertiesReducer } from './properties/properties-reducer';
 import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
@@ -76,7 +77,7 @@ import { MiddlewareListReducer } from 'common/plugintypes';
 
 declare global {
     interface Window {
-      __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
+        __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
     }
 }
 
@@ -174,6 +175,7 @@ export function configureStore(history: History, services: ServiceRepository, co
         publicFavoritesMiddleware,
         collectionsContentAddress,
         subprocessMiddleware,
+        treePickerSearchMiddleware
     ];
 
     const reduceMiddlewaresFn: (a: Middleware[],
@@ -203,6 +205,7 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     router: routerReducer,
     snackbar: snackbarReducer,
     treePicker: treePickerReducer,
+    treePickerSearch: treePickerSearchReducer,
     fileUploader: fileUploaderReducer,
     processPanel: processPanelReducer,
     progressIndicator: progressIndicatorReducer,
index dd303233117af7a74e77f8bfa34b381ac183ddfa..0f306ac076a475c525cec24509df5c3da507dafb 100644 (file)
@@ -21,10 +21,9 @@ import { FilterBuilder, joinFilters } from 'services/api/filter-builder';
 import { subprocessPanelActions } from './subprocess-panel-actions';
 import { DataColumns } from 'components/data-table/data-table';
 import { ProcessStatusFilter, buildProcessStatusFilters } from '../resource-type-filters/resource-type-filters';
-import { ContainerRequestResource } from 'models/container-request';
+import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
 import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
 import { loadMissingProcessesInformation } from '../project-panel/project-panel-middleware-service';
-import { containerRequestFieldsNoMounts } from 'store/all-processes-panel/all-processes-panel-middleware-service';
 
 export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
index 23f548cd679b89025f58446ca7429ef8c74e07d9..460a23e3d778c0d3670e5d360ef06bdb00a0f408 100644 (file)
@@ -14,7 +14,7 @@ import { pipe, values } from 'lodash/fp';
 import { ResourceKind } from 'models/resource';
 import { GroupContentsResource } from 'services/groups-service/groups-service';
 import { getTreePicker, TreePicker } from './tree-picker';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from './tree-picker-middleware';
 import { OrderBuilder } from 'services/api/order-builder';
 import { ProjectResource } from 'models/project';
 import { mapTree } from '../../models/tree';
@@ -28,6 +28,7 @@ export const treePickerActions = unionize({
     LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
     APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
     TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
+    EXPAND_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
     ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
     DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
     TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
@@ -39,11 +40,27 @@ export const treePickerActions = unionize({
 
 export type TreePickerAction = UnionOf<typeof treePickerActions>;
 
+export interface LoadProjectParams {
+    includeCollections?: boolean;
+    includeFiles?: boolean;
+    includeFilterGroups?: boolean;
+    options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
+}
+
+export const treePickerSearchActions = unionize({
+    SET_TREE_PICKER_PROJECT_SEARCH: ofType<{ pickerId: string, projectSearchValue: string }>(),
+    SET_TREE_PICKER_COLLECTION_FILTER: ofType<{ pickerId: string, collectionFilterValue: string }>(),
+    SET_TREE_PICKER_LOAD_PARAMS: ofType<{ pickerId: string, params: LoadProjectParams }>(),
+});
+
+export type TreePickerSearchAction = UnionOf<typeof treePickerSearchActions>;
+
 export const getProjectsTreePickerIds = (pickerId: string) => ({
     home: `${pickerId}_home`,
     shared: `${pickerId}_shared`,
     favorites: `${pickerId}_favorites`,
-    publicFavorites: `${pickerId}_publicFavorites`
+    publicFavorites: `${pickerId}_publicFavorites`,
+    search: `${pickerId}_search`,
 });
 
 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
@@ -71,11 +88,12 @@ export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker)
 
 export const initProjectsTreePicker = (pickerId: string) =>
     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
-        const { home, shared, favorites, publicFavorites } = getProjectsTreePickerIds(pickerId);
+        const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
         dispatch<any>(initUserProject(home));
         dispatch<any>(initSharedProject(shared));
         dispatch<any>(initFavoritesProject(favorites));
         dispatch<any>(initPublicFavoritesProject(publicFavorites));
+        dispatch<any>(initSearchProject(search));
     };
 
 interface ReceiveTreePickerDataParams<T> {
@@ -93,55 +111,93 @@ export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>)
             nodes: data.map(item => initTreeNode(extractNodeData(item))),
             pickerId,
         }));
-        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
+        dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE({ id, pickerId }));
     };
 
-interface LoadProjectParams {
+interface LoadProjectParamsWithId extends LoadProjectParams {
     id: string;
     pickerId: string;
-    includeCollections?: boolean;
-    includeFiles?: boolean;
-    includeFilterGroups?: boolean;
     loadShared?: boolean;
-    options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
+    searchProjects?: boolean;
 }
-export const loadProject = (params: LoadProjectParams) =>
-    async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
-        const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options } = params;
+
+export const loadProject = (params: LoadProjectParamsWithId) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options, searchProjects = false } = params;
 
         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
 
-        const filters = pipe(
-            (fb: FilterBuilder) => includeCollections
-                ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
-                : fb.addIsA('uuid', [ResourceKind.PROJECT]),
-            fb => fb.getFilters(),
-        )(new FilterBuilder());
+        let filterB = new FilterBuilder();
+
+        filterB = (includeCollections && !searchProjects)
+            ? filterB.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
+            : filterB.addIsA('uuid', [ResourceKind.PROJECT]);
+
+        const state = getState();
+
+        if (state.treePickerSearch.collectionFilterValues[pickerId]) {
+            filterB = filterB.addFullTextSearch(state.treePickerSearch.collectionFilterValues[pickerId], 'collections');
+        } else {
+            filterB = filterB.addNotIn("collections.properties.type", ["intermediate", "log"]);
+        }
+
+        if (searchProjects && state.treePickerSearch.projectSearchValues[pickerId]) {
+            filterB = filterB.addFullTextSearch(state.treePickerSearch.projectSearchValues[pickerId], 'groups');
+        }
+
+        const filters = filterB.getFilters();
+
+        const itemLimit = 200;
+
+        const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
+
+        if (itemsAvailable > itemLimit) {
+            items.push({
+                uuid: "more-items-available",
+                kind: ResourceKind.WORKFLOW,
+                name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
+                description: "",
+                definition: "",
+                ownerUuid: "",
+                createdAt: "",
+                modifiedByClientUuid: "",
+                modifiedByUserUuid: "",
+                modifiedAt: "",
+                href: "",
+                etag: ""
+            });
+        }
 
-        const { items } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined });
         dispatch<any>(receiveTreePickerData<GroupContentsResource>({
             id,
             pickerId,
             data: items.filter((item) => {
-                    if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
-                        return false;
-                    }
+                if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
+                    return false;
+                }
 
-                    if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
-                        return false;
-                    }
+                if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
+                    return false;
+                }
 
-                    return true;
-                }),
-            extractNodeData: item => ({
-                id: item.uuid,
-                value: item,
-                status: item.kind === ResourceKind.PROJECT
-                    ? TreeNodeStatus.INITIAL
-                    : includeFiles
-                        ? TreeNodeStatus.INITIAL
-                        : TreeNodeStatus.LOADED
+                return true;
             }),
+            extractNodeData: item => (
+                item.uuid === "more-items-available" ?
+                    {
+                        id: item.uuid,
+                        value: item,
+                        status: TreeNodeStatus.LOADED
+                    }
+                    : {
+                        id: item.uuid,
+                        value: item,
+                        status: item.kind === ResourceKind.PROJECT
+                            ? TreeNodeStatus.INITIAL
+                            : includeFiles
+                                ? TreeNodeStatus.INITIAL
+                                : TreeNodeStatus.LOADED
+                    }),
         }));
     };
 
@@ -154,7 +210,7 @@ export const loadCollection = (id: string, pickerId: string) =>
 
             const node = getNode(id)(picker);
             if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
-                const files = await services.collectionService.files(node.value.portableDataHash);
+                const files = await services.collectionService.files(node.value.uuid);
                 const tree = createCollectionFilesTree(files);
                 const sorted = sortFilesTree(tree);
                 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
@@ -179,7 +235,7 @@ export const initUserProject = (pickerId: string) =>
             dispatch(receiveTreePickerData({
                 id: '',
                 pickerId,
-                data: [{ uuid, name: 'Projects' }],
+                data: [{ uuid, name: 'Home Projects' }],
                 extractNodeData: value => ({
                     id: value.uuid,
                     status: TreeNodeStatus.INITIAL,
@@ -188,7 +244,7 @@ export const initUserProject = (pickerId: string) =>
             }));
         }
     };
-export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean } ) =>
+export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const uuid = getUserUuid(getState());
         if (uuid) {
@@ -241,6 +297,22 @@ export const initPublicFavoritesProject = (pickerId: string) =>
         }));
     };
 
+export const SEARCH_PROJECT_ID = 'Search all Projects';
+export const initSearchProject = (pickerId: string) =>
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(receiveTreePickerData({
+            id: '',
+            pickerId,
+            data: [{ uuid: SEARCH_PROJECT_ID, name: SEARCH_PROJECT_ID }],
+            extractNodeData: value => ({
+                id: value.uuid,
+                status: TreeNodeStatus.INITIAL,
+                value,
+            }),
+        }));
+    };
+
+
 interface LoadFavoritesProjectParams {
     pickerId: string;
     includeCollections?: boolean;
diff --git a/src/store/tree-picker/tree-picker-middleware.ts b/src/store/tree-picker/tree-picker-middleware.ts
new file mode 100644 (file)
index 0000000..8fa3ee4
--- /dev/null
@@ -0,0 +1,118 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from 'store/store';
+import { ServiceRepository } from 'services/services';
+import { Middleware } from "redux";
+import { getNode, getNodeDescendantsIds, TreeNodeStatus } from 'models/tree';
+import { getTreePicker } from './tree-picker';
+import {
+    treePickerSearchActions, loadProject, loadFavoritesProject, loadPublicFavoritesProject,
+    SHARED_PROJECT_ID, FAVORITES_PROJECT_ID, PUBLIC_FAVORITES_PROJECT_ID, SEARCH_PROJECT_ID
+} from "./tree-picker-actions";
+import { LinkResource } from "models/link";
+import { GroupContentsResource } from 'services/groups-service/groups-service';
+import { CollectionDirectory, CollectionFile } from 'models/collection-file';
+
+export interface ProjectsTreePickerRootItem {
+    id: string;
+    name: string;
+}
+
+export type ProjectsTreePickerItem = ProjectsTreePickerRootItem | GroupContentsResource | CollectionDirectory | CollectionFile | LinkResource;
+
+export const treePickerSearchMiddleware: Middleware = store => next => action => {
+    let isSearchAction = false;
+    let searchChanged = false;
+
+    treePickerSearchActions.match(action, {
+        SET_TREE_PICKER_PROJECT_SEARCH: ({ pickerId, projectSearchValue }) => {
+            isSearchAction = true;
+            searchChanged = store.getState().treePickerSearch.projectSearchValues[pickerId] !== projectSearchValue;
+        },
+
+        SET_TREE_PICKER_COLLECTION_FILTER: ({ pickerId, collectionFilterValue }) => {
+            isSearchAction = true;
+            searchChanged = store.getState().treePickerSearch.collectionFilterValues[pickerId] !== collectionFilterValue;
+        },
+        default: () => { }
+    });
+
+    if (isSearchAction && !searchChanged) {
+        return;
+    }
+
+    // pass it on to the reducer
+    const r = next(action);
+
+    treePickerSearchActions.match(action, {
+        SET_TREE_PICKER_PROJECT_SEARCH: ({ pickerId }) =>
+            store.dispatch<any>((dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+                const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
+                if (picker) {
+                    const loadParams = getState().treePickerSearch.loadProjectParams[pickerId];
+                    dispatch<any>(loadProject({
+                        ...loadParams,
+                        id: SEARCH_PROJECT_ID,
+                        pickerId: pickerId,
+                        searchProjects: true
+                    }));
+                }
+            }),
+
+        SET_TREE_PICKER_COLLECTION_FILTER: ({ pickerId }) =>
+            store.dispatch<any>((dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+                const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
+                if (picker) {
+                    const loadParams = getState().treePickerSearch.loadProjectParams[pickerId];
+                    getNodeDescendantsIds('')(picker)
+                        .map(id => {
+                            const node = getNode(id)(picker);
+                            if (node && node.status !== TreeNodeStatus.INITIAL) {
+                                if (node.id.substring(6, 11) === 'tpzed' || node.id.substring(6, 11) === 'j7d0g') {
+                                    dispatch<any>(loadProject({
+                                        ...loadParams,
+                                        id: node.id,
+                                        pickerId: pickerId,
+                                    }));
+                                }
+                                if (node.id === SHARED_PROJECT_ID) {
+                                    dispatch<any>(loadProject({
+                                        ...loadParams,
+                                        id: node.id,
+                                        pickerId: pickerId,
+                                        loadShared: true
+                                    }));
+                                }
+                                if (node.id === SEARCH_PROJECT_ID) {
+                                    dispatch<any>(loadProject({
+                                        ...loadParams,
+                                        id: node.id,
+                                        pickerId: pickerId,
+                                        searchProjects: true
+                                    }));
+                                }
+                                if (node.id === FAVORITES_PROJECT_ID) {
+                                    dispatch<any>(loadFavoritesProject({
+                                        ...loadParams,
+                                        pickerId: pickerId,
+                                    }));
+                                }
+                                if (node.id === PUBLIC_FAVORITES_PROJECT_ID) {
+                                    dispatch<any>(loadPublicFavoritesProject({
+                                        ...loadParams,
+                                        pickerId: pickerId,
+                                    }));
+                                }
+                            }
+                            return id;
+                        });
+                }
+            }),
+        default: () => { }
+    });
+
+    return r;
+}
index 349240eaf490ed0a309f437dff44064906bbc865..df0ee0ad167376af2eec2a81293ade246714ba96 100644 (file)
@@ -2,13 +2,15 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode, selectNodes, deselectNodes } from 'models/tree';
+import {
+    createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus,
+    expandNode, deactivateNode, selectNodes, deselectNodes,
+    activateNode, getNode, toggleNodeCollapse, toggleNodeSelection, appendSubtree
+} from 'models/tree';
 import { TreePicker } from "./tree-picker";
-import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
+import { treePickerActions, treePickerSearchActions, TreePickerAction, TreePickerSearchAction, LoadProjectParams } from "./tree-picker-actions";
 import { compose } from "redux";
-import { activateNode, getNode, toggleNodeCollapse, toggleNodeSelection } from 'models/tree';
 import { pipe } from 'lodash/fp';
-import { appendSubtree } from 'models/tree';
 
 export const treePickerReducer = (state: TreePicker = {}, action: TreePickerAction) =>
     treePickerActions.match(action, {
@@ -18,12 +20,15 @@ export const treePickerReducer = (state: TreePicker = {}, action: TreePickerActi
         LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes, pickerId }) =>
             updateOrCreatePicker(state, pickerId, compose(receiveNodes(nodes)(id), setNodeStatus(id)(TreeNodeStatus.LOADED))),
 
-        APPEND_TREE_PICKER_NODE_SUBTREE: ({ id, subtree, pickerId}) =>
+        APPEND_TREE_PICKER_NODE_SUBTREE: ({ id, subtree, pickerId }) =>
             updateOrCreatePicker(state, pickerId, compose(appendSubtree(id, subtree), setNodeStatus(id)(TreeNodeStatus.LOADED))),
 
         TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id, pickerId }) =>
             updateOrCreatePicker(state, pickerId, toggleNodeCollapse(id)),
 
+        EXPAND_TREE_PICKER_NODE: ({ id, pickerId }) =>
+            updateOrCreatePicker(state, pickerId, expandNode(id)),
+
         ACTIVATE_TREE_PICKER_NODE: ({ id, pickerId, relatedTreePickers = [] }) =>
             pipe(
                 () => relatedTreePickers.reduce(
@@ -67,6 +72,33 @@ const receiveNodes = <V>(nodes: Array<TreeNode<V>>) => (parent: string) => (stat
         newState = setNode({ ...parentNode, children: [] })(state);
     }
     return nodes.reduce((tree, node) => {
+        const preexistingNode = getNode(node.id)(state);
+        if (preexistingNode) {
+            node = { ...preexistingNode, value: node.value };
+        }
         return setNode({ ...node, parent })(tree);
     }, newState);
 };
+
+interface TreePickerSearch {
+    projectSearchValues: { [pickerId: string]: string };
+    collectionFilterValues: { [pickerId: string]: string };
+    loadProjectParams: { [pickerId: string]: LoadProjectParams };
+}
+
+export const treePickerSearchReducer = (state: TreePickerSearch = { projectSearchValues: {}, collectionFilterValues: {}, loadProjectParams: {} }, action: TreePickerSearchAction) =>
+    treePickerSearchActions.match(action, {
+        SET_TREE_PICKER_PROJECT_SEARCH: ({ pickerId, projectSearchValue }) => ({
+            ...state, projectSearchValues: { ...state.projectSearchValues, [pickerId]: projectSearchValue }
+        }),
+
+        SET_TREE_PICKER_COLLECTION_FILTER: ({ pickerId, collectionFilterValue }) => ({
+            ...state, collectionFilterValues: { ...state.collectionFilterValues, [pickerId]: collectionFilterValue }
+        }),
+
+        SET_TREE_PICKER_LOAD_PARAMS: ({ pickerId, params }) => ({
+            ...state, loadProjectParams: { ...state.loadProjectParams, [pickerId]: params }
+        }),
+
+        default: () => state
+    });
index 0a3484310ee74a5d6e5182f3e47fb27290520ef3..0ad3fb8e55e496e7b3e9274ee1fd47935842ae73 100644 (file)
@@ -3,40 +3,46 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from 'redux';
-import { RootState } from "store/store";
-import { getUserUuid } from "common/getuser";
+import { RootState } from 'store/store';
+import { getUserUuid } from 'common/getuser';
 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-import { favoritePanelActions, loadFavoritePanel } from 'store/favorite-panel/favorite-panel-action';
 import {
-    getProjectPanelCurrentUuid,
-    openProjectPanel,
-    projectPanelActions,
-    setIsProjectPanelTrashed
+  favoritePanelActions,
+  loadFavoritePanel,
+} from 'store/favorite-panel/favorite-panel-action';
+import {
+  getProjectPanelCurrentUuid,
+  openProjectPanel,
+  projectPanelActions,
+  setIsProjectPanelTrashed,
 } from 'store/project-panel/project-panel-action';
 import {
-    activateSidePanelTreeItem,
-    initSidePanelTree,
-    loadSidePanelTreeProjects,
-    SidePanelTreeCategory
+  activateSidePanelTreeItem,
+  initSidePanelTree,
+  loadSidePanelTreeProjects,
+  SidePanelTreeCategory,
 } from 'store/side-panel-tree/side-panel-tree-actions';
 import { updateResources } from 'store/resources/resources-actions';
 import { projectPanelColumns } from 'views/project-panel/project-panel';
 import { favoritePanelColumns } from 'views/favorite-panel/favorite-panel';
 import { matchRootRoute } from 'routes/routes';
 import {
-    setBreadcrumbs,
-    setGroupDetailsBreadcrumbs,
-    setGroupsBreadcrumbs,
-    setProcessBreadcrumbs,
-    setSharedWithMeBreadcrumbs,
-    setSidePanelBreadcrumbs,
-    setTrashBreadcrumbs,
-    setUsersBreadcrumbs,
-    setMyAccountBreadcrumbs,
-    setUserProfileBreadcrumbs,
+  setBreadcrumbs,
+  setGroupDetailsBreadcrumbs,
+  setGroupsBreadcrumbs,
+  setProcessBreadcrumbs,
+  setSharedWithMeBreadcrumbs,
+  setSidePanelBreadcrumbs,
+  setTrashBreadcrumbs,
+  setUsersBreadcrumbs,
+  setMyAccountBreadcrumbs,
+  setUserProfileBreadcrumbs,
 } from 'store/breadcrumbs/breadcrumbs-actions';
-import { navigateTo, navigateToRootProject } from 'store/navigation/navigation-action';
+import {
+  navigateTo,
+  navigateToRootProject,
+} from 'store/navigation/navigation-action';
 import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
 import { ServiceRepository } from 'services/services';
 import { getResource } from 'store/resources/resources';
@@ -50,17 +56,23 @@ import * as processesActions from 'store/processes/processes-actions';
 import * as processMoveActions from 'store/processes/process-move-actions';
 import * as processUpdateActions from 'store/processes/process-update-actions';
 import * as processCopyActions from 'store/processes/process-copy-actions';
-import { trashPanelColumns } from "views/trash-panel/trash-panel";
-import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
+import { trashPanelColumns } from 'views/trash-panel/trash-panel';
+import {
+  loadTrashPanel,
+  trashPanelActions,
+} from 'store/trash-panel/trash-panel-action';
 import { loadProcessPanel } from 'store/process-panel/process-panel-actions';
 import {
-    loadSharedWithMePanel,
-    sharedWithMePanelActions
+  loadSharedWithMePanel,
+  sharedWithMePanelActions,
 } from 'store/shared-with-me-panel/shared-with-me-panel-actions';
 import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
 import { workflowPanelActions } from 'store/workflow-panel/workflow-panel-actions';
 import { loadSshKeysPanel } from 'store/auth/auth-action-ssh';
-import { loadLinkAccountPanel, linkAccountPanelActions } from 'store/link-account-panel/link-account-panel-actions';
+import {
+  loadLinkAccountPanel,
+  linkAccountPanelActions,
+} from 'store/link-account-panel/link-account-panel-actions';
 import { loadSiteManagerPanel } from 'store/auth/auth-action-session';
 import { workflowPanelColumns } from 'views/workflow-panel/workflow-panel-view';
 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
@@ -70,11 +82,14 @@ import { FilterBuilder } from 'services/api/filter-builder';
 import { GroupContentsResource } from 'services/groups-service/groups-service';
 import { MatchCases, ofType, unionize, UnionOf } from 'common/unionize';
 import { loadRunProcessPanel } from 'store/run-process-panel/run-process-panel-actions';
-import { collectionPanelActions, loadCollectionPanel } from "store/collection-panel/collection-panel-action";
-import { CollectionResource } from "models/collection";
 import {
-    loadSearchResultsPanel,
-    searchResultsPanelActions
+  collectionPanelActions,
+  loadCollectionPanel,
+} from 'store/collection-panel/collection-panel-action';
+import { CollectionResource } from 'models/collection';
+import {
+  loadSearchResultsPanel,
+  searchResultsPanelActions,
 } from 'store/search-results-panel/search-results-panel-actions';
 import { searchResultsPanelColumns } from 'views/search-results-panel/search-results-panel-view';
 import { loadVirtualMachinesPanel } from 'store/virtual-machines/virtual-machines-actions';
@@ -82,23 +97,41 @@ import { loadRepositoriesPanel } from 'store/repositories/repositories-actions';
 import { loadKeepServicesPanel } from 'store/keep-services/keep-services-actions';
 import { loadUsersPanel, userBindedActions } from 'store/users/users-actions';
 import * as userProfilePanelActions from 'store/user-profile/user-profile-actions';
-import { linkPanelActions, loadLinkPanel } from 'store/link-panel/link-panel-actions';
+import {
+  linkPanelActions,
+  loadLinkPanel,
+} from 'store/link-panel/link-panel-actions';
 import { linkPanelColumns } from 'views/link-panel/link-panel-root';
 import { userPanelColumns } from 'views/user-panel/user-panel';
-import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from 'store/api-client-authorizations/api-client-authorizations-actions';
+import {
+  loadApiClientAuthorizationsPanel,
+  apiClientAuthorizationsActions,
+} from 'store/api-client-authorizations/api-client-authorizations-actions';
 import { apiClientAuthorizationPanelColumns } from 'views/api-client-authorization-panel/api-client-authorization-panel-root';
 import * as groupPanelActions from 'store/groups-panel/groups-panel-actions';
 import { groupsPanelColumns } from 'views/groups-panel/groups-panel';
 import * as groupDetailsPanelActions from 'store/group-details-panel/group-details-panel-actions';
-import { groupDetailsMembersPanelColumns, groupDetailsPermissionsPanelColumns } from 'views/group-details-panel/group-details-panel';
-import { DataTableFetchMode } from "components/data-table/data-table";
-import { loadPublicFavoritePanel, publicFavoritePanelActions } from 'store/public-favorites-panel/public-favorites-action';
+import {
+  groupDetailsMembersPanelColumns,
+  groupDetailsPermissionsPanelColumns,
+} from 'views/group-details-panel/group-details-panel';
+import { DataTableFetchMode } from 'components/data-table/data-table';
+import {
+  loadPublicFavoritePanel,
+  publicFavoritePanelActions,
+} from 'store/public-favorites-panel/public-favorites-action';
 import { publicFavoritePanelColumns } from 'views/public-favorites-panel/public-favorites-panel';
-import { loadCollectionsContentAddressPanel, collectionsContentAddressActions } from 'store/collections-content-address-panel/collections-content-address-panel-actions';
+import {
+  loadCollectionsContentAddressPanel,
+  collectionsContentAddressActions,
+} from 'store/collections-content-address-panel/collections-content-address-panel-actions';
 import { collectionContentAddressPanelColumns } from 'views/collection-content-address-panel/collection-content-address-panel';
 import { subprocessPanelActions } from 'store/subprocess-panel/subprocess-panel-actions';
 import { subprocessPanelColumns } from 'views/subprocess-panel/subprocess-panel-root';
-import { loadAllProcessesPanel, allProcessesPanelActions } from '../all-processes-panel/all-processes-panel-action';
+import {
+  loadAllProcessesPanel,
+  allProcessesPanelActions,
+} from '../all-processes-panel/all-processes-panel-action';
 import { allProcessesPanelColumns } from 'views/all-processes-panel/all-processes-panel';
 import { AdminMenuIcon } from 'components/icon/icon';
 import { userProfileGroupsColumns } from 'views/user-profile-panel/user-profile-panel-root';
@@ -106,497 +139,758 @@ import { userProfileGroupsColumns } from 'views/user-profile-panel/user-profile-
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
 export const isWorkbenchLoading = (state: RootState) => {
-    const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
-    return progress ? progress.working : false;
+  const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(
+    state.progressIndicator
+  );
+  return progress ? progress.working : false;
 };
 
-export const handleFirstTimeLoad = (action: any) =>
-    async (dispatch: Dispatch<any>, getState: () => RootState) => {
-        try {
-            await dispatch(action);
-        } finally {
-            if (isWorkbenchLoading(getState())) {
-                dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
-            }
-        }
-    };
-
-export const loadWorkbench = () =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
-        const { auth, router } = getState();
-        const { user } = auth;
-        if (user) {
-            dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
-            dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
-            dispatch(allProcessesPanelActions.SET_COLUMNS({ columns: allProcessesPanelColumns }));
-            dispatch(publicFavoritePanelActions.SET_COLUMNS({ columns: publicFavoritePanelColumns }));
-            dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
-            dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
-            dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
-            dispatch(searchResultsPanelActions.SET_FETCH_MODE({ fetchMode: DataTableFetchMode.INFINITE }));
-            dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
-            dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
-            dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
-            dispatch(groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({ columns: groupDetailsMembersPanelColumns }));
-            dispatch(groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({ columns: groupDetailsPermissionsPanelColumns }));
-            dispatch(userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({ columns: userProfileGroupsColumns }));
-            dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
-            dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
-            dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
-            dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
-
-            if (services.linkAccountService.getAccountToLink()) {
-                dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
-            }
-
-            dispatch<any>(initSidePanelTree());
-            if (router.location) {
-                const match = matchRootRoute(router.location.pathname);
-                if (match) {
-                    dispatch<any>(navigateToRootProject);
-                }
-            }
-        } else {
-            dispatch(userIsNotAuthenticated);
+export const handleFirstTimeLoad =
+  (action: any) =>
+  async (dispatch: Dispatch<any>, getState: () => RootState) => {
+    try {
+      await dispatch(action);
+    } finally {
+      if (isWorkbenchLoading(getState())) {
+        dispatch(
+          progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN)
+        );
+      }
+    }
+  };
+
+export const loadWorkbench =
+  () =>
+  async (
+    dispatch: Dispatch,
+    getState: () => RootState,
+    services: ServiceRepository
+  ) => {
+    dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
+    const { auth, router } = getState();
+    const { user } = auth;
+    if (user) {
+      dispatch(
+        projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns })
+      );
+      dispatch(
+        favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns })
+      );
+      dispatch(
+        allProcessesPanelActions.SET_COLUMNS({
+          columns: allProcessesPanelColumns,
+        })
+      );
+      dispatch(
+        publicFavoritePanelActions.SET_COLUMNS({
+          columns: publicFavoritePanelColumns,
+        })
+      );
+      dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
+      dispatch(
+        sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns })
+      );
+      dispatch(
+        workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns })
+      );
+      dispatch(
+        searchResultsPanelActions.SET_FETCH_MODE({
+          fetchMode: DataTableFetchMode.INFINITE,
+        })
+      );
+      dispatch(
+        searchResultsPanelActions.SET_COLUMNS({
+          columns: searchResultsPanelColumns,
+        })
+      );
+      dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
+      dispatch(
+        groupPanelActions.GroupsPanelActions.SET_COLUMNS({
+          columns: groupsPanelColumns,
+        })
+      );
+      dispatch(
+        groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({
+          columns: groupDetailsMembersPanelColumns,
+        })
+      );
+      dispatch(
+        groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({
+          columns: groupDetailsPermissionsPanelColumns,
+        })
+      );
+      dispatch(
+        userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({
+          columns: userProfileGroupsColumns,
+        })
+      );
+      dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
+      dispatch(
+        apiClientAuthorizationsActions.SET_COLUMNS({
+          columns: apiClientAuthorizationPanelColumns,
+        })
+      );
+      dispatch(
+        collectionsContentAddressActions.SET_COLUMNS({
+          columns: collectionContentAddressPanelColumns,
+        })
+      );
+      dispatch(
+        subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns })
+      );
+
+      if (services.linkAccountService.getAccountToLink()) {
+        dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
+      }
+
+      dispatch<any>(initSidePanelTree());
+      if (router.location) {
+        const match = matchRootRoute(router.location.pathname);
+        if (match) {
+          dispatch<any>(navigateToRootProject);
         }
-    };
+      }
+    } else {
+      dispatch(userIsNotAuthenticated);
+    }
+  };
 
 export const loadFavorites = () =>
-    handleFirstTimeLoad(
-        (dispatch: Dispatch) => {
-            dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
-            dispatch<any>(loadFavoritePanel());
-            dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
-        });
+  handleFirstTimeLoad((dispatch: Dispatch) => {
+    dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
+    dispatch<any>(loadFavoritePanel());
+    dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
+  });
 
 export const loadCollectionContentAddress = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadCollectionsContentAddressPanel());
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadCollectionsContentAddressPanel());
+  }
+);
 
 export const loadTrash = () =>
-    handleFirstTimeLoad(
-        (dispatch: Dispatch) => {
-            dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
-            dispatch<any>(loadTrashPanel());
-            dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
-        });
+  handleFirstTimeLoad((dispatch: Dispatch) => {
+    dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
+    dispatch<any>(loadTrashPanel());
+    dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
+  });
 
 export const loadAllProcesses = () =>
-    handleFirstTimeLoad(
-        (dispatch: Dispatch) => {
-            dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.ALL_PROCESSES));
-            dispatch<any>(loadAllProcessesPanel());
-            dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.ALL_PROCESSES));
-        }
+  handleFirstTimeLoad((dispatch: Dispatch) => {
+    dispatch<any>(
+      activateSidePanelTreeItem(SidePanelTreeCategory.ALL_PROCESSES)
     );
+    dispatch<any>(loadAllProcessesPanel());
+    dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.ALL_PROCESSES));
+  });
 
 export const loadProject = (uuid: string) =>
-    handleFirstTimeLoad(
-        async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-            const userUuid = getUserUuid(getState());
-            dispatch(setIsProjectPanelTrashed(false));
-            if (!userUuid) {
-                return;
-            }
-            if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
-                // Load another users home projects
-                dispatch(finishLoadingProject(uuid));
-            } else if (userUuid !== uuid) {
-                await dispatch(finishLoadingProject(uuid));
-                const match = await loadGroupContentsResource({ uuid, userUuid, services });
-                match({
-                    OWNED: async () => {
-                        await dispatch(activateSidePanelTreeItem(uuid));
-                        dispatch<any>(setSidePanelBreadcrumbs(uuid));
-                    },
-                    SHARED: async () => {
-                        await dispatch(activateSidePanelTreeItem(uuid));
-                        dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
-                    },
-                    TRASHED: async () => {
-                        await dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
-                        dispatch<any>(setTrashBreadcrumbs(uuid));
-                        dispatch(setIsProjectPanelTrashed(true));
-                    }
-                });
-            } else {
-                await dispatch(finishLoadingProject(userUuid));
-                await dispatch(activateSidePanelTreeItem(userUuid));
-                dispatch<any>(setSidePanelBreadcrumbs(userUuid));
-            }
+  handleFirstTimeLoad(
+    async (
+      dispatch: Dispatch<any>,
+      getState: () => RootState,
+      services: ServiceRepository
+    ) => {
+      const userUuid = getUserUuid(getState());
+      dispatch(setIsProjectPanelTrashed(false));
+      if (!userUuid) {
+        return;
+      }
+      if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
+        // Load another users home projects
+        dispatch(finishLoadingProject(uuid));
+      } else if (userUuid !== uuid) {
+        await dispatch(finishLoadingProject(uuid));
+        const match = await loadGroupContentsResource({
+          uuid,
+          userUuid,
+          services,
+        });
+        match({
+          OWNED: async () => {
+            await dispatch(activateSidePanelTreeItem(uuid));
+            dispatch<any>(setSidePanelBreadcrumbs(uuid));
+          },
+          SHARED: async () => {
+            await dispatch(activateSidePanelTreeItem(uuid));
+            dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
+          },
+          TRASHED: async () => {
+            await dispatch(
+              activateSidePanelTreeItem(SidePanelTreeCategory.TRASH)
+            );
+            dispatch<any>(setTrashBreadcrumbs(uuid));
+            dispatch(setIsProjectPanelTrashed(true));
+          },
         });
+      } else {
+        await dispatch(finishLoadingProject(userUuid));
+        await dispatch(activateSidePanelTreeItem(userUuid));
+        dispatch<any>(setSidePanelBreadcrumbs(userUuid));
+      }
+    }
+  );
 
-export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) =>
-    async (dispatch: Dispatch) => {
-        const newProject = await dispatch<any>(projectCreateActions.createProject(data));
-        if (newProject) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Project has been successfully created.",
-                hideDuration: 2000,
-                kind: SnackbarKind.SUCCESS
-            }));
-            await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
-            dispatch<any>(navigateTo(newProject.uuid));
-        }
-    };
-
-export const moveProject = (data: MoveToFormDialogData) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        try {
-            const oldProject = getResource(data.uuid)(getState().resources);
-            const oldOwnerUuid = oldProject ? oldProject.ownerUuid : '';
-            const movedProject = await dispatch<any>(projectMoveActions.moveProject(data));
-            if (movedProject) {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Project has been moved', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-                if (oldProject) {
-                    await dispatch<any>(loadSidePanelTreeProjects(oldProject.ownerUuid));
-                }
-                dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
-            }
-        } catch (e) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        }
-    };
-
-export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
-    async (dispatch: Dispatch) => {
-        const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
-        if (updatedProject) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Project has been successfully updated.",
-                hideDuration: 2000,
-                kind: SnackbarKind.SUCCESS
-            }));
-            await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
-            dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
-        }
-    };
-
-export const updateGroup = (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
-    async (dispatch: Dispatch) => {
-        const updatedGroup = await dispatch<any>(groupPanelActions.updateGroup(data));
-        if (updatedGroup) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Group has been successfully updated.",
-                hideDuration: 2000,
-                kind: SnackbarKind.SUCCESS
-            }));
-            await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
-            dispatch<any>(reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid]));
+export const createProject =
+  (data: projectCreateActions.ProjectCreateFormDialogData) =>
+  async (dispatch: Dispatch) => {
+    const newProject = await dispatch<any>(
+      projectCreateActions.createProject(data)
+    );
+    if (newProject) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: 'Project has been successfully created.',
+          hideDuration: 2000,
+          kind: SnackbarKind.SUCCESS,
+        })
+      );
+      await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
+      dispatch<any>(navigateTo(newProject.uuid));
+    }
+  };
+
+export const moveProject =
+  (data: MoveToFormDialogData) =>
+  async (
+    dispatch: Dispatch,
+    getState: () => RootState,
+    services: ServiceRepository
+  ) => {
+    try {
+      const oldProject = getResource(data.uuid)(getState().resources);
+      const oldOwnerUuid = oldProject ? oldProject.ownerUuid : '';
+      const movedProject = await dispatch<any>(
+        projectMoveActions.moveProject(data)
+      );
+      if (movedProject) {
+        dispatch(
+          snackbarActions.OPEN_SNACKBAR({
+            message: 'Project has been moved',
+            hideDuration: 2000,
+            kind: SnackbarKind.SUCCESS,
+          })
+        );
+        if (oldProject) {
+          await dispatch<any>(loadSidePanelTreeProjects(oldProject.ownerUuid));
         }
-    };
+        dispatch<any>(
+          reloadProjectMatchingUuid([
+            oldOwnerUuid,
+            movedProject.ownerUuid,
+            movedProject.uuid,
+          ])
+        );
+      }
+    } catch (e) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: e.message,
+          hideDuration: 2000,
+          kind: SnackbarKind.ERROR,
+        })
+      );
+    }
+  };
+
+export const updateProject =
+  (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
+  async (dispatch: Dispatch) => {
+    const updatedProject = await dispatch<any>(
+      projectUpdateActions.updateProject(data)
+    );
+    if (updatedProject) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: 'Project has been successfully updated.',
+          hideDuration: 2000,
+          kind: SnackbarKind.SUCCESS,
+        })
+      );
+      await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
+      dispatch<any>(
+        reloadProjectMatchingUuid([
+          updatedProject.ownerUuid,
+          updatedProject.uuid,
+        ])
+      );
+    }
+  };
+
+export const updateGroup =
+  (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
+  async (dispatch: Dispatch) => {
+    const updatedGroup = await dispatch<any>(
+      groupPanelActions.updateGroup(data)
+    );
+    if (updatedGroup) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: 'Group has been successfully updated.',
+          hideDuration: 2000,
+          kind: SnackbarKind.SUCCESS,
+        })
+      );
+      await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
+      dispatch<any>(
+        reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid])
+      );
+    }
+  };
 
 export const loadCollection = (uuid: string) =>
-    handleFirstTimeLoad(
-        async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-            const userUuid = getUserUuid(getState());
-            if (userUuid) {
-                const match = await loadGroupContentsResource({ uuid, userUuid, services });
-                match({
-                    OWNED: collection => {
-                        dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
-                        dispatch(updateResources([collection]));
-                        dispatch(activateSidePanelTreeItem(collection.ownerUuid));
-                        dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
-                        dispatch(loadCollectionPanel(collection.uuid));
-                    },
-                    SHARED: collection => {
-                        dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
-                        dispatch(updateResources([collection]));
-                        dispatch<any>(setSharedWithMeBreadcrumbs(collection.ownerUuid));
-                        dispatch(activateSidePanelTreeItem(collection.ownerUuid));
-                        dispatch(loadCollectionPanel(collection.uuid));
-                    },
-                    TRASHED: collection => {
-                        dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
-                        dispatch(updateResources([collection]));
-                        dispatch(setTrashBreadcrumbs(''));
-                        dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
-                        dispatch(loadCollectionPanel(collection.uuid));
-                    },
-                });
-            }
+  handleFirstTimeLoad(
+    async (
+      dispatch: Dispatch<any>,
+      getState: () => RootState,
+      services: ServiceRepository
+    ) => {
+      const userUuid = getUserUuid(getState());
+      if (userUuid) {
+        const match = await loadGroupContentsResource({
+          uuid,
+          userUuid,
+          services,
         });
+        match({
+          OWNED: (collection) => {
+            dispatch(
+              collectionPanelActions.SET_COLLECTION(
+                collection as CollectionResource
+              )
+            );
+            dispatch(updateResources([collection]));
+            dispatch(activateSidePanelTreeItem(collection.ownerUuid));
+            dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
+            dispatch(loadCollectionPanel(collection.uuid));
+          },
+          SHARED: (collection) => {
+            dispatch(
+              collectionPanelActions.SET_COLLECTION(
+                collection as CollectionResource
+              )
+            );
+            dispatch(updateResources([collection]));
+            dispatch<any>(setSharedWithMeBreadcrumbs(collection.ownerUuid));
+            dispatch(activateSidePanelTreeItem(collection.ownerUuid));
+            dispatch(loadCollectionPanel(collection.uuid));
+          },
+          TRASHED: (collection) => {
+            dispatch(
+              collectionPanelActions.SET_COLLECTION(
+                collection as CollectionResource
+              )
+            );
+            dispatch(updateResources([collection]));
+            dispatch(setTrashBreadcrumbs(''));
+            dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
+            dispatch(loadCollectionPanel(collection.uuid));
+          },
+        });
+      }
+    }
+  );
 
-export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) =>
-    async (dispatch: Dispatch) => {
-        const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
-        if (collection) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Collection has been successfully created.",
-                hideDuration: 2000,
-                kind: SnackbarKind.SUCCESS
-            }));
-            dispatch<any>(updateResources([collection]));
-            dispatch<any>(navigateTo(collection.uuid));
-        }
-    };
-
-export const copyCollection = (data: CopyFormDialogData) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        try {
-            const copyToProject = getResource(data.ownerUuid)(getState().resources);
-            const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
-            if (copyToProject && collection) {
-                dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
-                dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: 'Collection has been copied.',
-                    hideDuration: 3000,
-                    kind: SnackbarKind.SUCCESS,
-                    link: collection.ownerUuid
-                }));
-            }
-        } catch (e) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        }
-    };
-
-export const moveCollection = (data: MoveToFormDialogData) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        try {
-            const collection = await dispatch<any>(collectionMoveActions.moveCollection(data));
-            dispatch<any>(updateResources([collection]));
-            dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-        } catch (e) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        }
-    };
+export const createCollection =
+  (data: collectionCreateActions.CollectionCreateFormDialogData) =>
+  async (dispatch: Dispatch) => {
+    const collection = await dispatch<any>(
+      collectionCreateActions.createCollection(data)
+    );
+    if (collection) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: 'Collection has been successfully created.',
+          hideDuration: 2000,
+          kind: SnackbarKind.SUCCESS,
+        })
+      );
+      dispatch<any>(updateResources([collection]));
+      dispatch<any>(navigateTo(collection.uuid));
+    }
+  };
+
+export const copyCollection =
+  (data: CopyFormDialogData) =>
+  async (
+    dispatch: Dispatch,
+    getState: () => RootState,
+    services: ServiceRepository
+  ) => {
+    try {
+      const copyToProject = getResource(data.ownerUuid)(getState().resources);
+      const collection = await dispatch<any>(
+        collectionCopyActions.copyCollection(data)
+      );
+      if (copyToProject && collection) {
+        dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
+        dispatch(
+          snackbarActions.OPEN_SNACKBAR({
+            message: 'Collection has been copied.',
+            hideDuration: 3000,
+            kind: SnackbarKind.SUCCESS,
+            link: collection.ownerUuid,
+          })
+        );
+      }
+    } catch (e) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: e.message,
+          hideDuration: 2000,
+          kind: SnackbarKind.ERROR,
+        })
+      );
+    }
+  };
+
+export const moveCollection =
+  (data: MoveToFormDialogData) =>
+  async (
+    dispatch: Dispatch,
+    getState: () => RootState,
+    services: ServiceRepository
+  ) => {
+    try {
+      const collection = await dispatch<any>(
+        collectionMoveActions.moveCollection(data)
+      );
+      dispatch<any>(updateResources([collection]));
+      dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: 'Collection has been moved.',
+          hideDuration: 2000,
+          kind: SnackbarKind.SUCCESS,
+        })
+      );
+    } catch (e) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: e.message,
+          hideDuration: 2000,
+          kind: SnackbarKind.ERROR,
+        })
+      );
+    }
+  };
 
 export const loadProcess = (uuid: string) =>
-    handleFirstTimeLoad(
-        async (dispatch: Dispatch, getState: () => RootState) => {
-            dispatch<any>(loadProcessPanel(uuid));
-            const process = await dispatch<any>(processesActions.loadProcess(uuid));
-            await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
-            dispatch<any>(setProcessBreadcrumbs(uuid));
-            dispatch<any>(loadDetailsPanel(uuid));
-        });
+  handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState) => {
+    dispatch<any>(loadProcessPanel(uuid));
+    const process = await dispatch<any>(processesActions.loadProcess(uuid));
+    await dispatch<any>(
+      activateSidePanelTreeItem(process.containerRequest.ownerUuid)
+    );
+    dispatch<any>(setProcessBreadcrumbs(uuid));
+    dispatch<any>(loadDetailsPanel(uuid));
+  });
+
+export const updateProcess =
+  (data: processUpdateActions.ProcessUpdateFormDialogData) =>
+  async (dispatch: Dispatch) => {
+    try {
+      const process = await dispatch<any>(
+        processUpdateActions.updateProcess(data)
+      );
+      if (process) {
+        dispatch(
+          snackbarActions.OPEN_SNACKBAR({
+            message: 'Process has been successfully updated.',
+            hideDuration: 2000,
+            kind: SnackbarKind.SUCCESS,
+          })
+        );
+        dispatch<any>(updateResources([process]));
+        dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+      }
+    } catch (e) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: e.message,
+          hideDuration: 2000,
+          kind: SnackbarKind.ERROR,
+        })
+      );
+    }
+  };
 
-export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) =>
-    async (dispatch: Dispatch) => {
-        try {
-            const process = await dispatch<any>(processUpdateActions.updateProcess(data));
-            if (process) {
-                dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: "Process has been successfully updated.",
-                    hideDuration: 2000,
-                    kind: SnackbarKind.SUCCESS
-                }));
-                dispatch<any>(updateResources([process]));
-                dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
-            }
-        } catch (e) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        }
-    };
-
-export const moveProcess = (data: MoveToFormDialogData) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        try {
-            const process = await dispatch<any>(processMoveActions.moveProcess(data));
-            dispatch<any>(updateResources([process]));
-            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been moved.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-        } catch (e) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        }
-    };
-
-export const copyProcess = (data: CopyFormDialogData) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        try {
-            const process = await dispatch<any>(processCopyActions.copyProcess(data));
-            dispatch<any>(updateResources([process]));
-            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-        } catch (e) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        }
-    };
+export const moveProcess =
+  (data: MoveToFormDialogData) =>
+  async (
+    dispatch: Dispatch,
+    getState: () => RootState,
+    services: ServiceRepository
+  ) => {
+    try {
+      const process = await dispatch<any>(processMoveActions.moveProcess(data));
+      dispatch<any>(updateResources([process]));
+      dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: 'Process has been moved.',
+          hideDuration: 2000,
+          kind: SnackbarKind.SUCCESS,
+        })
+      );
+    } catch (e) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: e.message,
+          hideDuration: 2000,
+          kind: SnackbarKind.ERROR,
+        })
+      );
+    }
+  };
+
+export const copyProcess =
+  (data: CopyFormDialogData) =>
+  async (
+    dispatch: Dispatch,
+    getState: () => RootState,
+    services: ServiceRepository
+  ) => {
+    try {
+      const process = await dispatch<any>(processCopyActions.copyProcess(data));
+      dispatch<any>(updateResources([process]));
+      dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: 'Process has been copied.',
+          hideDuration: 2000,
+          kind: SnackbarKind.SUCCESS,
+        })
+      );
+    } catch (e) {
+      dispatch(
+        snackbarActions.OPEN_SNACKBAR({
+          message: e.message,
+          hideDuration: 2000,
+          kind: SnackbarKind.ERROR,
+        })
+      );
+    }
+  };
 
 export const resourceIsNotLoaded = (uuid: string) =>
-    snackbarActions.OPEN_SNACKBAR({
-        message: `Resource identified by ${uuid} is not loaded.`,
-        kind: SnackbarKind.ERROR
-    });
+  snackbarActions.OPEN_SNACKBAR({
+    message: `Resource identified by ${uuid} is not loaded.`,
+    kind: SnackbarKind.ERROR,
+  });
 
 export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
-    message: 'User is not authenticated',
-    kind: SnackbarKind.ERROR
+  message: 'User is not authenticated',
+  kind: SnackbarKind.ERROR,
 });
 
 export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
-    message: 'Could not load user',
-    kind: SnackbarKind.ERROR
+  message: 'Could not load user',
+  kind: SnackbarKind.ERROR,
 });
 
-export const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
-        if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
-            dispatch<any>(loadProject(currentProjectPanelUuid));
-        }
-    };
+export const reloadProjectMatchingUuid =
+  (matchingUuids: string[]) =>
+  async (
+    dispatch: Dispatch,
+    getState: () => RootState,
+    services: ServiceRepository
+  ) => {
+    const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
+    if (
+      currentProjectPanelUuid &&
+      matchingUuids.some((uuid) => uuid === currentProjectPanelUuid)
+    ) {
+      dispatch<any>(loadProject(currentProjectPanelUuid));
+    }
+  };
 
-export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
+export const loadSharedWithMe = handleFirstTimeLoad(
+  async (dispatch: Dispatch) => {
     dispatch<any>(loadSharedWithMePanel());
-    await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
-    await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
-});
+    await dispatch<any>(
+      activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME)
+    );
+    await dispatch<any>(
+      setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME)
+    );
+  }
+);
 
 export const loadRunProcess = handleFirstTimeLoad(
-    async (dispatch: Dispatch) => {
-        await dispatch<any>(loadRunProcessPanel());
-    }
+  async (dispatch: Dispatch) => {
+    await dispatch<any>(loadRunProcessPanel());
+  }
 );
 
 export const loadPublicFavorites = () =>
-    handleFirstTimeLoad(
-        (dispatch: Dispatch) => {
-            dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES));
-            dispatch<any>(loadPublicFavoritePanel());
-            dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES));
-        });
+  handleFirstTimeLoad((dispatch: Dispatch) => {
+    dispatch<any>(
+      activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES)
+    );
+    dispatch<any>(loadPublicFavoritePanel());
+    dispatch<any>(
+      setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES)
+    );
+  });
 
 export const loadSearchResults = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadSearchResultsPanel());
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadSearchResultsPanel());
+  }
+);
 
 export const loadLinks = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadLinkPanel());
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadLinkPanel());
+  }
+);
 
 export const loadVirtualMachines = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadVirtualMachinesPanel());
-        dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadVirtualMachinesPanel());
+    dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
+  }
+);
 
 export const loadVirtualMachinesAdmin = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadVirtualMachinesPanel());
-        dispatch(setBreadcrumbs([{ label: 'Virtual Machines Admin', icon: AdminMenuIcon }]));
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadVirtualMachinesPanel());
+    dispatch(
+      setBreadcrumbs([{ label: 'Virtual Machines Admin', icon: AdminMenuIcon }])
+    );
+  }
+);
 
 export const loadRepositories = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadRepositoriesPanel());
-        dispatch(setBreadcrumbs([{ label: 'Repositories' }]));
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadRepositoriesPanel());
+    dispatch(setBreadcrumbs([{ label: 'Repositories' }]));
+  }
+);
 
 export const loadSshKeys = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadSshKeysPanel());
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadSshKeysPanel());
+  }
+);
 
 export const loadSiteManager = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadSiteManagerPanel());
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadSiteManagerPanel());
+  }
+);
 
 export const loadUserProfile = (userUuid?: string) =>
-    handleFirstTimeLoad(
-        (dispatch: Dispatch<any>) => {
-            if (userUuid) {
-                dispatch(setUserProfileBreadcrumbs(userUuid));
-                dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
-            } else {
-                dispatch(setMyAccountBreadcrumbs());
-                dispatch(userProfilePanelActions.loadUserProfilePanel());
-            }
-        }
-    );
+  handleFirstTimeLoad((dispatch: Dispatch<any>) => {
+    if (userUuid) {
+      dispatch(setUserProfileBreadcrumbs(userUuid));
+      dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
+    } else {
+      dispatch(setMyAccountBreadcrumbs());
+      dispatch(userProfilePanelActions.loadUserProfilePanel());
+    }
+  });
 
 export const loadLinkAccount = handleFirstTimeLoad(
-    (dispatch: Dispatch<any>) => {
-        dispatch(loadLinkAccountPanel());
-    });
+  (dispatch: Dispatch<any>) => {
+    dispatch(loadLinkAccountPanel());
+  }
+);
 
 export const loadKeepServices = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadKeepServicesPanel());
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadKeepServicesPanel());
+  }
+);
 
 export const loadUsers = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadUsersPanel());
-        dispatch(setUsersBreadcrumbs());
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadUsersPanel());
+    dispatch(setUsersBreadcrumbs());
+  }
+);
 
 export const loadApiClientAuthorizations = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadApiClientAuthorizationsPanel());
-    });
+  async (dispatch: Dispatch<any>) => {
+    await dispatch(loadApiClientAuthorizationsPanel());
+  }
+);
 
 export const loadGroupsPanel = handleFirstTimeLoad(
-    (dispatch: Dispatch<any>) => {
-        dispatch(setGroupsBreadcrumbs());
-        dispatch(groupPanelActions.loadGroupsPanel());
-    });
-
+  (dispatch: Dispatch<any>) => {
+    dispatch(setGroupsBreadcrumbs());
+    dispatch(groupPanelActions.loadGroupsPanel());
+  }
+);
 
 export const loadGroupDetailsPanel = (groupUuid: string) =>
-    handleFirstTimeLoad(
-        (dispatch: Dispatch<any>) => {
-            dispatch(setGroupDetailsBreadcrumbs(groupUuid));
-            dispatch(groupDetailsPanelActions.loadGroupDetailsPanel(groupUuid));
-        });
-
-const finishLoadingProject = (project: GroupContentsResource | string) =>
-    async (dispatch: Dispatch<any>) => {
-        const uuid = typeof project === 'string' ? project : project.uuid;
-        dispatch(openProjectPanel(uuid));
-        dispatch(loadDetailsPanel(uuid));
-        if (typeof project !== 'string') {
-            dispatch(updateResources([project]));
-        }
-    };
+  handleFirstTimeLoad((dispatch: Dispatch<any>) => {
+    dispatch(setGroupDetailsBreadcrumbs(groupUuid));
+    dispatch(groupDetailsPanelActions.loadGroupDetailsPanel(groupUuid));
+  });
+
+const finishLoadingProject =
+  (project: GroupContentsResource | string) =>
+  async (dispatch: Dispatch<any>) => {
+    const uuid = typeof project === 'string' ? project : project.uuid;
+    dispatch(openProjectPanel(uuid));
+    dispatch(loadDetailsPanel(uuid));
+    if (typeof project !== 'string') {
+      dispatch(updateResources([project]));
+    }
+  };
 
 const loadGroupContentsResource = async (params: {
-    uuid: string,
-    userUuid: string,
-    services: ServiceRepository
+  uuid: string;
+  userUuid: string;
+  services: ServiceRepository;
 }) => {
-    const filters = new FilterBuilder()
-        .addEqual('uuid', params.uuid)
-        .getFilters();
-    const { items } = await params.services.groupsService.contents(params.userUuid, {
-        filters,
-        recursive: true,
-        includeTrash: true,
-    });
-    const resource = items.shift();
-    let handler: GroupContentsHandler;
-    if (resource) {
-        handler = (resource.kind === ResourceKind.COLLECTION || resource.kind === ResourceKind.PROJECT) && resource.isTrashed
-            ? groupContentsHandlers.TRASHED(resource)
-            : groupContentsHandlers.OWNED(resource);
+  const filters = new FilterBuilder()
+    .addEqual('uuid', params.uuid)
+    .getFilters();
+  const { items } = await params.services.groupsService.contents(
+    params.userUuid,
+    {
+      filters,
+      recursive: true,
+      includeTrash: true,
+    }
+  );
+  const resource = items.shift();
+  let handler: GroupContentsHandler;
+  if (resource) {
+    handler =
+      (resource.kind === ResourceKind.COLLECTION ||
+        resource.kind === ResourceKind.PROJECT) &&
+      resource.isTrashed
+        ? groupContentsHandlers.TRASHED(resource)
+        : groupContentsHandlers.OWNED(resource);
+  } else {
+    const kind = extractUuidKind(params.uuid);
+    let resource: GroupContentsResource;
+    if (kind === ResourceKind.COLLECTION) {
+      resource = await params.services.collectionService.get(params.uuid);
+    } else if (kind === ResourceKind.PROJECT) {
+      resource = await params.services.projectService.get(params.uuid);
     } else {
-        const kind = extractUuidKind(params.uuid);
-        let resource: GroupContentsResource;
-        if (kind === ResourceKind.COLLECTION) {
-            resource = await params.services.collectionService.get(params.uuid);
-        } else if (kind === ResourceKind.PROJECT) {
-            resource = await params.services.projectService.get(params.uuid);
-        } else {
-            resource = await params.services.containerRequestService.get(params.uuid);
-        }
-        handler = groupContentsHandlers.SHARED(resource);
+      resource = await params.services.containerRequestService.get(params.uuid);
     }
-    return (cases: MatchCases<typeof groupContentsHandlersRecord, GroupContentsHandler, void>) =>
-        groupContentsHandlers.match(handler, cases);
-
+    handler = groupContentsHandlers.SHARED(resource);
+  }
+  return (
+    cases: MatchCases<
+      typeof groupContentsHandlersRecord,
+      GroupContentsHandler,
+      void
+    >
+  ) => groupContentsHandlers.match(handler, cases);
 };
 
 const groupContentsHandlersRecord = {
-    TRASHED: ofType<GroupContentsResource>(),
-    SHARED: ofType<GroupContentsResource>(),
-    OWNED: ofType<GroupContentsResource>(),
+  TRASHED: ofType<GroupContentsResource>(),
+  SHARED: ofType<GroupContentsResource>(),
+  OWNED: ofType<GroupContentsResource>(),
 };
 
 const groupContentsHandlers = unionize(groupContentsHandlersRecord);
index 6e72ef689829ef3aad0b6cc1c265d0467491ee21..87a4c1f57e2523fee923b41619b288c929fc99f1 100644 (file)
@@ -8,8 +8,8 @@ import { isRsaKey } from './is-rsa-key';
 import { isRemoteHost } from "./is-remote-host";
 import { validFilePath, validName, validNameAllowSlash } from "./valid-name";
 
-export const TAG_KEY_VALIDATION = [require, maxLength(255)];
-export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
+export const TAG_KEY_VALIDATION = [maxLength(255)];
+export const TAG_VALUE_VALIDATION = [maxLength(255)];
 
 export const PROJECT_NAME_VALIDATION = [require, validName, maxLength(255)];
 export const PROJECT_NAME_VALIDATION_ALLOW_SLASH = [require, validNameAllowSlash, maxLength(255)];
index c4134aed4d948255fc0c07e8fb1ab9747e971836..0334097d2eee07ce079b2f7c9863e1bb59a2756d 100644 (file)
@@ -8,25 +8,24 @@ import { RootState } from 'store/store';
 import { Dispatch } from 'redux';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { getProperty } from '../../store/properties/properties';
-import { ResourceBreadcrumb, BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
+import { BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
 import { openSidePanelContextMenu } from 'store/context-menu/context-menu-actions';
-import { ProjectResource } from "models/project";
 
 type BreadcrumbsDataProps = Pick<BreadcrumbsProps, 'items' | 'resources'>;
 type BreadcrumbsActionProps = Pick<BreadcrumbsProps, 'onClick' | 'onContextMenu'>;
 
 const mapStateToProps = () => ({ properties, resources }: RootState): BreadcrumbsDataProps => ({
-    items: (getProperty<ResourceBreadcrumb[]>(BREADCRUMBS)(properties) || []),
+    items: (getProperty<Breadcrumb[]>(BREADCRUMBS)(properties) || []),
     resources,
 });
 
 const mapDispatchToProps = (dispatch: Dispatch): BreadcrumbsActionProps => ({
-    onClick: ({ uuid }: Breadcrumb & ProjectResource) => {
+    onClick: ({ uuid }: Breadcrumb) => {
         dispatch<any>(navigateTo(uuid));
     },
-    onContextMenu: (event, breadcrumb: Breadcrumb & ProjectResource) => {
+    onContextMenu: (event, breadcrumb: Breadcrumb) => {
         dispatch<any>(openSidePanelContextMenu(event, breadcrumb.uuid));
     }
 });
 
-export const Breadcrumbs = connect(mapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
\ No newline at end of file
+export const Breadcrumbs = connect(mapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
index 06d97038e759c96712502ab52f6e9c80ba2af3c1..46aca4554ddfe506deda4caca5e08e869d40a2af 100644 (file)
@@ -27,7 +27,6 @@ const mapStateToProps = (state: RootState, { id }: Props) => {
     const currentRoute = state.router.location ? state.router.location.pathname : '';
     const currentRefresh = localStorage.getItem(LAST_REFRESH_TIMESTAMP) || '';
     const currentItemUuid = currentRoute === '/workflows' ? state.properties.workflowPanelDetailsUuid : state.detailsPanel.resourceUuid;
-
     return {
         ...dataExplorerState,
         working: !!progress?.working,
index cbe815c0582897e19064e622791c03fa83625152..d274157c48e2b1cd22804179fa33954c4b8fb361 100644 (file)
@@ -61,9 +61,10 @@ import { getUserUuid } from 'common/getuser';
 import { VirtualMachinesResource } from 'models/virtual-machines';
 import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
 import { ProjectResource } from 'models/project';
+import { ProcessResource } from 'models/process';
 
-const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
 
+const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
     const navFunc = ("groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo);
     return <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
@@ -89,6 +90,7 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
     </Grid>;
 };
 
+
 const FrozenProject = (props: {item: ProjectResource}) => {
     const [fullUsername, setFullusername] = React.useState<any>(null);
     const getFullName = React.useCallback(() => {
@@ -113,6 +115,7 @@ export const ResourceName = connect(
         return resource;
     })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
 
+    
 const renderIcon = (item: GroupContentsResource) => {
     switch (item.kind) {
         case ResourceKind.PROJECT:
@@ -227,7 +230,12 @@ export const UserResourceFullName = connect(
 const renderUuid = (item: { uuid: string }) =>
     <Typography data-cy="uuid" noWrap>
         {item.uuid}
-        <CopyToClipboardSnackbar value={item.uuid} />
+        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-' }
+    </Typography>;
+
+const renderUuidCopyIcon = (item: { uuid: string }) =>
+    <Typography data-cy="uuid" noWrap>
+        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-' }
     </Typography>;
 
 export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
@@ -446,7 +454,7 @@ export const ResourceCluster = (props: { uuid: string }) => {
 
 // Links Resources
 const renderLinkName = (item: { name: string }) =>
-    <Typography noWrap>{item.name || '(none)'}</Typography>;
+    <Typography noWrap>{item.name || '-'}</Typography>;
 
 export const ResourceLinkName = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -664,17 +672,69 @@ export const ResourceWorkflowStatus = connect(
         };
     })((props: { ownerUuid?: string, uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
 
-export const ResourceLastModifiedDate = connect(
+export const ResourceContainerUuid = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
-        return { date: resource ? resource.modifiedAt : '' };
-    })((props: { date: string }) => renderDate(props.date));
+        const process = getProcess(props.uuid)(state.resources)
+        return { uuid: process?.container?.uuid ? process?.container?.uuid : '' };
+    })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
+
+enum ColumnSelection {
+    OUTPUT_UUID = 'outputUuid',
+    LOG_UUID = 'logUuid'
+}
+
+const renderUuidLinkWithCopyIcon = (dispatch: Dispatch, item: ProcessResource, column: string) => {
+    const selectedColumnUuid = item[column]
+    return <Grid container alignItems="center" wrap="nowrap" >
+        <Grid item>
+            {selectedColumnUuid ? 
+                <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} noWrap 
+                    onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}>
+                    {selectedColumnUuid} 
+                </Typography> 
+            : '-' }
+        </Grid>
+        <Grid item>
+            {selectedColumnUuid && renderUuidCopyIcon({ uuid: selectedColumnUuid })}
+        </Grid>
+    </Grid>;
+};
+
+export const ResourceOutputUuid = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<ProcessResource>(props.uuid)(state.resources);
+        return resource;
+    })((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.OUTPUT_UUID));
+
+export const ResourceLogUuid = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<ProcessResource>(props.uuid)(state.resources);
+        return resource;
+    })((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.LOG_UUID));
+
+export const ResourceParentProcess = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const process = getProcess(props.uuid)(state.resources)
+        return { parentProcess: process?.containerRequest?.requestingContainerUuid || '' };
+    })((props: { parentProcess: string }) => renderUuid({uuid: props.parentProcess}));
+
+export const ResourceModifiedByUserUuid = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const process = getProcess(props.uuid)(state.resources)
+        return { userUuid: process?.containerRequest?.modifiedByUserUuid || '' };
+    })((props: { userUuid: string }) => renderUuid({uuid: props.userUuid}));
 
-export const ResourceCreatedAtDate = connect(
+    export const ResourceCreatedAtDate = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return { date: resource ? resource.createdAt : '' };
     })((props: { date: string }) => renderDate(props.date));
+    
+export const ResourceLastModifiedDate = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
+        return { date: resource ? resource.modifiedAt : '' };
+    })((props: { date: string }) => renderDate(props.date));
 
 export const ResourceTrashDate = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -706,7 +766,7 @@ export const ResourceFileSize = connect(
 
 const renderOwner = (owner: string) =>
     <Typography noWrap>
-        {owner}
+        {owner || '-'}
     </Typography>;
 
 export const ResourceOwner = connect(
@@ -723,6 +783,45 @@ export const ResourceOwnerName = connect(
         return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
     })((props: { owner: string }) => renderOwner(props.owner));
 
+export const ResourceUUID = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        return { uuid: resource ? resource.uuid : '' };
+    })((props: { uuid: string }) => renderUuid({uuid: props.uuid}));
+
+const renderVersion = (version: number) =>{
+    return <Typography>{version ?? '-'}</Typography>
+}
+
+export const ResourceVersion = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        return { version: resource ? resource.version: '' };
+    })((props: { version: number }) => renderVersion(props.version));
+    
+const renderPortableDataHash = (portableDataHash:string | null) => 
+    <Typography noWrap>
+        {portableDataHash ? <>{portableDataHash}
+        <CopyToClipboardSnackbar value={portableDataHash} /></> : '-' }
+    </Typography>
+    
+export const ResourcePortableDataHash = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        return { portableDataHash: resource ? resource.portableDataHash : '' };    
+    })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
+
+
+const renderFileCount = (fileCount: number) =>{
+    return <Typography>{fileCount ?? '-'}</Typography>
+}
+
+export const ResourceFileCount = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        return { fileCount: resource ? resource.fileCount: '' };
+    })((props: { fileCount: number }) => renderFileCount(props.fileCount));
+
 const userFromID =
     connect(
         (state: RootState, props: { uuid: string }) => {
@@ -745,11 +844,14 @@ const ownerFromResourceId =
         userFromID
     );
 
+
+
+
+
 const _resourceWithName =
     withStyles({}, { withTheme: true })
         ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
             const { uuid, userFullname, dispatch, theme } = props;
-
             if (userFullname === '') {
                 dispatch<any>(loadResource(uuid, false));
                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
@@ -766,6 +868,8 @@ export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
 
 export const ResourceWithName = userFromID(_resourceWithName);
 
+
+
 export const UserNameFromID =
     compose(userFromID)(
         (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
@@ -938,6 +1042,6 @@ export const ContainerRunTime = connect((state: RootState, props: { uuid: string
     }
 
     render() {
-        return renderRunTime(this.state.runtime);
+        return this.props.process ? renderRunTime(this.state.runtime) : <Typography>-</Typography>;
     }
 });
index 7a3c5fddd1255c1c3f92c30d5ce137fdf2460f9e..3c584e4f51452381d56296e753ad182123af82f2 100644 (file)
@@ -24,8 +24,8 @@ export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyPr
 export const CollectionPartialCopyFields = memoize(
     (pickerId: string) =>
         () =>
-            <div>
+            <>
                 <CollectionNameField />
                 <CollectionDescriptionField />
                 <CollectionProjectPickerField {...{ pickerId }} />
-            </div>);
+            </>);
index 5605e6caa866c266990caa9722ac94be96004aef..a3e301195493be96d99b9b0c3c96648b2b6828e8 100644 (file)
@@ -25,7 +25,7 @@ export const DialogCopy = (props: CopyFormDialogProps & PickerIdProp) =>
 
 const CopyDialogFields = memoize((pickerId: string) =>
     () =>
-        <span>
+        <>
             <Field
                 name='name'
                 component={TextField as any}
@@ -36,4 +36,4 @@ const CopyDialogFields = memoize((pickerId: string) =>
                 component={ProjectTreePickerField}
                 validate={COPY_FILE_VALIDATION}
                 pickerId={pickerId}/>
-        </span>);
+        </>);
index a79ed0bcce76250490232ec0f94bb327655d6669..4e9dde6a12bc2ef56e1fafa1cf34d73329e660d8 100644 (file)
@@ -24,6 +24,6 @@ export const DialogCollectionPartialCopyToSelectedCollection = (props: DialogCol
 export const CollectionPartialCopyFields = memoize(
     (pickerId: string) =>
         () =>
-            <div>
+            <>
                 <CollectionPickerField {...{ pickerId }}/>
-            </div>);
+            </>);
index cde286c450c4764be139b288c65ab3ab55f9d4e2..be59261722377a7001b59c85bbac3d72dbf1536a 100644 (file)
@@ -36,7 +36,7 @@ const mapDispatchToProps = (dispatch: Dispatch, { onDrop }: FileUploaderProps):
 export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpload);
 
 export const FileUploaderField = (props: WrappedFieldProps & { label?: string }) =>
-    <div>
+    <>
         <Typography variant='caption'>{props.label}</Typography>
         <FileUploader disabled={false} onDrop={props.input.onChange} />
-    </div>;
+    </>;
index 777fa824dfbcb7df0233d0058a921e40d54bf9bf..be28ba2a29d7a449d7225b7fcd5e3ad8b5c39f6a 100644 (file)
@@ -3,20 +3,17 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from "react";
-import { Field, WrappedFieldProps, FieldArray } from 'redux-form';
+import { Field, FieldArray } from 'redux-form';
 import { TextField, DateTextField } from "components/text-field/text-field";
 import { CheckboxField } from 'components/checkbox-field/checkbox-field';
 import { NativeSelectField } from 'components/select-field/select-field';
 import { ResourceKind } from 'models/resource';
-import { HomeTreePicker } from 'views-components/projects-tree-picker/home-tree-picker';
-import { SEARCH_BAR_ADVANCED_FORM_PICKER_ID } from 'store/search-bar/search-bar-actions';
 import { SearchBarAdvancedPropertiesView } from 'views-components/search-bar/search-bar-advanced-properties-view';
-import { TreeItem } from "components/tree/tree";
-import { ProjectsTreePickerItem } from "views-components/projects-tree-picker/generic-projects-tree-picker";
 import { PropertyKeyField, } from 'views-components/resource-properties-form/property-key-field';
 import { PropertyValueField } from 'views-components/resource-properties-form/property-value-field';
 import { connect } from "react-redux";
 import { RootState } from "store/store";
+import { ProjectInput, ProjectCommandInputParameter } from 'views/run-process-panel/inputs/project-input';
 
 export const SearchBarTypeField = () =>
     <Field
@@ -36,7 +33,7 @@ interface SearchBarClusterFieldProps {
 
 export const SearchBarClusterField = connect(
     (state: RootState) => ({
-        clusters: [{key: '', value: 'Any'}].concat(
+        clusters: [{ key: '', value: 'Any' }].concat(
             state.auth.sessions
                 .filter(s => s.loggedIn)
                 .map(s => ({
@@ -46,24 +43,15 @@ export const SearchBarClusterField = connect(
     }))((props: SearchBarClusterFieldProps) => <Field
         name='cluster'
         component={NativeSelectField as any}
-        items={props.clusters}/>
+        items={props.clusters} />
     );
 
 export const SearchBarProjectField = () =>
-    <Field
-        name='projectUuid'
-        component={ProjectsPicker} />;
-
-const ProjectsPicker = (props: WrappedFieldProps) =>
-    <div style={{ height: '100px', display: 'flex', flexDirection: 'column', overflow: 'overlay' }}>
-        <HomeTreePicker
-            pickerId={SEARCH_BAR_ADVANCED_FORM_PICKER_ID}
-            toggleItemActive={
-                (_: any, { id }: TreeItem<ProjectsTreePickerItem>) => {
-                    props.input.onChange(id);
-                }
-            } />
-    </div>;
+    <ProjectInput input={{
+        id: "projectObject",
+        label: "Limit search to Project"
+    } as ProjectCommandInputParameter}
+        options={{ showOnlyOwned: false, showOnlyWritable: false }} />
 
 export const SearchBarTrashField = () =>
     <Field
index 271d77c1085319854c8edec9d887ff0968bc233c..3f4de301f2b465e59ea94e01c25116a23bba9257 100644 (file)
@@ -15,9 +15,15 @@ import RefreshButton from "components/refresh-button/refresh-button";
 import { loadSidePanelTreeProjects } from "store/side-panel-tree/side-panel-tree-actions";
 import { Dispatch } from "redux";
 
-type CssRules = "infoTooltip";
+type CssRules = 'mainBar' | 'breadcrumbContainer' | 'infoTooltip';
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
+    mainBar: {
+        flexWrap: 'nowrap',
+    },
+    breadcrumbContainer: {
+        overflow: 'hidden',
+    },
     infoTooltip: {
         marginTop: '-10px',
         marginLeft: '10px',
@@ -61,8 +67,8 @@ const mapDispatchToProps = () => (dispatch: Dispatch) => ({
 
 export const MainContentBar = connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(
     (props: MainContentBarProps & WithStyles<CssRules> & any) =>
-        <Toolbar><Grid container>
-            <Grid container item xs alignItems="center">
+        <Toolbar><Grid container className={props.classes.mainBar}>
+            <Grid container item xs alignItems="center" className={props.classes.breadcrumbContainer}>
                 <Breadcrumbs />
             </Grid>
             <Grid item>
index dd6e63bfc87fd8d9a77f082e303bce94714554cc..11b51caa884e5b52df00f396f87f4be16d21fa3b 100644 (file)
@@ -12,18 +12,12 @@ import { treePickerActions } from "store/tree-picker/tree-picker-actions";
 import { ListItemTextIcon } from "components/list-item-text-icon/list-item-text-icon";
 import { ProjectIcon, FileInputIcon, IconType, CollectionIcon } from 'components/icon/icon';
 import { loadProject, loadCollection } from 'store/tree-picker/tree-picker-actions';
-import { GroupContentsResource } from 'services/groups-service/groups-service';
-import { CollectionDirectory, CollectionFile, CollectionFileType } from 'models/collection-file';
+import { ProjectsTreePickerItem, ProjectsTreePickerRootItem } from 'store/tree-picker/tree-picker-middleware';
 import { ResourceKind } from 'models/resource';
 import { TreePickerProps, TreePicker } from "views-components/tree-picker/tree-picker";
-import { LinkResource } from "models/link";
+import { CollectionFileType } from 'models/collection-file';
 
-export interface ProjectsTreePickerRootItem {
-    id: string;
-    name: string;
-}
 
-export type ProjectsTreePickerItem = ProjectsTreePickerRootItem | GroupContentsResource | CollectionDirectory | CollectionFile | LinkResource;
 type PickedTreePickerProps = Pick<TreePickerProps<ProjectsTreePickerItem>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
 
 export interface ProjectsTreePickerDataProps {
@@ -35,7 +29,7 @@ export interface ProjectsTreePickerDataProps {
     disableActivation?: string[];
     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
     loadRootItem: (item: TreeItem<ProjectsTreePickerRootItem>, pickerId: string,
-         includeCollections?: boolean, includeFiles?: boolean, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) => void;
+        includeCollections?: boolean, includeFiles?: boolean, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) => void;
 }
 
 export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & Partial<PickedTreePickerProps>;
index 4e8eeda987e9fbac4123902c4a4976da088662fb..3133c5dbb951ed9632630dc5a1546aee89c62bfb 100644 (file)
@@ -6,12 +6,12 @@ import { connect } from 'react-redux';
 import { ProjectsTreePicker, ProjectsTreePickerProps } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
 import { Dispatch } from 'redux';
 import { loadUserProject } from 'store/tree-picker/tree-picker-actions';
-import { ProjectIcon } from 'components/icon/icon';
+import { ProjectsIcon } from 'components/icon/icon';
 
 export const HomeTreePicker = connect(() => ({
-    rootItemIcon: ProjectIcon,
+    rootItemIcon: ProjectsIcon,
 }), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
     loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
         dispatch<any>(loadUserProject(pickerId, includeCollections, includeFiles, options));
     },
-}))(ProjectsTreePicker);
\ No newline at end of file
+}))(ProjectsTreePicker);
index c4a6e3ab298cedc59e1aaaebd978265384245705..9ac0b64fc0e580fcdd4dc64e5db82d5319a3510c 100644 (file)
@@ -3,16 +3,26 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
+import { Dispatch } from 'redux';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from 'store/store';
 import { values, pipe } from 'lodash/fp';
 import { HomeTreePicker } from 'views-components/projects-tree-picker/home-tree-picker';
 import { SharedTreePicker } from 'views-components/projects-tree-picker/shared-tree-picker';
 import { FavoritesTreePicker } from 'views-components/projects-tree-picker/favorites-tree-picker';
-import { getProjectsTreePickerIds, SHARED_PROJECT_ID, FAVORITES_PROJECT_ID } from 'store/tree-picker/tree-picker-actions';
+import { SearchProjectsPicker } from 'views-components/projects-tree-picker/search-projects-picker';
+import {
+    getProjectsTreePickerIds, treePickerActions, treePickerSearchActions, initProjectsTreePicker,
+    SHARED_PROJECT_ID, FAVORITES_PROJECT_ID
+} from 'store/tree-picker/tree-picker-actions';
 import { TreeItem } from 'components/tree/tree';
-import { ProjectsTreePickerItem } from './generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { PublicFavoritesTreePicker } from './public-favorites-tree-picker';
+import { SearchInput } from 'components/search-input/search-input';
+import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
 
-export interface ProjectsTreePickerProps {
+export interface ToplevelPickerProps {
     pickerId: string;
     includeCollections?: boolean;
     includeFiles?: boolean;
@@ -22,29 +32,146 @@ export interface ProjectsTreePickerProps {
     toggleItemSelection?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectsTreePickerItem>, pickerId: string) => void;
 }
 
-export const ProjectsTreePicker = ({ pickerId, ...props }: ProjectsTreePickerProps) => {
-    const { home, shared, favorites, publicFavorites } = getProjectsTreePickerIds(pickerId);
-    const relatedTreePickers = getRelatedTreePickers(pickerId);
-    const p = {
+interface ProjectsTreePickerSearchProps {
+    projectSearch: string;
+    collectionFilter: string;
+}
+
+interface ProjectsTreePickerActionProps {
+    onProjectSearch: (value: string) => void;
+    onCollectionFilter: (value: string) => void;
+}
+
+const mapStateToProps = (state: RootState, props: ToplevelPickerProps): ProjectsTreePickerSearchProps => {
+    const { search } = getProjectsTreePickerIds(props.pickerId);
+    return {
         ...props,
-        relatedTreePickers,
-        disableActivation
+        projectSearch: state.treePickerSearch.projectSearchValues[search],
+        collectionFilter: state.treePickerSearch.collectionFilterValues[search],
+    };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch, props: ToplevelPickerProps): (ProjectsTreePickerActionProps & DispatchProp) => {
+    const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(props.pickerId);
+    const params = {
+        includeCollections: props.includeCollections,
+        includeFiles: props.includeFiles,
+        options: props.options
     };
-    return <div>
-        <div data-cy="projects-tree-home-tree-picker">
-            <HomeTreePicker pickerId={home} {...p} />
-        </div>
-        <div data-cy="projects-tree-shared-tree-picker">
-            <SharedTreePicker pickerId={shared} {...p} />
-        </div>
-        <div data-cy="projects-tree-public-favourites-tree-picker">
-            <PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
-        </div>
-        <div data-cy="projects-tree-favourites-tree-picker">
-            <FavoritesTreePicker pickerId={favorites} {...p} />
-        </div>
-    </div>;
+    dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: home, params }));
+    dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: shared, params }));
+    dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: favorites, params }));
+    dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: publicFavorites, params }));
+    dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: search, params }));
+
+    return {
+        onProjectSearch: (projectSearchValue: string) => dispatch(treePickerSearchActions.SET_TREE_PICKER_PROJECT_SEARCH({ pickerId: search, projectSearchValue })),
+        onCollectionFilter: (collectionFilterValue: string) => {
+            dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: home, collectionFilterValue }));
+            dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: shared, collectionFilterValue }));
+            dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: favorites, collectionFilterValue }));
+            dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: publicFavorites, collectionFilterValue }));
+            dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: search, collectionFilterValue }));
+        },
+        dispatch
+    }
 };
 
+type CssRules = 'pickerHeight' | 'searchFlex' | 'scrolledBox';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    pickerHeight: {
+        height: "100%",
+        display: "flex",
+        flexDirection: "column",
+    },
+    searchFlex: {
+        display: "flex",
+        justifyContent: "space-around",
+        paddingBottom: "1em"
+    },
+    scrolledBox: {
+        overflow: "scroll"
+    }
+});
+
+type ProjectsTreePickerCombinedProps = ToplevelPickerProps & ProjectsTreePickerSearchProps & ProjectsTreePickerActionProps & DispatchProp & WithStyles<CssRules>;
+
+export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(
+    withStyles(styles)(
+        class FileInputComponent extends React.Component<ProjectsTreePickerCombinedProps> {
+
+            componentDidMount() {
+                const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(this.props.pickerId);
+
+                this.props.dispatch<any>(initProjectsTreePicker(this.props.pickerId));
+
+                this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_PROJECT_SEARCH({ pickerId: search, projectSearchValue: "" }));
+                this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: search, collectionFilterValue: "" }));
+                this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: home, collectionFilterValue: "" }));
+                this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: shared, collectionFilterValue: "" }));
+                this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: favorites, collectionFilterValue: "" }));
+                this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: publicFavorites, collectionFilterValue: "" }));
+            }
+
+            componentWillUnmount() {
+                const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(this.props.pickerId);
+                // Release all the state, we don't need it to hang around forever.
+                this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: search }));
+                this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: home }));
+                this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: shared }));
+                this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: favorites }));
+                this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: publicFavorites }));
+            }
+
+            render() {
+                const pickerId = this.props.pickerId;
+                const onProjectSearch = this.props.onProjectSearch;
+                const onCollectionFilter = this.props.onCollectionFilter;
+
+                const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
+                const relatedTreePickers = getRelatedTreePickers(pickerId);
+                const p = {
+                    includeCollections: this.props.includeCollections,
+                    includeFiles: this.props.includeFiles,
+                    showSelection: this.props.showSelection,
+                    options: this.props.options,
+                    toggleItemActive: this.props.toggleItemActive,
+                    toggleItemSelection: this.props.toggleItemSelection,
+                    relatedTreePickers,
+                    disableActivation,
+                };
+                return <div className={this.props.classes.pickerHeight} >
+                    <span className={this.props.classes.searchFlex}>
+                        <SearchInput value="" label="Search for a Project" selfClearProp='' onSearch={onProjectSearch} debounce={500} />
+                        {this.props.includeCollections &&
+                            <SearchInput value="" label="Filter Collections list in Projects" selfClearProp='' onSearch={onCollectionFilter} debounce={500} />}
+                    </span>
+
+                    <div className={this.props.classes.scrolledBox}>
+                        {this.props.projectSearch ?
+                            <div data-cy="projects-tree-search-picker">
+                                <SearchProjectsPicker {...p} pickerId={search} />
+                            </div>
+                            :
+                            <>
+                                <div data-cy="projects-tree-home-tree-picker">
+                                    <HomeTreePicker {...p} pickerId={home} />
+                                </div>
+                                <div data-cy="projects-tree-shared-tree-picker">
+                                    <SharedTreePicker {...p} pickerId={shared} />
+                                </div>
+                                <div data-cy="projects-tree-public-favourites-tree-picker">
+                                    <PublicFavoritesTreePicker {...p} pickerId={publicFavorites} />
+                                </div>
+                                <div data-cy="projects-tree-favourites-tree-picker">
+                                    <FavoritesTreePicker {...p} pickerId={favorites} />
+                                </div>
+                            </>}
+                    </div>
+                </div >;
+            }
+        }));
+
 const getRelatedTreePickers = pipe(getProjectsTreePickerIds, values);
 const disableActivation = [SHARED_PROJECT_ID, FAVORITES_PROJECT_ID];
diff --git a/src/views-components/projects-tree-picker/search-projects-picker.tsx b/src/views-components/projects-tree-picker/search-projects-picker.tsx
new file mode 100644 (file)
index 0000000..7bad8ef
--- /dev/null
@@ -0,0 +1,18 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from 'react-redux';
+import { ProjectsTreePicker, ProjectsTreePickerProps } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { Dispatch } from 'redux';
+import { SearchIcon } from 'components/icon/icon';
+import { loadProject } from 'store/tree-picker/tree-picker-actions';
+import { SEARCH_PROJECT_ID } from 'store/tree-picker/tree-picker-actions';
+
+export const SearchProjectsPicker = connect(() => ({
+    rootItemIcon: SearchIcon,
+}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
+    loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
+        dispatch<any>(loadProject({ id: SEARCH_PROJECT_ID, pickerId, includeCollections, includeFiles, searchProjects: true, options }));
+    },
+}))(ProjectsTreePicker);
index 201bd118bcd3b9d7c1ec12aa650cc4e4c9837c4d..c15df6ba0c29e9831b8f215c4bf154b1e6896521 100644 (file)
@@ -7,11 +7,12 @@ import { ProjectsTreePicker, ProjectsTreePickerProps } from 'views-components/pr
 import { Dispatch } from 'redux';
 import { ShareMeIcon } from 'components/icon/icon';
 import { loadProject } from 'store/tree-picker/tree-picker-actions';
+import { SHARED_PROJECT_ID } from 'store/tree-picker/tree-picker-actions';
 
 export const SharedTreePicker = connect(() => ({
     rootItemIcon: ShareMeIcon,
 }), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
     loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
-        dispatch<any>(loadProject({ id: 'Shared with me', pickerId, includeCollections, includeFiles, loadShared: true, options }));
+        dispatch<any>(loadProject({ id: SHARED_PROJECT_ID, pickerId, includeCollections, includeFiles, loadShared: true, options }));
     },
-}))(ProjectsTreePicker);
\ No newline at end of file
+}))(ProjectsTreePicker);
index e5fecf9799fe03e898d6c641138a3b5e27ef22ab..357058c54bf1c1d6564044b8159b2f47d2cc3282 100644 (file)
@@ -7,19 +7,21 @@ import { Typography } from "@material-ui/core";
 import { TreeItem } from "components/tree/tree";
 import { WrappedFieldProps } from 'redux-form';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { PickerIdProp } from 'store/tree-picker/picker-id';
 
 export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
-    <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
-        <ProjectsTreePicker
-            pickerId={props.pickerId}
-            toggleItemActive={handleChange(props)}
-            options={{ showOnlyOwned: false, showOnlyWritable: true }} />
-        {props.meta.dirty && props.meta.error &&
-            <Typography variant='caption' color='error'>
-                {props.meta.error}
-            </Typography>}
+    <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
+        <div style={{ flexBasis: '200px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+            <ProjectsTreePicker
+                pickerId={props.pickerId}
+                toggleItemActive={handleChange(props)}
+                options={{ showOnlyOwned: false, showOnlyWritable: true }} />
+            {props.meta.dirty && props.meta.error &&
+                <Typography variant='caption' color='error'>
+                    {props.meta.error}
+                </Typography>}
+        </div>
     </div>;
 
 const handleChange = (props: WrappedFieldProps) =>
@@ -27,14 +29,16 @@ const handleChange = (props: WrappedFieldProps) =>
         props.input.onChange(id);
 
 export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
-    <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
-        <ProjectsTreePicker
-            pickerId={props.pickerId}
-            toggleItemActive={handleChange(props)}
-            options={{ showOnlyOwned: false, showOnlyWritable: true }}
-            includeCollections />
-        {props.meta.dirty && props.meta.error &&
-            <Typography variant='caption' color='error'>
-                {props.meta.error}
-            </Typography>}
-    </div>;
\ No newline at end of file
+    <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
+        <div style={{ flexBasis: '200px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+            <ProjectsTreePicker
+                pickerId={props.pickerId}
+                toggleItemActive={handleChange(props)}
+                options={{ showOnlyOwned: false, showOnlyWritable: true }}
+                includeCollections />
+            {props.meta.dirty && props.meta.error &&
+                <Typography variant='caption' color='error'>
+                    {props.meta.error}
+                </Typography>}
+        </div>
+    </div>;
index b8e525bf675ad5ebe6e7171e2798a393d2ea8855..8941d441a821fd9cbbfca21fc70544e87d19304e 100644 (file)
@@ -89,7 +89,7 @@ const getValidation = (props: PropertyValueFieldProps) =>
 
 const matchTagValues = ({ vocabulary, propertyKeyId }: PropertyValueFieldProps) =>
     (value: string) =>
-        getTagValues(propertyKeyId, vocabulary).find(v => v.label === value)
+        getTagValues(propertyKeyId, vocabulary).find(v => !value || v.label === value)
             ? undefined
             : 'Incorrect value';
 
index 25d0f2bb377e8a9bcb567838c212558a542ff4e8..0147312912730849da6fceba14be2fa799f53ec6 100644 (file)
@@ -3,13 +3,29 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { InjectedFormProps } from 'redux-form';
+import { RootState } from 'store/store';
+import { connect } from 'react-redux';
+import { formValueSelector, InjectedFormProps } from 'redux-form';
 import { Grid, withStyles, WithStyles } from '@material-ui/core';
 import { PropertyKeyField, PROPERTY_KEY_FIELD_NAME, PROPERTY_KEY_FIELD_ID } from './property-key-field';
 import { PropertyValueField, PROPERTY_VALUE_FIELD_NAME, PROPERTY_VALUE_FIELD_ID } from './property-value-field';
 import { ProgressButton } from 'components/progress-button/progress-button';
 import { GridClassKey } from '@material-ui/core/Grid';
 
+const AddButton = withStyles(theme => ({
+    root: { marginTop: theme.spacing.unit }
+}))(ProgressButton);
+
+const mapStateToProps = (state: RootState) => {
+    return {
+        applySelector: (selector) => selector(state, 'key', 'value', 'keyID', 'valueID')
+    }
+}
+
+interface ApplySelector {
+    applySelector: (selector) => any;
+}
+
 export interface ResourcePropertiesFormData {
     uuid: string;
     [PROPERTY_KEY_FIELD_NAME]: string;
@@ -19,10 +35,11 @@ export interface ResourcePropertiesFormData {
     clearPropertyKeyOnSelect?: boolean;
 }
 
-export type ResourcePropertiesFormProps = {uuid: string; clearPropertyKeyOnSelect?: boolean } & InjectedFormProps<ResourcePropertiesFormData, {uuid: string; }> & WithStyles<GridClassKey>;
+type ResourcePropertiesFormProps = {uuid: string; clearPropertyKeyOnSelect?: boolean } & InjectedFormProps<ResourcePropertiesFormData, {uuid: string;}> & WithStyles<GridClassKey> & ApplySelector;
 
-export const ResourcePropertiesForm = ({ handleSubmit, change, submitting, invalid, classes, uuid, clearPropertyKeyOnSelect }: ResourcePropertiesFormProps ) => {
+export const ResourcePropertiesForm = connect(mapStateToProps)(({ handleSubmit, change, submitting, invalid, classes, uuid, clearPropertyKeyOnSelect, applySelector,  ...props }: ResourcePropertiesFormProps ) => {
     change('uuid', uuid); // Sets the uuid field to the uuid of the resource.
+    const propertyValue = applySelector(formValueSelector(props.form));
     return <form data-cy='resource-properties-form' onSubmit={handleSubmit}>
         <Grid container spacing={16} classes={classes}>
             <Grid item xs>
@@ -32,19 +49,16 @@ export const ResourcePropertiesForm = ({ handleSubmit, change, submitting, inval
                 <PropertyValueField />
             </Grid>
             <Grid item>
-                <Button
+                <AddButton
                     data-cy='property-add-btn'
-                    disabled={invalid}
+                    disabled={invalid || !(propertyValue.key && propertyValue.value)}
                     loading={submitting}
                     color='primary'
                     variant='contained'
                     type='submit'>
                     Add
-                </Button>
+                </AddButton>
             </Grid>
         </Grid>
-    </form>};
-
-export const Button = withStyles(theme => ({
-    root: { marginTop: theme.spacing.unit }
-}))(ProgressButton);
+    </form>}
+);
\ No newline at end of file
index c7e3376745d1442136770ceb90ab602712d2147b..de271203e870cc4e86b8aa5e42954d9ea001e4c0 100644 (file)
@@ -65,7 +65,7 @@ const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
             ? FilterGroupIcon
             : ProjectIcon;
 
-const getSidePanelIcon = (category: string) => {
+export const getSidePanelIcon = (category: string) => {
     switch (category) {
         case SidePanelTreeCategory.FAVORITES:
             return FavoriteIcon;
index 8a7c7928933925e77462d7d8b5e4e8d0875bbbdd..064add3a6d940499daf3e600b6a62e9a36df80fb 100644 (file)
@@ -17,16 +17,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         position: 'relative',
         backgroundColor: theme.palette.grey["200"],
-        '&::after': {
-            content: `''`,
-            position: 'absolute',
-            top: 0,
-            left: 0,
-            bottom: 0,
-            right: 0,
-            background: 'url("arvados-logo-big.png") no-repeat center center',
-            opacity: 0.2,
-        }
+        background: 'url("arvados-logo-big.png") no-repeat center center',
+        backgroundBlendMode: 'soft-light',
     },
     ontop: {
         zIndex: 10
@@ -59,7 +51,6 @@ export interface InactivePanelStateProps {
 
 type InactivePanelProps = WithStyles<CssRules> & InactivePanelActionProps & InactivePanelStateProps;
 
-
 export const InactivePanelRoot = ({ classes, startLinking, inactivePageText, isLoginClusterFederation }: InactivePanelProps) =>
     <Grid container justify="center" alignItems="center" direction="column" spacing={24}
         className={classes.root}
index 3ac4d75519ed50d4d701cc813b86b3512c3bf97e..045bfca2113cce747cfdb3b099141d70fbb29efa 100644 (file)
@@ -242,9 +242,13 @@ type ProcessIOCardProps = ProcessIOCardDataProps & ProcessIOCardActionProps & Wi
 export const ProcessIOCard = withStyles(styles)(connect(null, mapDispatchToProps)(
     ({ classes, label, params, raw, mounts, outputUuid, doHidePanel, doMaximizePanel, doUnMaximizePanel, panelMaximized, panelName, process, navigateTo }: ProcessIOCardProps) => {
         const [mainProcTabState, setMainProcTabState] = useState(0);
+        const [subProcTabState, setSubProcTabState] = useState(0);
         const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
             setMainProcTabState(value);
         }
+        const handleSubProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+            setSubProcTabState(value);
+        }
 
         const [showImagePreview, setShowImagePreview] = useState(false);
 
@@ -255,6 +259,10 @@ export const ProcessIOCard = withStyles(styles)(connect(null, mapDispatchToProps
         const hasRaw = !!(raw && Object.keys(raw).length > 0);
         const hasParams = !!(params && params.length > 0);
 
+        // Subprocess
+        const hasInputMounts = !!(label === ProcessIOCardType.INPUT && mounts && mounts.length);
+        const hasOutputCollecton = !!(label === ProcessIOCardType.OUTPUT && outputUuid);
+
         return <Card className={classes.card} data-cy="process-io-card">
             <CardHeader
                 className={classes.header}
@@ -318,25 +326,32 @@ export const ProcessIOCard = withStyles(styles)(connect(null, mapDispatchToProps
                     </>) :
                     // Subprocess
                     (<>
-                        {((mounts && mounts.length) || outputUuid) ?
+                        {loading && <Grid container item alignItems='center' justify='center'>
+                            <CircularProgress />
+                        </Grid>}
+                        {!loading && (hasInputMounts || hasOutputCollecton || hasRaw) ?
                             <>
-                                <Tabs value={0} variant="fullWidth" className={classes.symmetricTabs}>
-                                    {label === ProcessIOCardType.INPUT && <Tab label="Collections" />}
-                                    {label === ProcessIOCardType.OUTPUT && <Tab label="Collection" />}
+                                <Tabs value={subProcTabState} onChange={handleSubProcTabChange} variant="fullWidth" className={classes.symmetricTabs}>
+                                    {hasInputMounts && <Tab label="Collections" />}
+                                    {hasOutputCollecton && <Tab label="Collection" />}
+                                    <Tab label="JSON" />
                                 </Tabs>
                                 <div className={classes.tableWrapper}>
-                                    {label === ProcessIOCardType.INPUT && <ProcessInputMounts mounts={mounts || []} />}
-                                    {label === ProcessIOCardType.OUTPUT && <>
+                                    {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
+                                    {subProcTabState === 0 && hasOutputCollecton && <>
                                         {outputUuid && <Typography className={classes.collectionLink}>
                                             Output Collection: <MuiLink className={classes.keepLink} onClick={() => {navigateTo(outputUuid || "")}}>
                                             {outputUuid}
                                         </MuiLink></Typography>}
                                         <ProcessOutputCollectionFiles isWritable={false} currentItemUuid={outputUuid} />
                                     </>}
+                                    {(subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && <div className={classes.tableWrapper}>
+                                        <ProcessIORaw data={raw} />
+                                    </div>}
                                 </div>
                             </> :
                             <Grid container item alignItems='center' justify='center'>
-                                <DefaultView messages={["No collection(s) found"]} />
+                                <DefaultView messages={["No data to display"]} />
                             </Grid>
                         }
                     </>)
index d9d14ae31c14252330be10990f8fb1eb7ad04e08..67fc4b351e8f1012741d33c6a83026407ab65972 100644 (file)
@@ -16,14 +16,27 @@ import { ContainerRequestState } from 'models/container-request';
 import { SortDirection } from 'components/data-table/data-column';
 import { ResourceKind, Resource } from 'models/resource';
 import {
+    ResourceName,
+    ProcessStatus as ResourceStatus,
+    ResourceType,
+    ResourceOwnerWithName,
+    ResourcePortableDataHash,
     ResourceFileSize,
+    ResourceFileCount,
+    ResourceUUID,
+    ResourceContainerUuid,
+    ContainerRunTime,
+    ResourceOutputUuid,
+    ResourceLogUuid,
+    ResourceParentProcess,
+    ResourceModifiedByUserUuid,
+    ResourceVersion,
+    ResourceCreatedAtDate,
     ResourceLastModifiedDate,
-    ProcessStatus,
-    ResourceType,
-    ResourceOwnerWithName
+    ResourceTrashDate,
+    ResourceDeleteDate,
 } from 'views-components/data-explorer/renderers';
 import { ProjectIcon } from 'components/icon/icon';
-import { ResourceName } from 'views-components/data-explorer/renderers';
 import {
     ResourcesState,
     getResource
@@ -63,8 +76,21 @@ export enum ProjectPanelColumnNames {
     STATUS = "Status",
     TYPE = "Type",
     OWNER = "Owner",
-    FILE_SIZE = "File size",
-    LAST_MODIFIED = "Last modified"
+    PORTABLE_DATA_HASH = "Portable Data Hash",
+    FILE_SIZE = "File Size",
+    FILE_COUNT = "File Count",
+    UUID = "UUID",
+    CONTAINER_UUID = "Container UUID",
+    RUNTIME = "Runtime",
+    OUTPUT_UUID = "Output UUID",
+    LOG_UUID = "Log UUID",
+    PARENT_PROCESS = 'Parent Process UUID',
+    MODIFIED_BY_USER_UUID = 'Modified by User UUID',
+    VERSION = "Version",
+    CREATED_AT = "Date Created",
+    LAST_MODIFIED = "Last Modified",
+    TRASH_AT = "Trash at",
+    DELETE_AT = "Delete at",
 }
 
 export interface ProjectPanelFilter extends DataTableFilterItem {
@@ -81,12 +107,12 @@ export const projectPanelColumns: DataColumns<string> = [
         render: uuid => <ResourceName uuid={uuid} />
     },
     {
-        name: "Status",
+        name: ProjectPanelColumnNames.STATUS,
         selected: true,
         configurable: true,
         mutuallyExclusiveFilters: true,
         filters: getInitialProcessStatusFilters(),
-        render: uuid => <ProcessStatus uuid={uuid} />,
+        render: uuid => <ResourceStatus uuid={uuid} />,
     },
     {
         name: ProjectPanelColumnNames.TYPE,
@@ -102,6 +128,13 @@ export const projectPanelColumns: DataColumns<string> = [
         filters: createTree(),
         render: uuid => <ResourceOwnerWithName uuid={uuid} />
     },
+    {
+        name: ProjectPanelColumnNames.PORTABLE_DATA_HASH,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourcePortableDataHash uuid={uuid}/>
+    },
     {
         name: ProjectPanelColumnNames.FILE_SIZE,
         selected: true,
@@ -109,6 +142,77 @@ export const projectPanelColumns: DataColumns<string> = [
         filters: createTree(),
         render: uuid => <ResourceFileSize uuid={uuid} />
     },
+    {
+        name: ProjectPanelColumnNames.FILE_COUNT,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid =><ResourceFileCount uuid={uuid}/>
+    },
+    {
+        name: ProjectPanelColumnNames.UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceUUID uuid={uuid}/>
+    },
+    {
+        name: ProjectPanelColumnNames.CONTAINER_UUID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceContainerUuid uuid={uuid}/>
+    },
+    {
+        name: ProjectPanelColumnNames.RUNTIME,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ContainerRunTime uuid={uuid} />
+    },
+    {
+        name: ProjectPanelColumnNames.OUTPUT_UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceOutputUuid uuid={uuid}/>
+    },
+    {
+        name: ProjectPanelColumnNames.LOG_UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLogUuid uuid={uuid}/>
+    },
+    {
+        name: ProjectPanelColumnNames.PARENT_PROCESS,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceParentProcess uuid={uuid}/>
+    },
+    {
+        name: ProjectPanelColumnNames.MODIFIED_BY_USER_UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceModifiedByUserUuid uuid={uuid}/>
+    },
+    {
+        name: ProjectPanelColumnNames.VERSION,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: uuid =><ResourceVersion uuid={uuid}/>
+    },
+    {
+        name: ProjectPanelColumnNames.CREATED_AT,
+        selected: false,
+        configurable: true,
+        sortDirection: SortDirection.DESC,
+        filters: createTree(),
+        render: uuid =><ResourceCreatedAtDate uuid={uuid}/>
+    },
     {
         name: ProjectPanelColumnNames.LAST_MODIFIED,
         selected: true,
@@ -116,7 +220,24 @@ export const projectPanelColumns: DataColumns<string> = [
         sortDirection: SortDirection.DESC,
         filters: createTree(),
         render: uuid => <ResourceLastModifiedDate uuid={uuid} />
-    }
+    },
+    {
+        name: ProjectPanelColumnNames.TRASH_AT,
+        selected: false,
+        configurable: true,
+        sortDirection: SortDirection.DESC,
+        filters: createTree(),
+        render: uuid => <ResourceTrashDate uuid={uuid} />
+    },
+    {
+        name: ProjectPanelColumnNames.DELETE_AT,
+        selected: false,
+        configurable: true,
+        sortDirection: SortDirection.DESC,
+        filters: createTree(),
+        render: uuid => <ResourceDeleteDate uuid={uuid} />
+    },
+    
 ];
 
 export const PROJECT_PANEL_ID = "projectPanel";
index 1b04718df8bcb169177935b10a8a10e250864c4b..e64dca0e49b9b8a8e55514267718108b82557662 100644 (file)
@@ -16,7 +16,7 @@ import { GenericInputProps, GenericInput } from './generic-input';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
 import { connect, DispatchProp } from 'react-redux';
 import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, getAllNodes } from 'store/tree-picker/tree-picker-actions';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { createSelector, createStructuredSelector } from 'reselect';
 import { ChipsInput } from 'components/chips-input/chips-input';
 import { identity, values, noop } from 'lodash';
@@ -231,31 +231,17 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
                 onBlur={this.props.input.onBlur}
                 disabled={this.props.commandInput.disabled} />
 
-        dialog = () =>
-            <Dialog
-                open={this.state.open}
-                onClose={this.closeDialog}
-                fullWidth
-                maxWidth='md' >
-                <DialogTitle>Choose collections</DialogTitle>
-                <DialogContent>
-                    <this.dialogContent />
-                </DialogContent>
-                <DialogActions>
-                    <Button onClick={this.closeDialog}>Cancel</Button>
-                    <Button
-                        data-cy='ok-button'
-                        variant='contained'
-                        color='primary'
-                        onClick={this.submit}>Ok</Button>
-                </DialogActions>
-            </Dialog>
-
         dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
             root: {
                 display: 'flex',
                 flexDirection: 'column',
-                height: `${spacing.unit * 8}vh`,
+            },
+            pickerWrapper: {
+                display: 'flex',
+                flexDirection: 'column',
+                flexBasis: `${spacing.unit * 8}vh`,
+                flexShrink: 1,
+                minHeight: 0,
             },
             tree: {
                 flex: 3,
@@ -270,11 +256,33 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
                 padding: `${spacing.unit}px 0`,
                 overflowX: 'hidden',
             },
-        })
+        });
+
+        dialog = withStyles(this.dialogContentStyles)(
+            ({ classes }: WithStyles<DialogContentCssRules>) =>
+                <Dialog
+                    open={this.state.open}
+                    onClose={this.closeDialog}
+                    fullWidth
+                    maxWidth='md' >
+                    <DialogTitle>Choose collections</DialogTitle>
+                    <DialogContent className={classes.root}>
+                        <this.dialogContent />
+                    </DialogContent>
+                    <DialogActions>
+                        <Button onClick={this.closeDialog}>Cancel</Button>
+                        <Button
+                            data-cy='ok-button'
+                            variant='contained'
+                            color='primary'
+                            onClick={this.submit}>Ok</Button>
+                    </DialogActions>
+                </Dialog>
+        );
 
         dialogContent = withStyles(this.dialogContentStyles)(
             ({ classes }: WithStyles<DialogContentCssRules>) =>
-                <div className={classes.root}>
+                <div className={classes.pickerWrapper}>
                     <div className={classes.tree}>
                         <ProjectsTreePicker
                             pickerId={this.props.commandInput.id}
@@ -298,4 +306,4 @@ const DirectoryArrayInputComponent = connect(mapStateToProps)(
 
     });
 
-type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
+type DialogContentCssRules = 'root' | 'pickerWrapper' | 'tree' | 'divider' | 'chips';
index ab1cf9d1af51146841143487bb3b9ac904439988..5348cc2b76ca93a4e24fc1d9474702bc327867e2 100644 (file)
@@ -6,7 +6,7 @@ import React from 'react';
 import { connect, DispatchProp } from 'react-redux';
 import { memoize } from 'lodash/fp';
 import { Field } from 'redux-form';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
 import {
     isRequiredInput,
     DirectoryCommandInputParameter,
@@ -17,7 +17,7 @@ import { GenericInputProps, GenericInput } from './generic-input';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
 import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
 import { TreeItem } from 'components/tree/tree';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { CollectionResource } from 'models/collection';
 import { ResourceKind } from 'models/resource';
 import { ERROR_MESSAGE } from 'validators/require';
@@ -26,6 +26,9 @@ export interface DirectoryInputProps {
     input: DirectoryCommandInputParameter;
     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
 }
+
+type DialogContentCssRules = 'root' | 'pickerWrapper';
+
 export const DirectoryInput = ({ input, options }: DirectoryInputProps) =>
     <Field
         name={input.id}
@@ -75,7 +78,7 @@ const DirectoryInputComponent = connect()(
         render() {
             return <>
                 {this.renderInput()}
-                {this.renderDialog()}
+                <this.dialog />
             </>;
         }
 
@@ -114,32 +117,45 @@ const DirectoryInputComponent = connect()(
                 {...this.props} />;
         }
 
-        renderDialog() {
-            return <Dialog
-                open={this.state.open}
-                onClose={this.closeDialog}
-                fullWidth
-                data-cy="choose-a-directory-dialog"
-                maxWidth='md'>
-                <DialogTitle>Choose a directory</DialogTitle>
-                <DialogContent>
-                    <ProjectsTreePicker
-                        pickerId={this.props.commandInput.id}
-                        includeCollections
-                        options={this.props.options}
-                        toggleItemActive={this.setDirectory} />
-                </DialogContent>
-                <DialogActions>
-                    <Button onClick={this.closeDialog}>Cancel</Button>
-                    <Button
-                        disabled={!this.state.directory}
-                        variant='contained'
-                        color='primary'
-                        onClick={this.submit}>Ok</Button>
-                </DialogActions>
-            </Dialog>;
-        }
+        dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+            root: {
+                display: 'flex',
+                flexDirection: 'column',
+            },
+            pickerWrapper: {
+                flexBasis: `${spacing.unit * 8}vh`,
+                flexShrink: 1,
+                minHeight: 0,
+            },
+        });
+
+        dialog = withStyles(this.dialogContentStyles)(
+            ({ classes }: WithStyles<DialogContentCssRules>) =>
+                <Dialog
+                    open={this.state.open}
+                    onClose={this.closeDialog}
+                    fullWidth
+                    data-cy="choose-a-directory-dialog"
+                    maxWidth='md'>
+                    <DialogTitle>Choose a directory</DialogTitle>
+                    <DialogContent className={classes.root}>
+                        <div className={classes.pickerWrapper}>
+                            <ProjectsTreePicker
+                                pickerId={this.props.commandInput.id}
+                                includeCollections
+                                options={this.props.options}
+                                toggleItemActive={this.setDirectory} />
+                        </div>
+                    </DialogContent>
+                    <DialogActions>
+                        <Button onClick={this.closeDialog}>Cancel</Button>
+                        <Button
+                            disabled={!this.state.directory}
+                            variant='contained'
+                            color='primary'
+                            onClick={this.submit}>Ok</Button>
+                    </DialogActions>
+                </Dialog>
+        );
 
     });
-
-
index ddb558b9e552afe99bb47a4663e7951ae5eb2515..0be8f19feda9e40fe2ab7aebffb497250ca3c688 100644 (file)
@@ -16,7 +16,7 @@ import { GenericInputProps, GenericInput } from './generic-input';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
 import { connect, DispatchProp } from 'react-redux';
 import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds } from 'store/tree-picker/tree-picker-actions';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { CollectionFile, CollectionFileType } from 'models/collection-file';
 import { createSelector, createStructuredSelector } from 'reselect';
 import { ChipsInput } from 'components/chips-input/chips-input';
@@ -212,31 +212,17 @@ const FileArrayInputComponent = connect(mapStateToProps)(
                 onKeyPress={!this.props.commandInput.disabled ? this.openDialog : undefined}
                 onBlur={this.props.input.onBlur} />
 
-        dialog = () =>
-            <Dialog
-                open={this.state.open}
-                onClose={this.closeDialog}
-                fullWidth
-                maxWidth='md' >
-                <DialogTitle>Choose files</DialogTitle>
-                <DialogContent>
-                    <this.dialogContent />
-                </DialogContent>
-                <DialogActions>
-                    <Button onClick={this.closeDialog}>Cancel</Button>
-                    <Button
-                        data-cy='ok-button'
-                        variant='contained'
-                        color='primary'
-                        onClick={this.submit}>Ok</Button>
-                </DialogActions>
-            </Dialog>
-
         dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
             root: {
                 display: 'flex',
                 flexDirection: 'column',
-                height: `${spacing.unit * 8}vh`,
+            },
+            pickerWrapper: {
+                display: 'flex',
+                flexDirection: 'column',
+                flexBasis: `${spacing.unit * 8}vh`,
+                flexShrink: 1,
+                minHeight: 0,
             },
             tree: {
                 flex: 3,
@@ -253,9 +239,32 @@ const FileArrayInputComponent = connect(mapStateToProps)(
             },
         })
 
+
+        dialog = withStyles(this.dialogContentStyles)(
+            ({ classes }: WithStyles<DialogContentCssRules>) =>
+                <Dialog
+                    open={this.state.open}
+                    onClose={this.closeDialog}
+                    fullWidth
+                    maxWidth='md' >
+                    <DialogTitle>Choose files</DialogTitle>
+                    <DialogContent className={classes.root}>
+                        <this.dialogContent />
+                    </DialogContent>
+                    <DialogActions>
+                        <Button onClick={this.closeDialog}>Cancel</Button>
+                        <Button
+                            data-cy='ok-button'
+                            variant='contained'
+                            color='primary'
+                            onClick={this.submit}>Ok</Button>
+                    </DialogActions>
+                </Dialog>
+        );
+
         dialogContent = withStyles(this.dialogContentStyles)(
             ({ classes }: WithStyles<DialogContentCssRules>) =>
-                <div className={classes.root}>
+                <div className={classes.pickerWrapper}>
                     <div className={classes.tree}>
                         <ProjectsTreePicker
                             pickerId={this.props.commandInput.id}
@@ -280,4 +289,4 @@ const FileArrayInputComponent = connect(mapStateToProps)(
 
     });
 
-type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
+type DialogContentCssRules = 'root' | 'pickerWrapper' | 'tree' | 'divider' | 'chips';
index a56d45f854f054c2882d1f8b41e8ecff5267ffa4..218bf4189cbabebe6330089bb47f6fa23c8e8f3a 100644 (file)
@@ -12,19 +12,22 @@ import {
 } from 'models/workflow';
 import { Field } from 'redux-form';
 import { ERROR_MESSAGE } from 'validators/require';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
 import { GenericInputProps, GenericInput } from './generic-input';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
 import { connect, DispatchProp } from 'react-redux';
 import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
 import { TreeItem } from 'components/tree/tree';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { CollectionFile, CollectionFileType } from 'models/collection-file';
 
 export interface FileInputProps {
     input: FileCommandInputParameter;
     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
 }
+
+type DialogContentCssRules = 'root' | 'pickerWrapper';
+
 export const FileInput = ({ input, options }: FileInputProps) =>
     <Field
         name={input.id}
@@ -73,7 +76,7 @@ const FileInputComponent = connect()(
         render() {
             return <>
                 {this.renderInput()}
-                {this.renderDialog()}
+                <this.dialog />
             </>;
         }
 
@@ -113,33 +116,52 @@ const FileInputComponent = connect()(
                 {...this.props} />;
         }
 
-        renderDialog() {
-            return <Dialog
-                open={this.state.open}
-                onClose={this.closeDialog}
-                fullWidth
-                data-cy="choose-a-file-dialog"
-                maxWidth='md'>
-                <DialogTitle>Choose a file</DialogTitle>
-                <DialogContent>
+        dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+            root: {
+                display: 'flex',
+                flexDirection: 'column',
+            },
+            pickerWrapper: {
+                flexBasis: `${spacing.unit * 8}vh`,
+                flexShrink: 1,
+                minHeight: 0,
+            },
+        });
+
+        dialog = withStyles(this.dialogContentStyles)(
+            ({ classes }: WithStyles<DialogContentCssRules>) =>
+                <Dialog
+                    open={this.state.open}
+                    onClose={this.closeDialog}
+                    fullWidth
+                    data-cy="choose-a-file-dialog"
+                    maxWidth='md'>
+                    <DialogTitle>Choose a file</DialogTitle>
+                    <DialogContent className={classes.root}>
+                        <this.dialogContent />
+                    </DialogContent>
+                    <DialogActions>
+                        <Button onClick={this.closeDialog}>Cancel</Button>
+                        <Button
+                            disabled={!this.state.file}
+                            variant='contained'
+                            color='primary'
+                            onClick={this.submit}>Ok</Button>
+                    </DialogActions>
+                </Dialog >
+        );
+
+        dialogContent = withStyles(this.dialogContentStyles)(
+            ({ classes }: WithStyles<DialogContentCssRules>) =>
+                <div className={classes.pickerWrapper}>
                     <ProjectsTreePicker
                         pickerId={this.props.commandInput.id}
                         includeCollections
                         includeFiles
                         options={this.props.options}
                         toggleItemActive={this.setFile} />
-                </DialogContent>
-                <DialogActions>
-                    <Button onClick={this.closeDialog}>Cancel</Button>
-                    <Button
-                        disabled={!this.state.file}
-                        variant='contained'
-                        color='primary'
-                        onClick={this.submit}>Ok</Button>
-                </DialogActions>
-            </Dialog>;
-        }
-
-    });
+                </div>
+        );
 
 
+    });
index 0c962ed8a326116f0c1134540e8d3f526d1c82a4..ef6d08f40086dbeb4aa2d0596cdb412885c841f9 100644 (file)
@@ -5,7 +5,7 @@
 import React from 'react';
 import { connect, DispatchProp } from 'react-redux';
 import { Field } from 'redux-form';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, withStyles, WithStyles, StyleRulesCallback } from '@material-ui/core';
 import {
     GenericCommandInputParameter
 } from 'models/workflow';
@@ -13,7 +13,7 @@ import { GenericInput, GenericInputProps } from './generic-input';
 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
 import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
 import { TreeItem } from 'components/tree/tree';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
 import { ProjectResource } from 'models/project';
 import { ResourceKind } from 'models/resource';
 import { RootState } from 'store/store';
@@ -27,6 +27,9 @@ export interface ProjectInputProps {
     input: ProjectCommandInputParameter;
     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
 }
+
+type DialogContentCssRules = 'root' | 'pickerWrapper';
+
 export const ProjectInput = ({ input, options }: ProjectInputProps) =>
     <Field
         name={input.id}
@@ -67,7 +70,7 @@ export const ProjectInputComponent = connect(mapStateToProps)(
         render() {
             return <>
                 {this.renderInput()}
-                {this.renderDialog()}
+                <this.dialog />
             </>;
         }
 
@@ -109,29 +112,44 @@ export const ProjectInputComponent = connect(mapStateToProps)(
                 {...this.props} />;
         }
 
-        renderDialog() {
-            return this.state.open ? <Dialog
-                open={this.state.open}
-                onClose={this.closeDialog}
-                fullWidth
-                data-cy="choose-a-project-dialog"
-                maxWidth='md'>
-                <DialogTitle>Choose a project</DialogTitle>
-                <DialogContent>
-                    <ProjectsTreePicker
-                        pickerId={this.props.commandInput.id}
-                        options={this.props.options}
-                        toggleItemActive={this.setProject} />
-                </DialogContent>
-                <DialogActions>
-                    <Button onClick={this.closeDialog}>Cancel</Button>
-                    <Button
-                        disabled={this.invalid()}
-                        variant='contained'
-                        color='primary'
-                        onClick={this.submit}>Ok</Button>
-                </DialogActions>
-            </Dialog> : null;
-        }
+        dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+            root: {
+                display: 'flex',
+                flexDirection: 'column',
+            },
+            pickerWrapper: {
+                flexBasis: `${spacing.unit * 8}vh`,
+                flexShrink: 1,
+                minHeight: 0,
+            },
+        });
+
+        dialog = withStyles(this.dialogContentStyles)(
+            ({ classes }: WithStyles<DialogContentCssRules>) =>
+                this.state.open ? <Dialog
+                    open={this.state.open}
+                    onClose={this.closeDialog}
+                    fullWidth
+                    data-cy="choose-a-project-dialog"
+                    maxWidth='md'>
+                    <DialogTitle>Choose a project</DialogTitle>
+                    <DialogContent className={classes.root}>
+                        <div className={classes.pickerWrapper}>
+                            <ProjectsTreePicker
+                                pickerId={this.props.commandInput.id}
+                                options={this.props.options}
+                                toggleItemActive={this.setProject} />
+                        </div>
+                    </DialogContent>
+                    <DialogActions>
+                        <Button onClick={this.closeDialog}>Cancel</Button>
+                        <Button
+                            disabled={this.invalid()}
+                            variant='contained'
+                            color='primary'
+                            onClick={this.submit}>Ok</Button>
+                    </DialogActions>
+                </Dialog> : null
+        );
 
     });