Merge branch '19783-picking-tweak' refs #19783
authorPeter Amstutz <peter.amstutz@curii.com>
Thu, 15 Dec 2022 22:01:28 +0000 (17:01 -0500)
committerPeter Amstutz <peter.amstutz@curii.com>
Thu, 15 Dec 2022 22:01:28 +0000 (17:01 -0500)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

33 files changed:
.gitignore
cypress/integration/collection.spec.js
cypress/integration/project.spec.js
src/common/config.ts
src/common/custom-theme.ts
src/common/formatters.ts
src/components/column-selector/column-selector.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.tsx
src/models/container-request.ts
src/models/log.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/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/workbench/workbench-actions.ts
src/validators/validators.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/resource-properties-form/property-value-field.tsx
src/views-components/resource-properties-form/resource-properties-form.tsx
src/views/inactive-panel/inactive-panel.tsx
src/views/process-panel/process-io-card.tsx
src/views/project-panel/project-panel.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 4a8751efb3a9d86689cdfbf7034ecc983d4cb333..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)}`,
@@ -912,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 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 fc89a4ae216b2790385570f293b74c9ebd3145ba..df8486429cae280f6fc81d2c40404531c5ebb5a2 100644 (file)
@@ -12,177 +12,177 @@ import red from '@material-ui/core/colors/red';
 import teal from '@material-ui/core/colors/teal';
 
 export interface ArvadosThemeOptions extends ThemeOptions {
-    customs: any;
+  customs: any;
 }
 
 export interface ArvadosTheme extends Theme {
-    customs: {
-        colors: Colors
-    };
+  customs: {
+    colors: Colors;
+  };
 }
 
 interface Colors {
-    green700: string;
-    yellow100: string;
-    yellow700: string;
-    yellow900: string;
-    red100: string;
-    red900: string;
-    blue500: string;
-    grey500: string;
-    purple: string;
-    orange: string;
+  green700: string;
+  yellow100: string;
+  yellow700: string;
+  yellow900: string;
+  red100: string;
+  red900: string;
+  blue500: string;
+  grey500: string;
+  purple: string;
+  orange: string;
 }
 
 const arvadosPurple = '#361336';
-const grey500 = grey["500"];
-const grey600 = grey["600"];
-const grey700 = grey["700"];
-const grey900 = grey["900"];
+const grey500 = grey['500'];
+const grey600 = grey['600'];
+const grey700 = grey['700'];
+const grey900 = grey['900'];
 
 export const themeOptions: ArvadosThemeOptions = {
-    typography: {
-        useNextVariants: true,
-    },
-    customs: {
-        colors: {
-            green700: green["700"],
-            yellow100: yellow["100"],
-            yellow700: yellow["700"],
-            yellow900: yellow["900"],
-            red100: red["100"],
-            red900: red['900'],
-            blue500: blue['500'],
-            grey500: grey500,
-            purple: arvadosPurple,
-            orange: '#f0ad4e',
-        }
-    },
-    overrides: {
-        MuiTypography: {
-            body1: {
-                fontSize: '0.8125rem'
-            }
-        },
-        MuiAppBar: {
-            colorPrimary: {
-                backgroundColor: arvadosPurple
-            }
-        },
-        MuiTabs: {
-            root: {
-                color: grey600
-            },
-            indicator: {
-                backgroundColor: arvadosPurple
-            }
-        },
-        MuiTab: {
-            root: {
-                '&$selected': {
-                    fontWeight: 700,
-                    color: arvadosPurple
-                }
-            }
-        },
-        MuiList: {
-            root: {
-                color: grey900
-            }
-        },
-        MuiListItemText: {
-            root: {
-                padding: 0
-            }
-        },
-        MuiListItemIcon: {
-            root: {
-                fontSize: '1.25rem'
-            }
-        },
-        MuiCardHeader: {
-            avatar: {
-                display: 'flex',
-                alignItems: 'center'
-            },
-            title: {
-                color: grey700,
-                fontSize: '1.25rem'
-            }
+  typography: {
+    useNextVariants: true,
+  },
+  customs: {
+    colors: {
+      green700: green['700'],
+      yellow100: yellow['100'],
+      yellow700: yellow['700'],
+      yellow900: yellow['900'],
+      red100: red['100'],
+      red900: red['900'],
+      blue500: blue['500'],
+      grey500: grey500,
+      purple: arvadosPurple,
+      orange: '#f0ad4e',
+    },
+  },
+  overrides: {
+    MuiTypography: {
+      body1: {
+        fontSize: '0.8125rem',
+      },
+    },
+    MuiAppBar: {
+      colorPrimary: {
+        backgroundColor: arvadosPurple,
+      },
+    },
+    MuiTabs: {
+      root: {
+        color: grey600,
+      },
+      indicator: {
+        backgroundColor: arvadosPurple,
+      },
+    },
+    MuiTab: {
+      root: {
+        '&$selected': {
+          fontWeight: 700,
+          color: arvadosPurple,
         },
-        MuiExpansionPanel: {
-            expanded: {
-                marginTop: '8px',
-            }
+      },
+    },
+    MuiList: {
+      root: {
+        color: grey900,
+      },
+    },
+    MuiListItemText: {
+      root: {
+        padding: 0,
+      },
+    },
+    MuiListItemIcon: {
+      root: {
+        fontSize: '1.25rem',
+      },
+    },
+    MuiCardHeader: {
+      avatar: {
+        display: 'flex',
+        alignItems: 'center',
+      },
+      title: {
+        color: grey700,
+        fontSize: '1.25rem',
+      },
+    },
+    MuiExpansionPanel: {
+      expanded: {
+        marginTop: '8px',
+      },
+    },
+    MuiExpansionPanelDetails: {
+      root: {
+        marginBottom: 0,
+        paddingBottom: '4px',
+      },
+    },
+    MuiExpansionPanelSummary: {
+      content: {
+        '&$expanded': {
+          margin: 0,
         },
-        MuiExpansionPanelDetails: {
-            root: {
-                marginBottom: 0,
-                paddingBottom: '4px',
-            }
+        color: grey700,
+        fontSize: '1.25rem',
+        margin: 0,
+      },
+      expanded: {},
+    },
+    MuiMenuItem: {
+      root: {
+        padding: '8px 16px',
+      },
+    },
+    MuiInput: {
+      root: {
+        fontSize: '0.875rem',
+      },
+      underline: {
+        '&:after': {
+          borderBottomColor: arvadosPurple,
         },
-        MuiExpansionPanelSummary: {
-            content: {
-                '&$expanded': {
-                    margin: 0,
-                },
-                color: grey700,
-                fontSize: '1.25rem',
-                margin: 0,
-            },
-            expanded: {},
+        '&:hover:not($disabled):not($focused):not($error):before': {
+          borderBottom: '1px solid inherit',
         },
-        MuiMenuItem: {
-            root: {
-                padding: '8px 16px'
-            }
+      },
+    },
+    MuiFormLabel: {
+      root: {
+        fontSize: '0.875rem',
+        '&$focused': {
+          '&$focused:not($error)': {
+            color: arvadosPurple,
+          },
         },
-        MuiInput: {
-            root: {
-                fontSize: '0.875rem'
-            },
-            underline: {
-                '&:after': {
-                    borderBottomColor: arvadosPurple
-                },
-                '&:hover:not($disabled):not($focused):not($error):before': {
-                    borderBottom: '1px solid inherit'
-                }
-            }
+      },
+    },
+    MuiStepIcon: {
+      root: {
+        '&$active': {
+          color: arvadosPurple,
         },
-        MuiFormLabel: {
-            root: {
-                fontSize: '0.875rem',
-                "&$focused": {
-                    "&$focused:not($error)": {
-                        color: arvadosPurple
-                    }
-                }
-            }
+        '&$completed': {
+          color: 'inherited',
         },
-        MuiStepIcon: {
-            root: {
-                '&$active': {
-                    color: arvadosPurple
-                },
-                '&$completed': {
-                    color: 'inherited'
-                },
-            }
-        }
-    },
-    mixins: {
-        toolbar: {
-            minHeight: '48px'
-        }
-    },
-    palette: {
-        primary: {
-            main: teal.A700,
-            dark: teal.A400,
-            contrastText: '#fff'
-        }
+      },
+    },
+  },
+  mixins: {
+    toolbar: {
+      minHeight: '48px',
+    },
+  },
+  palette: {
+    primary: {
+      main: teal.A700,
+      dark: teal.A400,
+      contrastText: '#fff',
     },
+  },
 };
 
-export const CustomTheme = createMuiTheme(themeOptions);
\ No newline at end of file
+export const CustomTheme = createMuiTheme(themeOptions);
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 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 d942234d0fcb4841d609850de408ed3c3cdfe3e1..e08039082fd1ba0df0d686264dd792678ddb3f3a 100644 (file)
@@ -167,11 +167,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 dc6bd84fb01690e857e740f3e0775c02fc0ed6b9..2a0e60ba9a4ae86c9d628359801f04c2dd1f71f1 100644 (file)
@@ -2,41 +2,43 @@
 //
 // 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;
 }
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 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 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 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 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 47e5b287ae7acb3fce265916da02964286b68748..28728daff3e486628cad956340d83c1255673ab7 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 }) => {
@@ -939,6 +1043,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 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 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 ceba293e34b4edef1815dc95b1977009f617132f..8b85617d32743d1654b7aff9cd794d189a43389b 100644 (file)
@@ -240,9 +240,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);
 
@@ -253,6 +257,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}
@@ -316,25 +324,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";