.idea
.vscode
+/public/config.json
# see https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
.pnp.*
});
});
+ 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)}`,
cy.get('[data-cy=form-dialog]')
.should('contain', 'Move to')
.within(() => {
+ // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
cy.get('[data-cy=projects-tree-home-tree-picker]')
.find('i')
- .click();
+ .then(el => el.click());
cy.get('[data-cy=projects-tree-home-tree-picker]')
.contains(projName)
.click();
// 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');
});
cy.get('[data-cy=choose-a-file-dialog]').as('chooseFileDialog');
- cy.get('@chooseFileDialog').contains('Projects').closest('ul').find('i').click();
+ cy.get('@chooseFileDialog').contains('Home Projects').closest('ul').find('i').click();
cy.get('@project1').then((project1) => {
cy.get('@chooseFileDialog').find(`[data-id=${project1.uuid}]`).find('i').click();
cy.get('label').contains('foo').parent('div').find('input').click();
cy.get('div[role=dialog]')
.within(() => {
- cy.get('p').contains('Projects').closest('div[role=button]')
- .within(() => {
- cy.get('svg[role=presentation]')
- .click({ multiple: true });
- });
+ // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+ cy.get('p').contains('Home Projects').closest('ul')
+ .find('i')
+ .then(el => el.click());
cy.get(`[data-id=${testCollection.uuid}]`)
.find('i').click();
cy.get('label').contains('bar').parent('div').find('input').click();
cy.get('div[role=dialog]')
.within(() => {
- cy.get('p').contains('Projects').closest('div[role=button]')
- .within(() => {
- cy.get('svg[role=presentation]')
- .click({ multiple: true });
- });
+ // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+ cy.get('p').contains('Home Projects').closest('ul')
+ .find('i')
+ .then(el => el.click());
cy.get(`[data-id=${testCollection.uuid}]`)
.find('input[type=checkbox]').click();
});
cy.get('[data-cy=form-dialog]').within(function () {
- cy.get('[data-cy=projects-tree-favourites-tree-picker]').find('i').click();
+ // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+ cy.get('[data-cy=projects-tree-favourites-tree-picker]')
+ .find('i')
+ .then(el => el.click());
cy.contains(myProject1.name);
cy.contains(mySharedWritableProject.name);
cy.get('[data-cy=projects-tree-favourites-tree-picker]')
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)}`,
});
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',
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');
});
});
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');
});
});
//
// 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: {
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()}`;
//
// 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}`;
+ }
};
// SPDX-License-Identifier: AGPL-3.0
import React from "react";
-import { configure, shallow } from "enzyme";
+import { configure, mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { Breadcrumbs } from "./breadcrumbs";
-import { Button } from "@material-ui/core";
+import { Button, MuiThemeProvider } from "@material-ui/core";
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import { CustomTheme } from 'common/custom-theme';
+import { Provider } from "react-redux";
+import { combineReducers, createStore } from "redux";
configure({ adapter: new Adapter() });
let onClick: () => void;
let resources = {};
-
+ let store;
beforeEach(() => {
onClick = jest.fn();
+ const initialAuthState = {
+ config: {
+ clusterConfig: {
+ Collections: {
+ ForwardSlashNameSubstitution: "/"
+ }
+ }
+ }
+ }
+ store = createStore(combineReducers({
+ auth: (state: any = initialAuthState, action: any) => state,
+ }));
});
it("renders one item", () => {
const items = [
- { label: 'breadcrumb 1' }
+ { label: 'breadcrumb 1', uuid: '1' }
];
- const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+ const breadcrumbs = mount(
+ <Provider store={store}>
+ <MuiThemeProvider theme={CustomTheme}>
+ <Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />
+ </MuiThemeProvider>
+ </Provider>);
expect(breadcrumbs.find(Button)).toHaveLength(1);
expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(0);
});
it("renders multiple items", () => {
const items = [
- { label: 'breadcrumb 1' },
- { label: 'breadcrumb 2' }
+ { label: 'breadcrumb 1', uuid: '1' },
+ { label: 'breadcrumb 2', uuid: '2' }
];
- const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+ const breadcrumbs = mount(
+ <Provider store={store}>
+ <MuiThemeProvider theme={CustomTheme}>
+ <Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />
+ </MuiThemeProvider>
+ </Provider>);
expect(breadcrumbs.find(Button)).toHaveLength(2);
expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(1);
});
it("calls onClick with clicked item", () => {
const items = [
- { label: 'breadcrumb 1' },
- { label: 'breadcrumb 2' }
+ { label: 'breadcrumb 1', uuid: '1' },
+ { label: 'breadcrumb 2', uuid: '2' }
];
- const breadcrumbs = shallow(<Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />).dive();
+ const breadcrumbs = mount(
+ <Provider store={store}>
+ <MuiThemeProvider theme={CustomTheme}>
+ <Breadcrumbs items={items} resources={resources} onClick={onClick} onContextMenu={jest.fn()} />
+ </MuiThemeProvider>
+ </Provider>);
breadcrumbs.find(Button).at(1).simulate('click');
expect(onClick).toBeCalledWith(items[1]);
});
import { IllegalNamingWarning } from '../warning/warning';
import { IconType, FreezeIcon } from 'components/icon/icon';
import grey from '@material-ui/core/colors/grey';
-import { ResourceBreadcrumb } from 'store/breadcrumbs/breadcrumbs-actions';
import { ResourcesState } from 'store/resources/resources';
+import classNames from 'classnames';
+import { ArvadosTheme } from 'common/custom-theme';
export interface Breadcrumb {
label: string;
icon?: IconType;
+ uuid: string;
}
-type CssRules = "item" | "currentItem" | "label" | "icon" | "frozenIcon";
+type CssRules = "item" | "chevron" | "label" | "buttonLabel" | "icon" | "frozenIcon";
-const styles: StyleRulesCallback<CssRules> = theme => ({
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
item: {
- opacity: 0.6
+ borderRadius: '16px',
+ height: '32px',
+ minWidth: '36px',
+ color: theme.customs.colors.grey700,
+ '&.parentItem': {
+ color: `${theme.palette.primary.main}`,
+ },
},
- currentItem: {
- opacity: 1
+ chevron: {
+ color: grey["600"],
},
label: {
- textTransform: "none"
+ textTransform: "none",
+ paddingRight: '3px',
+ paddingLeft: '3px',
+ lineHeight: '1.4',
+ },
+ buttonLabel: {
+ overflow: 'hidden',
+ justifyContent: 'flex-start',
},
icon: {
fontSize: 20,
color: grey["600"],
- marginRight: '10px',
+ marginRight: '5px',
},
frozenIcon: {
fontSize: 20,
color: grey["600"],
- marginLeft: '10px',
+ marginLeft: '3px',
},
});
export interface BreadcrumbsProps {
- items: ResourceBreadcrumb[];
+ items: Breadcrumb[];
resources: ResourcesState;
- onClick: (breadcrumb: ResourceBreadcrumb) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: ResourceBreadcrumb) => void;
+ onClick: (breadcrumb: Breadcrumb) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
}
export const Breadcrumbs = withStyles(styles)(
: isLastItem
? 'breadcrumb-last'
: false}
+ className={classNames(
+ isLastItem ? null : 'parentItem',
+ classes.item
+ )}
+ classes={{
+ label: classes.buttonLabel
+ }}
color="inherit"
- className={isLastItem ? classes.currentItem : classes.item}
onClick={() => onClick(item)}
onContextMenu={event => onContextMenu(event, item)}>
<Icon className={classes.icon} />
}
</Button>
</Tooltip>
- {!isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />}
+ {!isLastItem && <ChevronRightIcon color="inherit" className={classNames('parentItem', classes.chevron)} />}
</React.Fragment>
);
})
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'
}
});
<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>
<MenuIcon aria-label="Select columns" />
</IconButton>
</Tooltip>;
+
+
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>
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>;
}
width: "100%",
height: "200px",
position: "relative",
- border: "1px solid rgba(0, 0, 0, 0.42)"
+ border: "1px solid rgba(0, 0, 0, 0.42)",
+ boxSizing: 'border-box',
},
dropzoneBorder: {
content: "",
import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
import { WithDialogProps } from 'store/dialog/with-dialog';
-type CssRules = "button" | "lastButton" | "formContainer" | "dialogTitle" | "progressIndicator" | "dialogActions";
+type CssRules = "button" | "lastButton" | "form" | "formContainer" | "dialogTitle" | "progressIndicator" | "dialogActions";
const styles: StyleRulesCallback<CssRules> = theme => ({
button: {
marginLeft: theme.spacing.unit,
marginRight: "0",
},
+ form: {
+ display: 'flex',
+ overflowY: 'auto',
+ flexDirection: 'column',
+ flex: '0 1 auto',
+ },
formContainer: {
display: "flex",
flexDirection: "column",
disableEscapeKeyDown={props.submitting}
fullWidth
maxWidth='md'>
- <form data-cy='form-dialog'>
+ <form data-cy='form-dialog' className={props.classes.form}>
<DialogTitle className={props.classes.dialogTitle}>
{props.dialogTitle}
</DialogTitle>
import Add from '@material-ui/icons/Add';
import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
-import BubbleChart from '@material-ui/icons/BubbleChart';
import Build from '@material-ui/icons/Build';
import Cached from '@material-ui/icons/Cached';
import DescriptionIcon from '@material-ui/icons/Description';
import Search from '@material-ui/icons/Search';
import SettingsApplications from '@material-ui/icons/SettingsApplications';
import SettingsEthernet from '@material-ui/icons/SettingsEthernet';
+import Settings from '@material-ui/icons/Settings';
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
import Warning from '@material-ui/icons/Warning';
);
export const FreezeIcon = (props: any) =>
- <span {...props}>
- <span className='fas fa-snowflake' />
- </span>
+ <SvgIcon {...props}>
+ <path d="M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z" />
+ </SvgIcon>
export const UnfreezeIcon = (props: any) =>
- <div {...props}>
- <div className="fa-layers fa-1x fa-fw">
- <span className="fas fa-slash"
- data-fa-mask="fas fa-snowflake" data-fa-transform="down-1.5" />
- <span className="fas fa-slash" />
- </div>
- </div>;
+ <SvgIcon {...props}>
+ <path d="M11 5.12L9.29 3.41L10.71 2L12 3.29L13.29 2L14.71 3.41L13 5.12V7.38L15.45 8.82L17.45 7.69L18.07 5.36L20 5.88L19.54 7.65L21.31 8.12L20.79 10.05L18.46 9.43L16.46 10.56V13.26L14.5 11.3V10.56L12.74 9.54L10.73 7.53L11 7.38V5.12M18.46 14.57L16.87 13.67L19.55 16.35L21.3 15.88L20.79 13.95L18.46 14.57M13 16.62V18.88L14.7 20.59L13.29 22L12 20.71L10.71 22L9.29 20.59L11 18.88V16.62L8.55 15.18L6.55 16.31L5.93 18.64L4 18.12L4.47 16.36L2.7 15.89L3.22 13.96L5.55 14.58L7.55 13.45V10.56L5.55 9.43L3.22 10.05L2.7 8.12L4.47 7.65L4 5.89L1.11 3L2.39 1.73L22.11 21.46L20.84 22.73L14.1 16L13 16.62M12 14.89L12.63 14.5L9.5 11.39V13.44L12 14.89Z" />
+ </SvgIcon>
export const PendingIcon = (props: any) =>
<span {...props}>
export const PaginationDownIcon: IconType = (props) => <ArrowDropDown {...props} />;
export const PaginationLeftArrowIcon: IconType = (props) => <ChevronLeft {...props} />;
export const PaginationRightArrowIcon: IconType = (props) => <ChevronRight {...props} />;
-export const ProcessIcon: IconType = (props) => <BubbleChart {...props} />;
+export const ProcessIcon: IconType = (props) => <Settings {...props} />;
export const ProjectIcon: IconType = (props) => <Folder {...props} />;
export const FilterGroupIcon: IconType = (props) => <Pageview {...props} />;
export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
import { ReactNodeArray } from 'prop-types';
import classNames from 'classnames';
-type CssRules = 'button' | 'buttonIcon' | 'content';
+type CssRules = 'root' | 'button' | 'buttonIcon' | 'content';
const styles: StyleRulesCallback<CssRules> = theme => ({
+ root: {
+ marginTop: '10px',
+ },
button: {
padding: '2px 5px',
marginRight: '5px',
};
};
- return <Grid container {...props}>
+ return <Grid container {...props} className={classes.root}>
<Grid container item direction="row">
{ buttons.map((tgl, idx) => <Grid item key={idx}>{tgl}</Grid>) }
</Grid>
</Grid>;
};
-export const MPVContainer = withStyles(styles)(MPVContainerComponent);
\ No newline at end of file
+export const MPVContainer = withStyles(styles)(MPVContainerComponent);
//
// SPDX-License-Identifier: AGPL-3.0
-import { Resource, ResourceKind, ResourceWithProperties } from "./resource";
-import { MountType } from "models/mount-types";
+import { Resource, ResourceKind, ResourceWithProperties } from './resource';
+import { MountType } from 'models/mount-types';
import { RuntimeConstraints } from './runtime-constraints';
import { SchedulingParameters } from './scheduling-parameters';
export enum ContainerRequestState {
- UNCOMMITTED = "Uncommitted",
- COMMITTED = "Committed",
- FINAL = "Final"
+ UNCOMMITTED = 'Uncommitted',
+ COMMITTED = 'Committed',
+ FINAL = 'Final',
}
-export interface ContainerRequestResource extends Resource, ResourceWithProperties {
- kind: ResourceKind.CONTAINER_REQUEST;
- name: string;
- description: string;
- state: ContainerRequestState;
- requestingContainerUuid: string | null;
- cumulativeCost: number;
- containerUuid: string | null;
- containerCountMax: number;
- mounts: {[path: string]: MountType};
- runtimeConstraints: RuntimeConstraints;
- schedulingParameters: SchedulingParameters;
- containerImage: string;
- environment: any;
- cwd: string;
- command: string[];
- outputPath: string;
- outputName: string;
- outputTtl: number;
- priority: number | null;
- expiresAt: string;
- useExisting: boolean;
- logUuid: string | null;
- outputUuid: string | null;
- filters: string;
- containerCount: number;
+export interface ContainerRequestResource
+ extends Resource,
+ ResourceWithProperties {
+ command: string[];
+ containerCountMax: number;
+ containerCount: number;
+ containerImage: string;
+ containerUuid: string | null;
+ cumulativeCost: number;
+ cwd: string;
+ description: string;
+ environment: any;
+ expiresAt: string;
+ filters: string;
+ kind: ResourceKind.CONTAINER_REQUEST;
+ logUuid: string | null;
+ mounts: { [path: string]: MountType };
+ name: string;
+ outputName: string;
+ outputPath: string;
+ outputTtl: number;
+ outputUuid: string | null;
+ priority: number | null;
+ requestingContainerUuid: string | null;
+ runtimeConstraints: RuntimeConstraints;
+ schedulingParameters: SchedulingParameters;
+ state: ContainerRequestState;
+ useExisting: boolean;
}
+
+// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+export const containerRequestFieldsNoMounts = [
+ "command",
+ "container_count_max",
+ "container_count",
+ "container_image",
+ "container_uuid",
+ "created_at",
+ "cwd",
+ "description",
+ "environment",
+ "etag",
+ "expires_at",
+ "filters",
+ "href",
+ "kind",
+ "log_uuid",
+ "modified_at",
+ "modified_by_client_uuid",
+ "modified_by_user_uuid",
+ "name",
+ "output_name",
+ "output_path",
+ "output_properties",
+ "output_storage_classes",
+ "output_ttl",
+ "output_uuid",
+ "owner_uuid",
+ "priority",
+ "properties",
+ "requesting_container_uuid",
+ "runtime_constraints",
+ "scheduling_parameters",
+ "state",
+ "use_existing",
+ "uuid",
+];
STDERR = 'stderr',
CONTAINER = 'container',
KEEPSTORE = 'keepstore',
+ SNIP = 'snip-line', // This type is for internal use only. See #19851
}
export interface LogResource extends Resource, ResourceWithProperties {
// SPDX-License-Identifier: AGPL-3.0
import { ResourceKind } from 'models/resource';
+import { GroupResource } from './group';
export type SearchBarAdvancedFormData = {
type?: ResourceKind;
cluster?: string;
projectUuid?: string;
+ projectObject?: GroupResource;
inTrash: boolean;
pastVersions: boolean;
dateFrom: string;
return this.addCondition("properties." + field, "exists", false, "", "", resourcePrefix);
}
- public addFullTextSearch(value: string) {
+ public addFullTextSearch(value: string, table?: string) {
const regex = /"[^"]*"/;
const matches: any[] = [];
match = value.match(regex);
}
+ let searchIn = 'any';
+ if (table) {
+ searchIn = table + ".any";
+ }
+
const terms = value.trim().split(/(\s+)/).concat(matches);
terms.forEach(term => {
if (term !== " ") {
- this.addCondition("any", "ilike", term, "%", "%");
+ this.addCondition(searchIn, "ilike", term, "%", "%");
}
});
return this;
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) {
]));
}
- create(data?: Partial<T>) {
+ create(data?: Partial<T>, showErrors?: boolean) {
let payload: any;
if (data !== undefined) {
this.readOnlyFields.forEach( field => delete data[field] );
[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] );
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):
}
}
- 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
);
}
}
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',
}
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 = {}) {
import { OrderBuilder, OrderDirection } from "services/api/order-builder";
import { ProcessResource } from "models/process";
import { SortDirection } from "components/data-table/data-column";
+import { containerRequestFieldsNoMounts } from "models/container-request";
export class AllProcessesPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
}
}
-// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
-export const containerRequestFieldsNoMounts = [
- "command",
- "container_count_max",
- "container_count",
- "container_image",
- "container_uuid",
- "created_at",
- "cwd",
- "description",
- "environment",
- "etag",
- "expires_at",
- "filters",
- "href",
- "kind",
- "log_uuid",
- "modified_at",
- "modified_by_client_uuid",
- "modified_by_user_uuid",
- "name",
- "output_name",
- "output_path",
- "output_properties",
- "output_storage_classes",
- "output_ttl",
- "output_uuid",
- "owner_uuid",
- "priority",
- "properties",
- "requesting_container_uuid",
- "runtime_constraints",
- "scheduling_parameters",
- "state",
- "use_existing",
- "uuid",
-];
-
const getParams = ( dataExplorer: DataExplorer ) => ({
...dataExplorerToListParams(dataExplorer),
order: getOrder(dataExplorer),
// If the token is from a LoginCluster federation, get user & token data
// from the token issuing cluster.
+ if (!config) {
+ return;
+ }
const lc = (config as Config).loginCluster
const tokenCluster = tokenParts.length === 3
? tokenParts[1].substring(0, 5)
const client = await svc.apiClientAuthorizationService.get('current');
dispatch(authActions.SET_EXTRA_TOKEN({
extraApiToken: extraToken,
- extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt): undefined,
+ extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt) : undefined,
}));
return extraToken;
} catch (e) {
const newExtraToken = getTokenV2(client);
dispatch(authActions.SET_EXTRA_TOKEN({
extraApiToken: newExtraToken,
- extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt): undefined,
+ extraApiTokenExpiration: client.expiresAt ? new Date(client.expiresAt) : undefined,
}));
return newExtraToken;
} catch {
import { Dispatch } from 'redux';
import { RootState } from 'store/store';
import { getUserUuid } from "common/getuser";
-import { Breadcrumb } from 'components/breadcrumbs/breadcrumbs';
import { getResource } from 'store/resources/resources';
import { TreePicker } from '../tree-picker/tree-picker';
import { getSidePanelTreeBranch, getSidePanelTreeNodeAncestorsIds } from '../side-panel-tree/side-panel-tree-actions';
import { GroupResource } from 'models/group';
import { extractUuidKind } from 'models/resource';
import { UserResource } from 'models/user';
+import { FilterBuilder } from 'services/api/filter-builder';
+import { ProcessResource } from 'models/process';
+import { OrderBuilder } from 'services/api/order-builder';
+import { Breadcrumb } from 'components/breadcrumbs/breadcrumbs';
+import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
+import { CollectionIcon, IconType, ProcessIcon, ProjectIcon } from 'components/icon/icon';
+import { CollectionResource } from 'models/collection';
+import { getSidePanelIcon } from 'views-components/side-panel-tree/side-panel-tree';
export const BREADCRUMBS = 'breadcrumbs';
-export interface ResourceBreadcrumb extends Breadcrumb {
- uuid: string;
-}
-
-export const setBreadcrumbs = (breadcrumbs: any, currentItem?: any) => {
+export const setBreadcrumbs = (breadcrumbs: any, currentItem?: CollectionResource | ContainerRequestResource | GroupResource) => {
if (currentItem) {
- const addLastItem = { label: currentItem.name, uuid: currentItem.uuid };
- breadcrumbs.push(addLastItem);
+ breadcrumbs.push(resourceToBreadcrumb(currentItem));
}
return propertiesActions.SET_PROPERTY({ key: BREADCRUMBS, value: breadcrumbs });
};
+const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerRequestResource | GroupResource): IconType | undefined => {
+ switch (resource.kind) {
+ case ResourceKind.PROJECT:
+ return ProjectIcon;
+ case ResourceKind.PROCESS:
+ return ProcessIcon;
+ case ResourceKind.COLLECTION:
+ return CollectionIcon;
+ default:
+ return undefined;
+ }
+}
+
+const resourceToBreadcrumb = (resource: CollectionResource | ContainerRequestResource | GroupResource): Breadcrumb => ({
+ label: resource.name,
+ uuid: resource.uuid,
+ icon: resourceToBreadcrumbIcon(resource),
+})
-const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker): ResourceBreadcrumb[] => {
+const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker): Breadcrumb[] => {
const nodes = getSidePanelTreeBranch(uuid)(treePicker);
return nodes.map(node =>
typeof node.value === 'string'
- ? { label: node.value, uuid: node.id }
- : { label: node.value.name, uuid: node.value.uuid });
+ ? {
+ label: node.value,
+ uuid: node.id,
+ icon: getSidePanelIcon(node.value)
+ }
+ : resourceToBreadcrumb(node.value));
};
export const setSidePanelBreadcrumbs = (uuid: string) =>
if (uuidKind === ResourceKind.COLLECTION) {
const collectionItem = item ? item : await services.collectionService.get(currentUuid);
+ const parentProcessItem = await getCollectionParent(collectionItem)(services);
+ if (parentProcessItem) {
+ const mainProcessItem = await getProcessParent(parentProcessItem)(services);
+ mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
+ breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ }
dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
} else if (uuidKind === ResourceKind.PROCESS) {
const processItem = await services.containerRequestService.get(currentUuid);
+ const parentProcessItem = await getProcessParent(processItem)(services);
+ if (parentProcessItem) {
+ breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ }
dispatch(setBreadcrumbs(breadcrumbs, processItem));
}
dispatch(setBreadcrumbs(breadcrumbs));
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const ancestors = await services.ancestorsService.ancestors(uuid, '');
dispatch(updateResources(ancestors));
- const initialBreadcrumbs: ResourceBreadcrumb[] = [
- { label: category, uuid: category }
+ const initialBreadcrumbs: Breadcrumb[] = [
+ {
+ label: category,
+ uuid: category,
+ icon: getSidePanelIcon(category)
+ }
];
const { collectionPanel: { item } } = getState();
const path = getState().router.location!.pathname;
const currentUuid = path.split('/')[2];
const uuidKind = extractUuidKind(currentUuid);
- const breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
+ let breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
ancestor.kind === ResourceKind.GROUP
- ? [...breadcrumbs, { label: ancestor.name, uuid: ancestor.uuid }]
+ ? [...breadcrumbs, resourceToBreadcrumb(ancestor)]
: breadcrumbs,
initialBreadcrumbs);
if (uuidKind === ResourceKind.COLLECTION) {
const collectionItem = item ? item : await services.collectionService.get(currentUuid);
+ const parentProcessItem = await getCollectionParent(collectionItem)(services);
+ if (parentProcessItem) {
+ const mainProcessItem = await getProcessParent(parentProcessItem)(services);
+ mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
+ breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ }
dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
} else if (uuidKind === ResourceKind.PROCESS) {
const processItem = await services.containerRequestService.get(currentUuid);
+ const parentProcessItem = await getProcessParent(processItem)(services);
+ if (parentProcessItem) {
+ breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ }
dispatch(setBreadcrumbs(breadcrumbs, processItem));
}
dispatch(setBreadcrumbs(breadcrumbs));
};
+const getProcessParent = (childProcess: ContainerRequestResource) =>
+ async (services: ServiceRepository): Promise<ContainerRequestResource | undefined> => {
+ if (childProcess.requestingContainerUuid) {
+ const parentProcesses = await services.containerRequestService.list({
+ order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
+ filters: new FilterBuilder().addEqual('container_uuid', childProcess.requestingContainerUuid).getFilters(),
+ select: containerRequestFieldsNoMounts,
+ });
+ if (parentProcesses.items.length > 0) {
+ return parentProcesses.items[0];
+ } else {
+ return undefined;
+ }
+ } else {
+ return undefined;
+ }
+ }
+
+const getCollectionParent = (collection: CollectionResource) =>
+ async (services: ServiceRepository): Promise<ContainerRequestResource | undefined> => {
+ const parentOutputPromise = services.containerRequestService.list({
+ order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
+ filters: new FilterBuilder().addEqual('output_uuid', collection.uuid).getFilters(),
+ select: containerRequestFieldsNoMounts,
+ });
+ const parentLogPromise = services.containerRequestService.list({
+ order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
+ filters: new FilterBuilder().addEqual('log_uuid', collection.uuid).getFilters(),
+ select: containerRequestFieldsNoMounts,
+ });
+ const [parentOutput, parentLog] = await Promise.all([parentOutputPromise, parentLogPromise]);
+ return parentOutput.items.length > 0 ?
+ parentOutput.items[0] :
+ parentLog.items.length > 0 ?
+ parentLog.items[0] :
+ undefined;
+ }
+
+
export const setProjectBreadcrumbs = (uuid: string) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const ancestors = getSidePanelTreeNodeAncestorsIds(uuid)(getState().treePicker);
};
export const setGroupsBreadcrumbs = () =>
- setBreadcrumbs([{ label: SidePanelTreeCategory.GROUPS }]);
+ setBreadcrumbs([{
+ label: SidePanelTreeCategory.GROUPS,
+ uuid: SidePanelTreeCategory.GROUPS,
+ icon: getSidePanelIcon(SidePanelTreeCategory.GROUPS)
+ }]);
export const setGroupDetailsBreadcrumbs = (groupUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const group = getResource<GroupResource>(groupUuid)(getState().resources);
- const breadcrumbs: ResourceBreadcrumb[] = [
- { label: SidePanelTreeCategory.GROUPS, uuid: SidePanelTreeCategory.GROUPS },
+ const breadcrumbs: Breadcrumb[] = [
+ {
+ label: SidePanelTreeCategory.GROUPS,
+ uuid: SidePanelTreeCategory.GROUPS,
+ icon: getSidePanelIcon(SidePanelTreeCategory.GROUPS)
+ },
{ label: group ? group.name : (await services.groupsService.get(groupUuid)).name, uuid: groupUuid },
];
try {
const user = getResource<UserResource>(userUuid)(getState().resources)
|| await services.userService.get(userUuid, false);
- const breadcrumbs: ResourceBreadcrumb[] = [
+ const breadcrumbs: Breadcrumb[] = [
{ label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
{ label: user ? user.username : userUuid, uuid: userUuid },
];
dispatch(setBreadcrumbs(breadcrumbs));
} catch (e) {
- const breadcrumbs: ResourceBreadcrumb[] = [
+ const breadcrumbs: Breadcrumb[] = [
{ label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
{ label: userUuid, uuid: userUuid },
];
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));
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
}));
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));
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 }));
}
//
// 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,
});
-
// 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),
+ });
};
-};
+ };
// 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;
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';
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));
}
}
};
-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;
};
...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,
}
};
-const MAX_AMOUNT_OF_LOGS = 10000;
-
const ALL_FILTER_TYPE = 'All logs';
const MAIN_FILTER_TYPE = 'Main logs';
LogEventType.CRUNCH_RUN,
LogEventType.STDERR,
LogEventType.STDOUT,
+ LogEventType.SNIP,
];
const PROCESS_PANEL_LOG_EVENT_TYPES = [
LogEventType.STDOUT,
LogEventType.CONTAINER,
LogEventType.KEEPSTORE,
+ LogEventType.SNIP,
];
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;
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;
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));
}
};
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;
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 }));
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;
}
};
export const setSearchValueFromAdvancedData = (data: SearchBarAdvancedFormData, prevData?: SearchBarAdvancedFormData) =>
(dispatch: Dispatch, getState: () => RootState) => {
+ if (data.projectObject) {
+ data.projectUuid = data.projectObject.uuid;
+ }
const searchValue = getState().searchBar.searchValue;
const value = getQueryFromAdvancedData({
...data,
};
export const setAdvancedDataFromSearchValue = (search: string, vocabulary: Vocabulary) =>
- async (dispatch: Dispatch) => {
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const data = getAdvancedDataFromQuery(search, vocabulary);
+ if (data.projectUuid) {
+ data.projectObject = await services.projectService.get(data.projectUuid);
+ }
dispatch<any>(initialize(SEARCH_BAR_ADVANCED_FORM_NAME, data));
if (data.projectUuid) {
await dispatch<any>(activateSearchBarProject(data.projectUuid));
recursive: true
}, session, cancelTokens[index].token);
}));
-
+
cancelTokens = [];
-
+
const items = lists.reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
-
+
if (lists.filter(list => !!(list as any).items).length !== lists.length) {
dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
} else {
};
(data.properties || []).forEach(p =>
fo[`prop-"${p.keyID || p.key}":"${p.valueID || p.value}"`] = `"${p.valueID || p.value}"`
- );
+ );
return fo;
};
[`has:"${p.keyID || p.key}"`, `prop-"${p.keyID || p.key}":"${p.valueID || p.value}"`]
));
- const modified = getModifiedKeysValues(flatData(data), prevData ? flatData(prevData):{});
+ const modified = getModifiedKeysValues(flatData(data), prevData ? flatData(prevData) : {});
value = buildQueryFromKeyMap(
- {searchValue: data.searchValue, ...modified} as SearchBarAdvancedFormData, keyMap);
+ { searchValue: data.searchValue, ...modified } as SearchBarAdvancedFormData, keyMap);
value = value.trim();
return value;
// SPDX-License-Identifier: AGPL-3.0
import { getTreePicker, TreePicker } from "store/tree-picker/tree-picker";
-import { getNode, getNodeAncestorsIds, initTreeNode, TreeNodeStatus } from "models/tree";
+import { getNode, getNodeAncestorsIds, initTreeNode } from "models/tree";
import { Dispatch } from "redux";
import { RootState } from "store/store";
import { getUserUuid } from "common/getuser";
};
export const activateSearchBarProject = (id: string) =>
- async (dispatch: Dispatch, getState: () => RootState) => {
- const { treePicker } = getState();
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+
+
+ /*const { treePicker } = getState();
const node = getSearchBarTreeNode(id)(treePicker);
if (node && node.status !== TreeNodeStatus.LOADED) {
await dispatch<any>(loadSearchBarTreeProjects(id));
ids: getSearchBarTreeNodeAncestorsIds(id)(treePicker),
pickerId: SEARCH_BAR_ADVANCED_FORM_PICKER_ID
}));
- dispatch<any>(expandSearchBarTreeItem(id));
+ dispatch<any>(expandSearchBarTreeItem(id));*/
};
import { pluginConfig } from 'plugins';
export enum SidePanelTreeCategory {
- PROJECTS = 'Projects',
+ PROJECTS = 'Home Projects',
SHARED_WITH_ME = 'Shared with me',
PUBLIC_FAVORITES = 'Public Favorites',
FAVORITES = 'My Favorites',
import { collectionPanelReducer } from './collection-panel/collection-panel-reducer';
import { dialogReducer } from './dialog/dialog-reducer';
import { ServiceRepository } from "services/services";
-import { treePickerReducer } from './tree-picker/tree-picker-reducer';
+import { treePickerReducer, treePickerSearchReducer } from './tree-picker/tree-picker-reducer';
+import { treePickerSearchMiddleware } from './tree-picker/tree-picker-middleware';
import { resourcesReducer } from 'store/resources/resources-reducer';
import { propertiesReducer } from './properties/properties-reducer';
import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
declare global {
interface Window {
- __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
+ __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
}
}
publicFavoritesMiddleware,
collectionsContentAddress,
subprocessMiddleware,
+ treePickerSearchMiddleware
];
const reduceMiddlewaresFn: (a: Middleware[],
router: routerReducer,
snackbar: snackbarReducer,
treePicker: treePickerReducer,
+ treePickerSearch: treePickerSearchReducer,
fileUploader: fileUploaderReducer,
processPanel: processPanelReducer,
progressIndicator: progressIndicatorReducer,
import { subprocessPanelActions } from './subprocess-panel-actions';
import { DataColumns } from 'components/data-table/data-table';
import { ProcessStatusFilter, buildProcessStatusFilters } from '../resource-type-filters/resource-type-filters';
-import { ContainerRequestResource } from 'models/container-request';
+import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
import { loadMissingProcessesInformation } from '../project-panel/project-panel-middleware-service';
-import { containerRequestFieldsNoMounts } from 'store/all-processes-panel/all-processes-panel-middleware-service';
export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
import { ResourceKind } from 'models/resource';
import { GroupContentsResource } from 'services/groups-service/groups-service';
import { getTreePicker, TreePicker } from './tree-picker';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from './tree-picker-middleware';
import { OrderBuilder } from 'services/api/order-builder';
import { ProjectResource } from 'models/project';
import { mapTree } from '../../models/tree';
LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
+ EXPAND_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
export type TreePickerAction = UnionOf<typeof treePickerActions>;
+export interface LoadProjectParams {
+ includeCollections?: boolean;
+ includeFiles?: boolean;
+ includeFilterGroups?: boolean;
+ options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
+}
+
+export const treePickerSearchActions = unionize({
+ SET_TREE_PICKER_PROJECT_SEARCH: ofType<{ pickerId: string, projectSearchValue: string }>(),
+ SET_TREE_PICKER_COLLECTION_FILTER: ofType<{ pickerId: string, collectionFilterValue: string }>(),
+ SET_TREE_PICKER_LOAD_PARAMS: ofType<{ pickerId: string, params: LoadProjectParams }>(),
+});
+
+export type TreePickerSearchAction = UnionOf<typeof treePickerSearchActions>;
+
export const getProjectsTreePickerIds = (pickerId: string) => ({
home: `${pickerId}_home`,
shared: `${pickerId}_shared`,
favorites: `${pickerId}_favorites`,
- publicFavorites: `${pickerId}_publicFavorites`
+ publicFavorites: `${pickerId}_publicFavorites`,
+ search: `${pickerId}_search`,
});
export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
export const initProjectsTreePicker = (pickerId: string) =>
async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
- const { home, shared, favorites, publicFavorites } = getProjectsTreePickerIds(pickerId);
+ const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
dispatch<any>(initUserProject(home));
dispatch<any>(initSharedProject(shared));
dispatch<any>(initFavoritesProject(favorites));
dispatch<any>(initPublicFavoritesProject(publicFavorites));
+ dispatch<any>(initSearchProject(search));
};
interface ReceiveTreePickerDataParams<T> {
nodes: data.map(item => initTreeNode(extractNodeData(item))),
pickerId,
}));
- dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE({ id, pickerId }));
};
-interface LoadProjectParams {
+interface LoadProjectParamsWithId extends LoadProjectParams {
id: string;
pickerId: string;
- includeCollections?: boolean;
- includeFiles?: boolean;
- includeFilterGroups?: boolean;
loadShared?: boolean;
- options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
+ searchProjects?: boolean;
}
-export const loadProject = (params: LoadProjectParams) =>
- async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
- const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options } = params;
+
+export const loadProject = (params: LoadProjectParamsWithId) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options, searchProjects = false } = params;
dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
- const filters = pipe(
- (fb: FilterBuilder) => includeCollections
- ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
- : fb.addIsA('uuid', [ResourceKind.PROJECT]),
- fb => fb.getFilters(),
- )(new FilterBuilder());
+ let filterB = new FilterBuilder();
+
+ filterB = (includeCollections && !searchProjects)
+ ? filterB.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
+ : filterB.addIsA('uuid', [ResourceKind.PROJECT]);
+
+ const state = getState();
+
+ if (state.treePickerSearch.collectionFilterValues[pickerId]) {
+ filterB = filterB.addFullTextSearch(state.treePickerSearch.collectionFilterValues[pickerId], 'collections');
+ } else {
+ filterB = filterB.addNotIn("collections.properties.type", ["intermediate", "log"]);
+ }
+
+ if (searchProjects && state.treePickerSearch.projectSearchValues[pickerId]) {
+ filterB = filterB.addFullTextSearch(state.treePickerSearch.projectSearchValues[pickerId], 'groups');
+ }
+
+ const filters = filterB.getFilters();
+
+ const itemLimit = 200;
+
+ const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
+
+ if (itemsAvailable > itemLimit) {
+ items.push({
+ uuid: "more-items-available",
+ kind: ResourceKind.WORKFLOW,
+ name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
+ description: "",
+ definition: "",
+ ownerUuid: "",
+ createdAt: "",
+ modifiedByClientUuid: "",
+ modifiedByUserUuid: "",
+ modifiedAt: "",
+ href: "",
+ etag: ""
+ });
+ }
- const { items } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined });
dispatch<any>(receiveTreePickerData<GroupContentsResource>({
id,
pickerId,
data: items.filter((item) => {
- if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
- return false;
- }
+ if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
+ return false;
+ }
- if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
- return false;
- }
+ if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
+ return false;
+ }
- return true;
- }),
- extractNodeData: item => ({
- id: item.uuid,
- value: item,
- status: item.kind === ResourceKind.PROJECT
- ? TreeNodeStatus.INITIAL
- : includeFiles
- ? TreeNodeStatus.INITIAL
- : TreeNodeStatus.LOADED
+ return true;
}),
+ extractNodeData: item => (
+ item.uuid === "more-items-available" ?
+ {
+ id: item.uuid,
+ value: item,
+ status: TreeNodeStatus.LOADED
+ }
+ : {
+ id: item.uuid,
+ value: item,
+ status: item.kind === ResourceKind.PROJECT
+ ? TreeNodeStatus.INITIAL
+ : includeFiles
+ ? TreeNodeStatus.INITIAL
+ : TreeNodeStatus.LOADED
+ }),
}));
};
const node = getNode(id)(picker);
if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
- const files = await services.collectionService.files(node.value.portableDataHash);
+ const files = await services.collectionService.files(node.value.uuid);
const tree = createCollectionFilesTree(files);
const sorted = sortFilesTree(tree);
const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
dispatch(receiveTreePickerData({
id: '',
pickerId,
- data: [{ uuid, name: 'Projects' }],
+ data: [{ uuid, name: 'Home Projects' }],
extractNodeData: value => ({
id: value.uuid,
status: TreeNodeStatus.INITIAL,
}));
}
};
-export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean } ) =>
+export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const uuid = getUserUuid(getState());
if (uuid) {
}));
};
+export const SEARCH_PROJECT_ID = 'Search all Projects';
+export const initSearchProject = (pickerId: string) =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(receiveTreePickerData({
+ id: '',
+ pickerId,
+ data: [{ uuid: SEARCH_PROJECT_ID, name: SEARCH_PROJECT_ID }],
+ extractNodeData: value => ({
+ id: value.uuid,
+ status: TreeNodeStatus.INITIAL,
+ value,
+ }),
+ }));
+ };
+
+
interface LoadFavoritesProjectParams {
pickerId: string;
includeCollections?: boolean;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from 'store/store';
+import { ServiceRepository } from 'services/services';
+import { Middleware } from "redux";
+import { getNode, getNodeDescendantsIds, TreeNodeStatus } from 'models/tree';
+import { getTreePicker } from './tree-picker';
+import {
+ treePickerSearchActions, loadProject, loadFavoritesProject, loadPublicFavoritesProject,
+ SHARED_PROJECT_ID, FAVORITES_PROJECT_ID, PUBLIC_FAVORITES_PROJECT_ID, SEARCH_PROJECT_ID
+} from "./tree-picker-actions";
+import { LinkResource } from "models/link";
+import { GroupContentsResource } from 'services/groups-service/groups-service';
+import { CollectionDirectory, CollectionFile } from 'models/collection-file';
+
+export interface ProjectsTreePickerRootItem {
+ id: string;
+ name: string;
+}
+
+export type ProjectsTreePickerItem = ProjectsTreePickerRootItem | GroupContentsResource | CollectionDirectory | CollectionFile | LinkResource;
+
+export const treePickerSearchMiddleware: Middleware = store => next => action => {
+ let isSearchAction = false;
+ let searchChanged = false;
+
+ treePickerSearchActions.match(action, {
+ SET_TREE_PICKER_PROJECT_SEARCH: ({ pickerId, projectSearchValue }) => {
+ isSearchAction = true;
+ searchChanged = store.getState().treePickerSearch.projectSearchValues[pickerId] !== projectSearchValue;
+ },
+
+ SET_TREE_PICKER_COLLECTION_FILTER: ({ pickerId, collectionFilterValue }) => {
+ isSearchAction = true;
+ searchChanged = store.getState().treePickerSearch.collectionFilterValues[pickerId] !== collectionFilterValue;
+ },
+ default: () => { }
+ });
+
+ if (isSearchAction && !searchChanged) {
+ return;
+ }
+
+ // pass it on to the reducer
+ const r = next(action);
+
+ treePickerSearchActions.match(action, {
+ SET_TREE_PICKER_PROJECT_SEARCH: ({ pickerId }) =>
+ store.dispatch<any>((dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
+ if (picker) {
+ const loadParams = getState().treePickerSearch.loadProjectParams[pickerId];
+ dispatch<any>(loadProject({
+ ...loadParams,
+ id: SEARCH_PROJECT_ID,
+ pickerId: pickerId,
+ searchProjects: true
+ }));
+ }
+ }),
+
+ SET_TREE_PICKER_COLLECTION_FILTER: ({ pickerId }) =>
+ store.dispatch<any>((dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
+ if (picker) {
+ const loadParams = getState().treePickerSearch.loadProjectParams[pickerId];
+ getNodeDescendantsIds('')(picker)
+ .map(id => {
+ const node = getNode(id)(picker);
+ if (node && node.status !== TreeNodeStatus.INITIAL) {
+ if (node.id.substring(6, 11) === 'tpzed' || node.id.substring(6, 11) === 'j7d0g') {
+ dispatch<any>(loadProject({
+ ...loadParams,
+ id: node.id,
+ pickerId: pickerId,
+ }));
+ }
+ if (node.id === SHARED_PROJECT_ID) {
+ dispatch<any>(loadProject({
+ ...loadParams,
+ id: node.id,
+ pickerId: pickerId,
+ loadShared: true
+ }));
+ }
+ if (node.id === SEARCH_PROJECT_ID) {
+ dispatch<any>(loadProject({
+ ...loadParams,
+ id: node.id,
+ pickerId: pickerId,
+ searchProjects: true
+ }));
+ }
+ if (node.id === FAVORITES_PROJECT_ID) {
+ dispatch<any>(loadFavoritesProject({
+ ...loadParams,
+ pickerId: pickerId,
+ }));
+ }
+ if (node.id === PUBLIC_FAVORITES_PROJECT_ID) {
+ dispatch<any>(loadPublicFavoritesProject({
+ ...loadParams,
+ pickerId: pickerId,
+ }));
+ }
+ }
+ return id;
+ });
+ }
+ }),
+ default: () => { }
+ });
+
+ return r;
+}
//
// SPDX-License-Identifier: AGPL-3.0
-import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode, selectNodes, deselectNodes } from 'models/tree';
+import {
+ createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus,
+ expandNode, deactivateNode, selectNodes, deselectNodes,
+ activateNode, getNode, toggleNodeCollapse, toggleNodeSelection, appendSubtree
+} from 'models/tree';
import { TreePicker } from "./tree-picker";
-import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
+import { treePickerActions, treePickerSearchActions, TreePickerAction, TreePickerSearchAction, LoadProjectParams } from "./tree-picker-actions";
import { compose } from "redux";
-import { activateNode, getNode, toggleNodeCollapse, toggleNodeSelection } from 'models/tree';
import { pipe } from 'lodash/fp';
-import { appendSubtree } from 'models/tree';
export const treePickerReducer = (state: TreePicker = {}, action: TreePickerAction) =>
treePickerActions.match(action, {
LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes, pickerId }) =>
updateOrCreatePicker(state, pickerId, compose(receiveNodes(nodes)(id), setNodeStatus(id)(TreeNodeStatus.LOADED))),
- APPEND_TREE_PICKER_NODE_SUBTREE: ({ id, subtree, pickerId}) =>
+ APPEND_TREE_PICKER_NODE_SUBTREE: ({ id, subtree, pickerId }) =>
updateOrCreatePicker(state, pickerId, compose(appendSubtree(id, subtree), setNodeStatus(id)(TreeNodeStatus.LOADED))),
TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id, pickerId }) =>
updateOrCreatePicker(state, pickerId, toggleNodeCollapse(id)),
+ EXPAND_TREE_PICKER_NODE: ({ id, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, expandNode(id)),
+
ACTIVATE_TREE_PICKER_NODE: ({ id, pickerId, relatedTreePickers = [] }) =>
pipe(
() => relatedTreePickers.reduce(
newState = setNode({ ...parentNode, children: [] })(state);
}
return nodes.reduce((tree, node) => {
+ const preexistingNode = getNode(node.id)(state);
+ if (preexistingNode) {
+ node = { ...preexistingNode, value: node.value };
+ }
return setNode({ ...node, parent })(tree);
}, newState);
};
+
+interface TreePickerSearch {
+ projectSearchValues: { [pickerId: string]: string };
+ collectionFilterValues: { [pickerId: string]: string };
+ loadProjectParams: { [pickerId: string]: LoadProjectParams };
+}
+
+export const treePickerSearchReducer = (state: TreePickerSearch = { projectSearchValues: {}, collectionFilterValues: {}, loadProjectParams: {} }, action: TreePickerSearchAction) =>
+ treePickerSearchActions.match(action, {
+ SET_TREE_PICKER_PROJECT_SEARCH: ({ pickerId, projectSearchValue }) => ({
+ ...state, projectSearchValues: { ...state.projectSearchValues, [pickerId]: projectSearchValue }
+ }),
+
+ SET_TREE_PICKER_COLLECTION_FILTER: ({ pickerId, collectionFilterValue }) => ({
+ ...state, collectionFilterValues: { ...state.collectionFilterValues, [pickerId]: collectionFilterValue }
+ }),
+
+ SET_TREE_PICKER_LOAD_PARAMS: ({ pickerId, params }) => ({
+ ...state, loadProjectParams: { ...state.loadProjectParams, [pickerId]: params }
+ }),
+
+ default: () => state
+ });
// 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';
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';
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';
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';
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);
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)];
import { Dispatch } from 'redux';
import { navigateTo } from 'store/navigation/navigation-action';
import { getProperty } from '../../store/properties/properties';
-import { ResourceBreadcrumb, BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
+import { BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
import { openSidePanelContextMenu } from 'store/context-menu/context-menu-actions';
-import { ProjectResource } from "models/project";
type BreadcrumbsDataProps = Pick<BreadcrumbsProps, 'items' | 'resources'>;
type BreadcrumbsActionProps = Pick<BreadcrumbsProps, 'onClick' | 'onContextMenu'>;
const mapStateToProps = () => ({ properties, resources }: RootState): BreadcrumbsDataProps => ({
- items: (getProperty<ResourceBreadcrumb[]>(BREADCRUMBS)(properties) || []),
+ items: (getProperty<Breadcrumb[]>(BREADCRUMBS)(properties) || []),
resources,
});
const mapDispatchToProps = (dispatch: Dispatch): BreadcrumbsActionProps => ({
- onClick: ({ uuid }: Breadcrumb & ProjectResource) => {
+ onClick: ({ uuid }: Breadcrumb) => {
dispatch<any>(navigateTo(uuid));
},
- onContextMenu: (event, breadcrumb: Breadcrumb & ProjectResource) => {
+ onContextMenu: (event, breadcrumb: Breadcrumb) => {
dispatch<any>(openSidePanelContextMenu(event, breadcrumb.uuid));
}
});
-export const Breadcrumbs = connect(mapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
\ No newline at end of file
+export const Breadcrumbs = connect(mapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
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,
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>
</Grid>;
};
+
const FrozenProject = (props: {item: ProjectResource}) => {
const [fullUsername, setFullusername] = React.useState<any>(null);
const getFullName = React.useCallback(() => {
return resource;
})((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
+
const renderIcon = (item: GroupContentsResource) => {
switch (item.kind) {
case ResourceKind.PROJECT:
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 }) => (
// 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 }) => {
};
})((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 }) => {
const renderOwner = (owner: string) =>
<Typography noWrap>
- {owner}
+ {owner || '-'}
</Typography>;
export const ResourceOwner = 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 }) => {
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>
export const ResourceWithName = userFromID(_resourceWithName);
+
+
export const UserNameFromID =
compose(userFromID)(
(props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
}
render() {
- return renderRunTime(this.state.runtime);
+ return this.props.process ? renderRunTime(this.state.runtime) : <Typography>-</Typography>;
}
});
export const CollectionPartialCopyFields = memoize(
(pickerId: string) =>
() =>
- <div>
+ <>
<CollectionNameField />
<CollectionDescriptionField />
<CollectionProjectPickerField {...{ pickerId }} />
- </div>);
+ </>);
const CopyDialogFields = memoize((pickerId: string) =>
() =>
- <span>
+ <>
<Field
name='name'
component={TextField as any}
component={ProjectTreePickerField}
validate={COPY_FILE_VALIDATION}
pickerId={pickerId}/>
- </span>);
+ </>);
export const CollectionPartialCopyFields = memoize(
(pickerId: string) =>
() =>
- <div>
+ <>
<CollectionPickerField {...{ pickerId }}/>
- </div>);
+ </>);
export const FileUploader = connect(mapStateToProps, mapDispatchToProps)(FileUpload);
export const FileUploaderField = (props: WrappedFieldProps & { label?: string }) =>
- <div>
+ <>
<Typography variant='caption'>{props.label}</Typography>
<FileUploader disabled={false} onDrop={props.input.onChange} />
- </div>;
+ </>;
// SPDX-License-Identifier: AGPL-3.0
import React from "react";
-import { Field, WrappedFieldProps, FieldArray } from 'redux-form';
+import { Field, FieldArray } from 'redux-form';
import { TextField, DateTextField } from "components/text-field/text-field";
import { CheckboxField } from 'components/checkbox-field/checkbox-field';
import { NativeSelectField } from 'components/select-field/select-field';
import { ResourceKind } from 'models/resource';
-import { HomeTreePicker } from 'views-components/projects-tree-picker/home-tree-picker';
-import { SEARCH_BAR_ADVANCED_FORM_PICKER_ID } from 'store/search-bar/search-bar-actions';
import { SearchBarAdvancedPropertiesView } from 'views-components/search-bar/search-bar-advanced-properties-view';
-import { TreeItem } from "components/tree/tree";
-import { ProjectsTreePickerItem } from "views-components/projects-tree-picker/generic-projects-tree-picker";
import { PropertyKeyField, } from 'views-components/resource-properties-form/property-key-field';
import { PropertyValueField } from 'views-components/resource-properties-form/property-value-field';
import { connect } from "react-redux";
import { RootState } from "store/store";
+import { ProjectInput, ProjectCommandInputParameter } from 'views/run-process-panel/inputs/project-input';
export const SearchBarTypeField = () =>
<Field
export const SearchBarClusterField = connect(
(state: RootState) => ({
- clusters: [{key: '', value: 'Any'}].concat(
+ clusters: [{ key: '', value: 'Any' }].concat(
state.auth.sessions
.filter(s => s.loggedIn)
.map(s => ({
}))((props: SearchBarClusterFieldProps) => <Field
name='cluster'
component={NativeSelectField as any}
- items={props.clusters}/>
+ items={props.clusters} />
);
export const SearchBarProjectField = () =>
- <Field
- name='projectUuid'
- component={ProjectsPicker} />;
-
-const ProjectsPicker = (props: WrappedFieldProps) =>
- <div style={{ height: '100px', display: 'flex', flexDirection: 'column', overflow: 'overlay' }}>
- <HomeTreePicker
- pickerId={SEARCH_BAR_ADVANCED_FORM_PICKER_ID}
- toggleItemActive={
- (_: any, { id }: TreeItem<ProjectsTreePickerItem>) => {
- props.input.onChange(id);
- }
- } />
- </div>;
+ <ProjectInput input={{
+ id: "projectObject",
+ label: "Limit search to Project"
+ } as ProjectCommandInputParameter}
+ options={{ showOnlyOwned: false, showOnlyWritable: false }} />
export const SearchBarTrashField = () =>
<Field
import { loadSidePanelTreeProjects } from "store/side-panel-tree/side-panel-tree-actions";
import { Dispatch } from "redux";
-type CssRules = "infoTooltip";
+type CssRules = 'mainBar' | 'breadcrumbContainer' | 'infoTooltip';
const styles: StyleRulesCallback<CssRules> = theme => ({
+ mainBar: {
+ flexWrap: 'nowrap',
+ },
+ breadcrumbContainer: {
+ overflow: 'hidden',
+ },
infoTooltip: {
marginTop: '-10px',
marginLeft: '10px',
export const MainContentBar = connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(
(props: MainContentBarProps & WithStyles<CssRules> & any) =>
- <Toolbar><Grid container>
- <Grid container item xs alignItems="center">
+ <Toolbar><Grid container className={props.classes.mainBar}>
+ <Grid container item xs alignItems="center" className={props.classes.breadcrumbContainer}>
<Breadcrumbs />
</Grid>
<Grid item>
import { ListItemTextIcon } from "components/list-item-text-icon/list-item-text-icon";
import { ProjectIcon, FileInputIcon, IconType, CollectionIcon } from 'components/icon/icon';
import { loadProject, loadCollection } from 'store/tree-picker/tree-picker-actions';
-import { GroupContentsResource } from 'services/groups-service/groups-service';
-import { CollectionDirectory, CollectionFile, CollectionFileType } from 'models/collection-file';
+import { ProjectsTreePickerItem, ProjectsTreePickerRootItem } from 'store/tree-picker/tree-picker-middleware';
import { ResourceKind } from 'models/resource';
import { TreePickerProps, TreePicker } from "views-components/tree-picker/tree-picker";
-import { LinkResource } from "models/link";
+import { CollectionFileType } from 'models/collection-file';
-export interface ProjectsTreePickerRootItem {
- id: string;
- name: string;
-}
-export type ProjectsTreePickerItem = ProjectsTreePickerRootItem | GroupContentsResource | CollectionDirectory | CollectionFile | LinkResource;
type PickedTreePickerProps = Pick<TreePickerProps<ProjectsTreePickerItem>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
export interface ProjectsTreePickerDataProps {
disableActivation?: string[];
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
loadRootItem: (item: TreeItem<ProjectsTreePickerRootItem>, pickerId: string,
- includeCollections?: boolean, includeFiles?: boolean, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) => void;
+ includeCollections?: boolean, includeFiles?: boolean, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) => void;
}
export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & Partial<PickedTreePickerProps>;
import { ProjectsTreePicker, ProjectsTreePickerProps } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
import { Dispatch } from 'redux';
import { loadUserProject } from 'store/tree-picker/tree-picker-actions';
-import { ProjectIcon } from 'components/icon/icon';
+import { ProjectsIcon } from 'components/icon/icon';
export const HomeTreePicker = connect(() => ({
- rootItemIcon: ProjectIcon,
+ rootItemIcon: ProjectsIcon,
}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
dispatch<any>(loadUserProject(pickerId, includeCollections, includeFiles, options));
},
-}))(ProjectsTreePicker);
\ No newline at end of file
+}))(ProjectsTreePicker);
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
+import { Dispatch } from 'redux';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from 'store/store';
import { values, pipe } from 'lodash/fp';
import { HomeTreePicker } from 'views-components/projects-tree-picker/home-tree-picker';
import { SharedTreePicker } from 'views-components/projects-tree-picker/shared-tree-picker';
import { FavoritesTreePicker } from 'views-components/projects-tree-picker/favorites-tree-picker';
-import { getProjectsTreePickerIds, SHARED_PROJECT_ID, FAVORITES_PROJECT_ID } from 'store/tree-picker/tree-picker-actions';
+import { SearchProjectsPicker } from 'views-components/projects-tree-picker/search-projects-picker';
+import {
+ getProjectsTreePickerIds, treePickerActions, treePickerSearchActions, initProjectsTreePicker,
+ SHARED_PROJECT_ID, FAVORITES_PROJECT_ID
+} from 'store/tree-picker/tree-picker-actions';
import { TreeItem } from 'components/tree/tree';
-import { ProjectsTreePickerItem } from './generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { PublicFavoritesTreePicker } from './public-favorites-tree-picker';
+import { SearchInput } from 'components/search-input/search-input';
+import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
-export interface ProjectsTreePickerProps {
+export interface ToplevelPickerProps {
pickerId: string;
includeCollections?: boolean;
includeFiles?: boolean;
toggleItemSelection?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<ProjectsTreePickerItem>, pickerId: string) => void;
}
-export const ProjectsTreePicker = ({ pickerId, ...props }: ProjectsTreePickerProps) => {
- const { home, shared, favorites, publicFavorites } = getProjectsTreePickerIds(pickerId);
- const relatedTreePickers = getRelatedTreePickers(pickerId);
- const p = {
+interface ProjectsTreePickerSearchProps {
+ projectSearch: string;
+ collectionFilter: string;
+}
+
+interface ProjectsTreePickerActionProps {
+ onProjectSearch: (value: string) => void;
+ onCollectionFilter: (value: string) => void;
+}
+
+const mapStateToProps = (state: RootState, props: ToplevelPickerProps): ProjectsTreePickerSearchProps => {
+ const { search } = getProjectsTreePickerIds(props.pickerId);
+ return {
...props,
- relatedTreePickers,
- disableActivation
+ projectSearch: state.treePickerSearch.projectSearchValues[search],
+ collectionFilter: state.treePickerSearch.collectionFilterValues[search],
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch, props: ToplevelPickerProps): (ProjectsTreePickerActionProps & DispatchProp) => {
+ const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(props.pickerId);
+ const params = {
+ includeCollections: props.includeCollections,
+ includeFiles: props.includeFiles,
+ options: props.options
};
- return <div>
- <div data-cy="projects-tree-home-tree-picker">
- <HomeTreePicker pickerId={home} {...p} />
- </div>
- <div data-cy="projects-tree-shared-tree-picker">
- <SharedTreePicker pickerId={shared} {...p} />
- </div>
- <div data-cy="projects-tree-public-favourites-tree-picker">
- <PublicFavoritesTreePicker pickerId={publicFavorites} {...p} />
- </div>
- <div data-cy="projects-tree-favourites-tree-picker">
- <FavoritesTreePicker pickerId={favorites} {...p} />
- </div>
- </div>;
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: home, params }));
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: shared, params }));
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: favorites, params }));
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: publicFavorites, params }));
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_LOAD_PARAMS({ pickerId: search, params }));
+
+ return {
+ onProjectSearch: (projectSearchValue: string) => dispatch(treePickerSearchActions.SET_TREE_PICKER_PROJECT_SEARCH({ pickerId: search, projectSearchValue })),
+ onCollectionFilter: (collectionFilterValue: string) => {
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: home, collectionFilterValue }));
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: shared, collectionFilterValue }));
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: favorites, collectionFilterValue }));
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: publicFavorites, collectionFilterValue }));
+ dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: search, collectionFilterValue }));
+ },
+ dispatch
+ }
};
+type CssRules = 'pickerHeight' | 'searchFlex' | 'scrolledBox';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ pickerHeight: {
+ height: "100%",
+ display: "flex",
+ flexDirection: "column",
+ },
+ searchFlex: {
+ display: "flex",
+ justifyContent: "space-around",
+ paddingBottom: "1em"
+ },
+ scrolledBox: {
+ overflow: "scroll"
+ }
+});
+
+type ProjectsTreePickerCombinedProps = ToplevelPickerProps & ProjectsTreePickerSearchProps & ProjectsTreePickerActionProps & DispatchProp & WithStyles<CssRules>;
+
+export const ProjectsTreePicker = connect(mapStateToProps, mapDispatchToProps)(
+ withStyles(styles)(
+ class FileInputComponent extends React.Component<ProjectsTreePickerCombinedProps> {
+
+ componentDidMount() {
+ const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(this.props.pickerId);
+
+ this.props.dispatch<any>(initProjectsTreePicker(this.props.pickerId));
+
+ this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_PROJECT_SEARCH({ pickerId: search, projectSearchValue: "" }));
+ this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: search, collectionFilterValue: "" }));
+ this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: home, collectionFilterValue: "" }));
+ this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: shared, collectionFilterValue: "" }));
+ this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: favorites, collectionFilterValue: "" }));
+ this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: publicFavorites, collectionFilterValue: "" }));
+ }
+
+ componentWillUnmount() {
+ const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(this.props.pickerId);
+ // Release all the state, we don't need it to hang around forever.
+ this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: search }));
+ this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: home }));
+ this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: shared }));
+ this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: favorites }));
+ this.props.dispatch(treePickerActions.RESET_TREE_PICKER({ pickerId: publicFavorites }));
+ }
+
+ render() {
+ const pickerId = this.props.pickerId;
+ const onProjectSearch = this.props.onProjectSearch;
+ const onCollectionFilter = this.props.onCollectionFilter;
+
+ const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
+ const relatedTreePickers = getRelatedTreePickers(pickerId);
+ const p = {
+ includeCollections: this.props.includeCollections,
+ includeFiles: this.props.includeFiles,
+ showSelection: this.props.showSelection,
+ options: this.props.options,
+ toggleItemActive: this.props.toggleItemActive,
+ toggleItemSelection: this.props.toggleItemSelection,
+ relatedTreePickers,
+ disableActivation,
+ };
+ return <div className={this.props.classes.pickerHeight} >
+ <span className={this.props.classes.searchFlex}>
+ <SearchInput value="" label="Search for a Project" selfClearProp='' onSearch={onProjectSearch} debounce={500} />
+ {this.props.includeCollections &&
+ <SearchInput value="" label="Filter Collections list in Projects" selfClearProp='' onSearch={onCollectionFilter} debounce={500} />}
+ </span>
+
+ <div className={this.props.classes.scrolledBox}>
+ {this.props.projectSearch ?
+ <div data-cy="projects-tree-search-picker">
+ <SearchProjectsPicker {...p} pickerId={search} />
+ </div>
+ :
+ <>
+ <div data-cy="projects-tree-home-tree-picker">
+ <HomeTreePicker {...p} pickerId={home} />
+ </div>
+ <div data-cy="projects-tree-shared-tree-picker">
+ <SharedTreePicker {...p} pickerId={shared} />
+ </div>
+ <div data-cy="projects-tree-public-favourites-tree-picker">
+ <PublicFavoritesTreePicker {...p} pickerId={publicFavorites} />
+ </div>
+ <div data-cy="projects-tree-favourites-tree-picker">
+ <FavoritesTreePicker {...p} pickerId={favorites} />
+ </div>
+ </>}
+ </div>
+ </div >;
+ }
+ }));
+
const getRelatedTreePickers = pipe(getProjectsTreePickerIds, values);
const disableActivation = [SHARED_PROJECT_ID, FAVORITES_PROJECT_ID];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from 'react-redux';
+import { ProjectsTreePicker, ProjectsTreePickerProps } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { Dispatch } from 'redux';
+import { SearchIcon } from 'components/icon/icon';
+import { loadProject } from 'store/tree-picker/tree-picker-actions';
+import { SEARCH_PROJECT_ID } from 'store/tree-picker/tree-picker-actions';
+
+export const SearchProjectsPicker = connect(() => ({
+ rootItemIcon: SearchIcon,
+}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
+ loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
+ dispatch<any>(loadProject({ id: SEARCH_PROJECT_ID, pickerId, includeCollections, includeFiles, searchProjects: true, options }));
+ },
+}))(ProjectsTreePicker);
import { Dispatch } from 'redux';
import { ShareMeIcon } from 'components/icon/icon';
import { loadProject } from 'store/tree-picker/tree-picker-actions';
+import { SHARED_PROJECT_ID } from 'store/tree-picker/tree-picker-actions';
export const SharedTreePicker = connect(() => ({
rootItemIcon: ShareMeIcon,
}), (dispatch: Dispatch): Pick<ProjectsTreePickerProps, 'loadRootItem'> => ({
loadRootItem: (_, pickerId, includeCollections, includeFiles, options) => {
- dispatch<any>(loadProject({ id: 'Shared with me', pickerId, includeCollections, includeFiles, loadShared: true, options }));
+ dispatch<any>(loadProject({ id: SHARED_PROJECT_ID, pickerId, includeCollections, includeFiles, loadShared: true, options }));
},
-}))(ProjectsTreePicker);
\ No newline at end of file
+}))(ProjectsTreePicker);
import { TreeItem } from "components/tree/tree";
import { WrappedFieldProps } from 'redux-form';
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { PickerIdProp } from 'store/tree-picker/picker-id';
export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
- <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
- <ProjectsTreePicker
- pickerId={props.pickerId}
- toggleItemActive={handleChange(props)}
- options={{ showOnlyOwned: false, showOnlyWritable: true }} />
- {props.meta.dirty && props.meta.error &&
- <Typography variant='caption' color='error'>
- {props.meta.error}
- </Typography>}
+ <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
+ <div style={{ flexBasis: '200px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+ <ProjectsTreePicker
+ pickerId={props.pickerId}
+ toggleItemActive={handleChange(props)}
+ options={{ showOnlyOwned: false, showOnlyWritable: true }} />
+ {props.meta.dirty && props.meta.error &&
+ <Typography variant='caption' color='error'>
+ {props.meta.error}
+ </Typography>}
+ </div>
</div>;
const handleChange = (props: WrappedFieldProps) =>
props.input.onChange(id);
export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
- <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
- <ProjectsTreePicker
- pickerId={props.pickerId}
- toggleItemActive={handleChange(props)}
- options={{ showOnlyOwned: false, showOnlyWritable: true }}
- includeCollections />
- {props.meta.dirty && props.meta.error &&
- <Typography variant='caption' color='error'>
- {props.meta.error}
- </Typography>}
- </div>;
\ No newline at end of file
+ <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
+ <div style={{ flexBasis: '200px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+ <ProjectsTreePicker
+ pickerId={props.pickerId}
+ toggleItemActive={handleChange(props)}
+ options={{ showOnlyOwned: false, showOnlyWritable: true }}
+ includeCollections />
+ {props.meta.dirty && props.meta.error &&
+ <Typography variant='caption' color='error'>
+ {props.meta.error}
+ </Typography>}
+ </div>
+ </div>;
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';
// 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;
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>
<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
? FilterGroupIcon
: ProjectIcon;
-const getSidePanelIcon = (category: string) => {
+export const getSidePanelIcon = (category: string) => {
switch (category) {
case SidePanelTreeCategory.FAVORITES:
return FavoriteIcon;
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
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}
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);
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}
</>) :
// 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>
}
</>)
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
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 {
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,
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,
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,
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";
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
import { connect, DispatchProp } from 'react-redux';
import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, getAllNodes } from 'store/tree-picker/tree-picker-actions';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { createSelector, createStructuredSelector } from 'reselect';
import { ChipsInput } from 'components/chips-input/chips-input';
import { identity, values, noop } from 'lodash';
onBlur={this.props.input.onBlur}
disabled={this.props.commandInput.disabled} />
- dialog = () =>
- <Dialog
- open={this.state.open}
- onClose={this.closeDialog}
- fullWidth
- maxWidth='md' >
- <DialogTitle>Choose collections</DialogTitle>
- <DialogContent>
- <this.dialogContent />
- </DialogContent>
- <DialogActions>
- <Button onClick={this.closeDialog}>Cancel</Button>
- <Button
- data-cy='ok-button'
- variant='contained'
- color='primary'
- onClick={this.submit}>Ok</Button>
- </DialogActions>
- </Dialog>
-
dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
root: {
display: 'flex',
flexDirection: 'column',
- height: `${spacing.unit * 8}vh`,
+ },
+ pickerWrapper: {
+ display: 'flex',
+ flexDirection: 'column',
+ flexBasis: `${spacing.unit * 8}vh`,
+ flexShrink: 1,
+ minHeight: 0,
},
tree: {
flex: 3,
padding: `${spacing.unit}px 0`,
overflowX: 'hidden',
},
- })
+ });
+
+ dialog = withStyles(this.dialogContentStyles)(
+ ({ classes }: WithStyles<DialogContentCssRules>) =>
+ <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ maxWidth='md' >
+ <DialogTitle>Choose collections</DialogTitle>
+ <DialogContent className={classes.root}>
+ <this.dialogContent />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ data-cy='ok-button'
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog>
+ );
dialogContent = withStyles(this.dialogContentStyles)(
({ classes }: WithStyles<DialogContentCssRules>) =>
- <div className={classes.root}>
+ <div className={classes.pickerWrapper}>
<div className={classes.tree}>
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
});
-type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
+type DialogContentCssRules = 'root' | 'pickerWrapper' | 'tree' | 'divider' | 'chips';
import { connect, DispatchProp } from 'react-redux';
import { memoize } from 'lodash/fp';
import { Field } from 'redux-form';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
import {
isRequiredInput,
DirectoryCommandInputParameter,
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
import { TreeItem } from 'components/tree/tree';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { CollectionResource } from 'models/collection';
import { ResourceKind } from 'models/resource';
import { ERROR_MESSAGE } from 'validators/require';
input: DirectoryCommandInputParameter;
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}
+
+type DialogContentCssRules = 'root' | 'pickerWrapper';
+
export const DirectoryInput = ({ input, options }: DirectoryInputProps) =>
<Field
name={input.id}
render() {
return <>
{this.renderInput()}
- {this.renderDialog()}
+ <this.dialog />
</>;
}
{...this.props} />;
}
- renderDialog() {
- return <Dialog
- open={this.state.open}
- onClose={this.closeDialog}
- fullWidth
- data-cy="choose-a-directory-dialog"
- maxWidth='md'>
- <DialogTitle>Choose a directory</DialogTitle>
- <DialogContent>
- <ProjectsTreePicker
- pickerId={this.props.commandInput.id}
- includeCollections
- options={this.props.options}
- toggleItemActive={this.setDirectory} />
- </DialogContent>
- <DialogActions>
- <Button onClick={this.closeDialog}>Cancel</Button>
- <Button
- disabled={!this.state.directory}
- variant='contained'
- color='primary'
- onClick={this.submit}>Ok</Button>
- </DialogActions>
- </Dialog>;
- }
+ dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+ root: {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ pickerWrapper: {
+ flexBasis: `${spacing.unit * 8}vh`,
+ flexShrink: 1,
+ minHeight: 0,
+ },
+ });
+
+ dialog = withStyles(this.dialogContentStyles)(
+ ({ classes }: WithStyles<DialogContentCssRules>) =>
+ <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ data-cy="choose-a-directory-dialog"
+ maxWidth='md'>
+ <DialogTitle>Choose a directory</DialogTitle>
+ <DialogContent className={classes.root}>
+ <div className={classes.pickerWrapper}>
+ <ProjectsTreePicker
+ pickerId={this.props.commandInput.id}
+ includeCollections
+ options={this.props.options}
+ toggleItemActive={this.setDirectory} />
+ </div>
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ disabled={!this.state.directory}
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog>
+ );
});
-
-
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
import { connect, DispatchProp } from 'react-redux';
import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds } from 'store/tree-picker/tree-picker-actions';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { CollectionFile, CollectionFileType } from 'models/collection-file';
import { createSelector, createStructuredSelector } from 'reselect';
import { ChipsInput } from 'components/chips-input/chips-input';
onKeyPress={!this.props.commandInput.disabled ? this.openDialog : undefined}
onBlur={this.props.input.onBlur} />
- dialog = () =>
- <Dialog
- open={this.state.open}
- onClose={this.closeDialog}
- fullWidth
- maxWidth='md' >
- <DialogTitle>Choose files</DialogTitle>
- <DialogContent>
- <this.dialogContent />
- </DialogContent>
- <DialogActions>
- <Button onClick={this.closeDialog}>Cancel</Button>
- <Button
- data-cy='ok-button'
- variant='contained'
- color='primary'
- onClick={this.submit}>Ok</Button>
- </DialogActions>
- </Dialog>
-
dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
root: {
display: 'flex',
flexDirection: 'column',
- height: `${spacing.unit * 8}vh`,
+ },
+ pickerWrapper: {
+ display: 'flex',
+ flexDirection: 'column',
+ flexBasis: `${spacing.unit * 8}vh`,
+ flexShrink: 1,
+ minHeight: 0,
},
tree: {
flex: 3,
},
})
+
+ dialog = withStyles(this.dialogContentStyles)(
+ ({ classes }: WithStyles<DialogContentCssRules>) =>
+ <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ maxWidth='md' >
+ <DialogTitle>Choose files</DialogTitle>
+ <DialogContent className={classes.root}>
+ <this.dialogContent />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ data-cy='ok-button'
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog>
+ );
+
dialogContent = withStyles(this.dialogContentStyles)(
({ classes }: WithStyles<DialogContentCssRules>) =>
- <div className={classes.root}>
+ <div className={classes.pickerWrapper}>
<div className={classes.tree}>
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
});
-type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
+type DialogContentCssRules = 'root' | 'pickerWrapper' | 'tree' | 'divider' | 'chips';
} from 'models/workflow';
import { Field } from 'redux-form';
import { ERROR_MESSAGE } from 'validators/require';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
import { GenericInputProps, GenericInput } from './generic-input';
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
import { connect, DispatchProp } from 'react-redux';
import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
import { TreeItem } from 'components/tree/tree';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { CollectionFile, CollectionFileType } from 'models/collection-file';
export interface FileInputProps {
input: FileCommandInputParameter;
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}
+
+type DialogContentCssRules = 'root' | 'pickerWrapper';
+
export const FileInput = ({ input, options }: FileInputProps) =>
<Field
name={input.id}
render() {
return <>
{this.renderInput()}
- {this.renderDialog()}
+ <this.dialog />
</>;
}
{...this.props} />;
}
- renderDialog() {
- return <Dialog
- open={this.state.open}
- onClose={this.closeDialog}
- fullWidth
- data-cy="choose-a-file-dialog"
- maxWidth='md'>
- <DialogTitle>Choose a file</DialogTitle>
- <DialogContent>
+ dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+ root: {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ pickerWrapper: {
+ flexBasis: `${spacing.unit * 8}vh`,
+ flexShrink: 1,
+ minHeight: 0,
+ },
+ });
+
+ dialog = withStyles(this.dialogContentStyles)(
+ ({ classes }: WithStyles<DialogContentCssRules>) =>
+ <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ data-cy="choose-a-file-dialog"
+ maxWidth='md'>
+ <DialogTitle>Choose a file</DialogTitle>
+ <DialogContent className={classes.root}>
+ <this.dialogContent />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ disabled={!this.state.file}
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog >
+ );
+
+ dialogContent = withStyles(this.dialogContentStyles)(
+ ({ classes }: WithStyles<DialogContentCssRules>) =>
+ <div className={classes.pickerWrapper}>
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
includeCollections
includeFiles
options={this.props.options}
toggleItemActive={this.setFile} />
- </DialogContent>
- <DialogActions>
- <Button onClick={this.closeDialog}>Cancel</Button>
- <Button
- disabled={!this.state.file}
- variant='contained'
- color='primary'
- onClick={this.submit}>Ok</Button>
- </DialogActions>
- </Dialog>;
- }
-
- });
+ </div>
+ );
+ });
import React from 'react';
import { connect, DispatchProp } from 'react-redux';
import { Field } from 'redux-form';
-import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, withStyles, WithStyles, StyleRulesCallback } from '@material-ui/core';
import {
GenericCommandInputParameter
} from 'models/workflow';
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
import { TreeItem } from 'components/tree/tree';
-import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { ProjectResource } from 'models/project';
import { ResourceKind } from 'models/resource';
import { RootState } from 'store/store';
input: ProjectCommandInputParameter;
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}
+
+type DialogContentCssRules = 'root' | 'pickerWrapper';
+
export const ProjectInput = ({ input, options }: ProjectInputProps) =>
<Field
name={input.id}
render() {
return <>
{this.renderInput()}
- {this.renderDialog()}
+ <this.dialog />
</>;
}
{...this.props} />;
}
- renderDialog() {
- return this.state.open ? <Dialog
- open={this.state.open}
- onClose={this.closeDialog}
- fullWidth
- data-cy="choose-a-project-dialog"
- maxWidth='md'>
- <DialogTitle>Choose a project</DialogTitle>
- <DialogContent>
- <ProjectsTreePicker
- pickerId={this.props.commandInput.id}
- options={this.props.options}
- toggleItemActive={this.setProject} />
- </DialogContent>
- <DialogActions>
- <Button onClick={this.closeDialog}>Cancel</Button>
- <Button
- disabled={this.invalid()}
- variant='contained'
- color='primary'
- onClick={this.submit}>Ok</Button>
- </DialogActions>
- </Dialog> : null;
- }
+ dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+ root: {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ pickerWrapper: {
+ flexBasis: `${spacing.unit * 8}vh`,
+ flexShrink: 1,
+ minHeight: 0,
+ },
+ });
+
+ dialog = withStyles(this.dialogContentStyles)(
+ ({ classes }: WithStyles<DialogContentCssRules>) =>
+ this.state.open ? <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ data-cy="choose-a-project-dialog"
+ maxWidth='md'>
+ <DialogTitle>Choose a project</DialogTitle>
+ <DialogContent className={classes.root}>
+ <div className={classes.pickerWrapper}>
+ <ProjectsTreePicker
+ pickerId={this.props.commandInput.id}
+ options={this.props.options}
+ toggleItemActive={this.setProject} />
+ </div>
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ disabled={this.invalid()}
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog> : null
+ );
});