Merge branch '17306-Favorites-in-copy-dialog-is-different-to-favorite-list'
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Thu, 18 Feb 2021 21:12:08 +0000 (22:12 +0100)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Thu, 18 Feb 2021 21:12:14 +0000 (22:12 +0100)
closes #17306

Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

16 files changed:
cypress/integration/collection.spec.js [moved from cypress/integration/collection-panel.spec.js with 92% similarity]
cypress/integration/project.spec.js [new file with mode: 0644]
cypress/integration/side-panel.spec.js
src/components/details-attribute/details-attribute.tsx
src/store/collection-panel/collection-panel-action.ts
src/store/projects/project-create-actions.ts
src/store/resources/resources-actions.ts
src/store/subprocess-panel/subprocess-panel-middleware-service.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/details-panel/process-details.tsx
src/views-components/details-panel/project-details.tsx
src/views-components/project-properties/create-project-properties-form.tsx
src/views-components/project-properties/create-project-properties-list.tsx
src/views-components/resource-properties-form/property-chip.tsx
src/views-components/resource-properties-form/resource-properties-form.tsx
src/views/collection-panel/collection-panel.tsx

similarity index 92%
rename from cypress/integration/collection-panel.spec.js
rename to cypress/integration/collection.spec.js
index 3e241cebbea390a58d164e5e2be93dedfab042c9..8db574ce9ff473201adc5b7e81f4e2002cc6a8f9 100644 (file)
@@ -38,7 +38,7 @@ describe('Collection panel tests', function() {
             cy.doSearch(`${this.testCollection.uuid}`);
 
             // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
-            cy.get('[data-cy=collection-properties-form]').within(() => {
+            cy.get('[data-cy=resource-properties-form]').within(() => {
                 cy.get('[data-cy=property-field-key]').within(() => {
                     cy.get('input').type('Color');
                 });
@@ -64,6 +64,10 @@ describe('Collection panel tests', function() {
     it('shows collection by URL', function() {
         cy.loginAs(activeUser);
         [true, false].map(function(isWritable) {
+            // Using different file names to avoid test flakyness: the second iteration
+            // on this loop may pass an assertion from the first iteration by looking
+            // for the same file name.
+            const fileName = isWritable ? 'bar' : 'foo';
             cy.createGroup(adminUser.token, {
                 name: 'Shared project',
                 group_class: 'project',
@@ -74,7 +78,7 @@ describe('Collection panel tests', function() {
                     name: 'Test collection',
                     owner_uuid: this.sharedGroup.uuid,
                     properties: {someKey: 'someValue'},
-                    manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
+                    manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`})
                 .as('testCollection').then(function() {
                     // Share the group with active user.
                     cy.createLink(adminUser.token, {
@@ -107,7 +111,7 @@ describe('Collection panel tests', function() {
                         .and('not.contain', 'anotherValue')
                     if (isWritable === true) {
                         // Check that properties can be added.
-                        cy.get('[data-cy=collection-properties-form]').within(() => {
+                        cy.get('[data-cy=resource-properties-form]').within(() => {
                             cy.get('[data-cy=property-field-key]').within(() => {
                                 cy.get('input').type('anotherKey');
                             });
@@ -121,18 +125,18 @@ describe('Collection panel tests', function() {
                             .and('contain', 'anotherValue')
                     } else {
                         // Properties form shouldn't be displayed.
-                        cy.get('[data-cy=collection-properties-form]').should('not.exist');
+                        cy.get('[data-cy=resource-properties-form]').should('not.exist');
                     }
                     // Check that the file listing show both read & write operations
                     cy.get('[data-cy=collection-files-panel]').within(() => {
-                        cy.root().should('contain', 'bar');
+                        cy.root().should('contain', fileName);
                         if (isWritable) {
                             cy.get('[data-cy=upload-button]')
                                 .should(`${isWritable ? '' : 'not.'}contain`, 'Upload data');
                         }
                     });
                     cy.get('[data-cy=collection-files-panel]')
-                        .contains('bar').rightclick();
+                        .contains(fileName).rightclick();
                     cy.get('[data-cy=context-menu]')
                         .should('contain', 'Download')
                         .and('contain', 'Open in new tab')
@@ -468,4 +472,30 @@ describe('Collection panel tests', function() {
                 .should('contain', 'foo').and('contain', 'bar');
         });
     });
+
+    it('creates new collection on home project', function() {
+        cy.loginAs(activeUser);
+        cy.doSearch(`${activeUser.user.uuid}`);
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('not.exist');
+        // Create new collection
+        cy.get('[data-cy=side-panel-button]').click();
+        cy.get('[data-cy=side-panel-new-collection]').click();
+        const collName = `Test collection (${Math.floor(999999 * Math.random())})`;
+        cy.get('[data-cy=form-dialog]')
+            .should('contain', 'New collection')
+            .within(() => {
+                cy.get('[data-cy=parent-field]').within(() => {
+                    cy.get('input').should('have.value', 'Home project');
+                })
+                cy.get('[data-cy=name-field]').within(() => {
+                    cy.get('input').type(collName);
+                })
+            })
+        cy.get('[data-cy=form-submit-btn]').click();
+        // Confirm that the user was taken to the newly created thing
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('contain', collName);
+    });
 })
diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
new file mode 100644 (file)
index 0000000..69809b2
--- /dev/null
@@ -0,0 +1,108 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('Project tests', function() {
+    let activeUser;
+    let adminUser;
+
+    before(function() {
+        // Only set up common users once. These aren't set up as aliases because
+        // aliases are cleaned up after every test. Also it doesn't make sense
+        // to set the same users on beforeEach() over and over again, so we
+        // separate a little from Cypress' 'Best Practices' here.
+        cy.getUser('admin', 'Admin', 'User', true, true)
+            .as('adminUser').then(function() {
+                adminUser = this.adminUser;
+            }
+        );
+        cy.getUser('user', 'Active', 'User', false, true)
+            .as('activeUser').then(function() {
+                activeUser = this.activeUser;
+            }
+        );
+    });
+
+    beforeEach(function() {
+        cy.clearCookies();
+        cy.clearLocalStorage();
+    });
+
+    it('adds creates a new project with properties', function() {
+        const projName = `Test project (${Math.floor(999999 * Math.random())})`;
+        cy.loginAs(activeUser);
+        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(projName);
+                });
+
+            });
+        // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
+        cy.get('[data-cy=resource-properties-form]').within(() => {
+            cy.get('[data-cy=property-field-key]').within(() => {
+                cy.get('input').type('Color');
+            });
+            cy.get('[data-cy=property-field-value]').within(() => {
+                cy.get('input').type('Magenta');
+            });
+            cy.root().submit();
+        });
+        // Confirm proper vocabulary labels are displayed on the UI.
+        cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
+
+        // Create project and confirm the properties' real values.
+        cy.get('[data-cy=form-submit-btn]').click();
+        cy.get('[data-cy=breadcrumb-last]').should('contain', projName);
+        cy.doRequest('GET', '/arvados/v1/groups', null, {
+            filters: `[["name", "=", "${projName}"], ["group_class", "=", "project"]]`,
+        })
+        .its('body.items').as('projects')
+        .then(function() {
+            expect(this.projects).to.have.lengthOf(1);
+            expect(this.projects[0].properties).to.deep.equal(
+                {IDTAGCOLORS: 'IDVALCOLORS3'});
+        });
+    });
+
+    it('creates new project on home project and then a subproject inside it', function() {
+        const createProject = function(name, parentName) {
+            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=parent-field]').within(() => {
+                        cy.get('input').invoke('val').then((val) => {
+                            expect(val).to.include(parentName);
+                        });
+                    });
+                    cy.get('[data-cy=name-field]').within(() => {
+                        cy.get('input').type(name);
+                    });
+                });
+            cy.get('[data-cy=form-submit-btn]').click();
+        }
+
+        cy.loginAs(activeUser);
+        cy.doSearch(`${activeUser.user.uuid}`);
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('not.exist');
+        // Create new project
+        const projName = `Test project (${Math.floor(999999 * Math.random())})`;
+        createProject(projName, 'Home project');
+        // Confirm that the user was taken to the newly created thing
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('contain', projName);
+        // Create a subproject
+        const subProjName = `Test project (${Math.floor(999999 * Math.random())})`;
+        createProject(subProjName, projName);
+        cy.get('[data-cy=form-dialog]').should('not.exist');
+        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
+        cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
+    });
+})
\ No newline at end of file
index fe9d840010350c216a4d7f1e730f9685544cea55..309037ec58a9536112f62133cb2b46fa84936f69 100644 (file)
@@ -75,68 +75,4 @@ describe('Side panel tests', function() {
                 .and('be.disabled');
         })
     })
-
-    it('creates new collection on home project', function() {
-        cy.loginAs(activeUser);
-        cy.doSearch(`${activeUser.user.uuid}`);
-        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
-        cy.get('[data-cy=breadcrumb-last]').should('not.exist');
-        // Create new collection
-        cy.get('[data-cy=side-panel-button]').click();
-        cy.get('[data-cy=side-panel-new-collection]').click();
-        const collName = `Test collection (${Math.floor(999999 * Math.random())})`;
-        cy.get('[data-cy=form-dialog]')
-            .should('contain', 'New collection')
-            .within(() => {
-                cy.get('[data-cy=parent-field]').within(() => {
-                    cy.get('input').should('have.value', 'Home project');
-                })
-                cy.get('[data-cy=name-field]').within(() => {
-                    cy.get('input').type(collName);
-                })
-            })
-        cy.get('[data-cy=form-submit-btn]').click();
-        // Confirm that the user was taken to the newly created thing
-        cy.get('[data-cy=form-dialog]').should('not.exist');
-        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
-        cy.get('[data-cy=breadcrumb-last]').should('contain', collName);
-    })
-
-    it('creates new project on home project and then a subproject inside it', function() {
-        const createProject = function(name, parentName) {
-            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=parent-field]').within(() => {
-                        cy.get('input').invoke('val').then((val) => {
-                            expect(val).to.include(parentName);
-                        })
-                    })
-                    cy.get('[data-cy=name-field]').within(() => {
-                        cy.get('input').type(name);
-                    })
-                })
-            cy.get('[data-cy=form-submit-btn]').click();
-        }
-
-        cy.loginAs(activeUser);
-        cy.doSearch(`${activeUser.user.uuid}`);
-        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
-        cy.get('[data-cy=breadcrumb-last]').should('not.exist');
-        // Create new project
-        const projName = `Test project (${Math.floor(999999 * Math.random())})`;
-        createProject(projName, 'Home project');
-        // Confirm that the user was taken to the newly created thing
-        cy.get('[data-cy=form-dialog]').should('not.exist');
-        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
-        cy.get('[data-cy=breadcrumb-last]').should('contain', projName);
-        // Create a subproject
-        const subProjName = `Test project (${Math.floor(999999 * Math.random())})`;
-        createProject(subProjName, projName);
-        cy.get('[data-cy=form-dialog]').should('not.exist');
-        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
-        cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
-    })
 })
\ No newline at end of file
index 01276c57edbfc3d07aa2a00a75e432a3844732b0..7633b71a45685137c92ec69a32f972d4cb0c3109 100644 (file)
@@ -62,6 +62,7 @@ interface DetailsAttributeDataProps {
     onValueClick?: () => void;
     linkToUuid?: string;
     copyValue?: string;
+    uuidEnhancer?: Function;
 }
 
 type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules> & FederationConfig & DispatchProp;
@@ -84,23 +85,25 @@ export const DetailsAttribute = connect(mapStateToProps)(withStyles(styles)(
         }
 
         render() {
-            const { label, link, value, children, classes, classLabel,
+            const { uuidEnhancer, label, link, value, children, classes, classLabel,
                 classValue, lowercaseValue, onValueClick, linkToUuid,
                 localCluster, remoteHostsConfig, sessions, copyValue } = this.props;
             let valueNode: React.ReactNode;
 
             if (linkToUuid) {
+            const uuid = uuidEnhancer ? uuidEnhancer(linkToUuid) : linkToUuid;
                 const linkUrl = getNavUrl(linkToUuid || "", { localCluster, remoteHostsConfig, sessions });
                 if (linkUrl[0] === '/') {
-                    valueNode = <Link to={linkUrl} className={classes.link}>{linkToUuid}</Link>;
+                    valueNode = <Link to={linkUrl} className={classes.link}>{uuid}</Link>;
                 } else {
-                    valueNode = <a href={linkUrl} className={classes.link} target='_blank'>{linkToUuid}</a>;
+                    valueNode = <a href={linkUrl} className={classes.link} target='_blank'>{uuid}</a>;
                 }
             } else if (link) {
                 valueNode = <a href={link} className={classes.link} target='_blank'>{value}</a>;
             } else {
                 valueNode = value;
             }
+
             return <Typography component="div" className={classes.attribute}>
                 <Typography component="div" className={classnames([classes.label, classLabel])}>{label}</Typography>
                 <Typography
index 7881d6723854242525c4b5c88a907d72772e870c..ff89ca38853946dde94c818e151ea067ee51e994 100644 (file)
@@ -61,7 +61,7 @@ export const createCollectionTag = (data: TagProperty) =>
             dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection));
             dispatch(resourcesActions.SET_RESOURCES([updatedCollection]));
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Tag has been successfully added.",
+                message: "Property has been successfully added.",
                 hideDuration: 2000,
                 kind: SnackbarKind.SUCCESS }));
             dispatch<any>(loadDetailsPanel(updatedCollection.uuid));
index 583a4bd6978237bfc695fd8fe8f1cdae93bd41a1..3599378ceec836abbd81f9b9f9054413cc7a39c2 100644 (file)
@@ -13,6 +13,7 @@ import { ServiceRepository } from '~/services/services';
 import { matchProjectRoute, matchRunProcessRoute } from '~/routes/routes';
 import { ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form';
 import { RouterState } from "react-router-redux";
+import { addProperty, deleteProperty } from "~/lib/resource-properties";
 
 export interface ProjectCreateFormDialogData {
     ownerUuid: string;
@@ -22,7 +23,7 @@ export interface ProjectCreateFormDialogData {
 }
 
 export interface ProjectProperties {
-    [key: string]: string;
+    [key: string]: string | string[];
 }
 
 export const PROJECT_CREATE_FORM_NAME = 'projectCreateFormName';
@@ -69,13 +70,19 @@ export const createProject = (project: Partial<ProjectResource>) =>
 export const addPropertyToCreateProjectForm = (data: ResourcePropertiesFormData) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
-        properties[data.key] = data.value;
-        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties));
+        const key = data.keyID || data.key;
+        const value =  data.valueID || data.value;
+        dispatch(change(
+            PROJECT_CREATE_FORM_NAME,
+            'properties',
+            addProperty(properties, key, value)));
     };
 
-export const removePropertyFromCreateProjectForm = (key: string) =>
+export const removePropertyFromCreateProjectForm = (key: string, value: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
-        delete properties[key];
-        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties));
+        dispatch(change(
+            PROJECT_CREATE_FORM_NAME,
+            'properties',
+            deleteProperty(properties, key, value)));
     };
index 1de2feff8b9f8d42111090f132f7dfba4a624a0c..5465db62aef508d3a00ca42eb007a52878562ffa 100644 (file)
@@ -18,13 +18,13 @@ export type ResourcesAction = UnionOf<typeof resourcesActions>;
 
 export const updateResources = (resources: Resource[]) => resourcesActions.SET_RESOURCES(resources);
 
-export const loadResource = (uuid: string) =>
+export const loadResource = (uuid: string, showErrors?: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
             const kind = extractUuidKind(uuid);
             const service = getResourceService(kind)(services);
             if (service) {
-                const resource = await service.get(uuid);
+                const resource = await service.get(uuid, showErrors);
                 dispatch<any>(updateResources([resource]));
                 return resource;
             }
index 6fdb8de24ea40a34bf1948707c1b86dbe1b73cc8..4383063dfcbe616d8bf9e879d859d39cd6d708ce 100644 (file)
@@ -32,11 +32,12 @@ export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
 
     async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
         const state = api.getState();
+        const parentContainerRequestUuid = state.processPanel.containerRequestUuid;
+        if (parentContainerRequestUuid === "") { return; }
         const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
 
         try {
             api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
-            const parentContainerRequestUuid = state.processPanel.containerRequestUuid;
             const parentContainerRequest = await this.services.containerRequestService.get(parentContainerRequestUuid);
             const containerRequests = await this.services.containerRequestService.list(
                 { ...getParams(dataExplorer, parentContainerRequest) });
index 6d95196d3ed1ce8eb79fa8600d8cc4c618f56c92..6cf29faecf444683540de031966273298fc19314 100644 (file)
@@ -20,13 +20,14 @@ import { WorkflowResource } from '~/models/workflow';
 import { ResourceStatus as WorkflowStatus } from '~/views/workflow-panel/workflow-panel-view';
 import { getUuidPrefix, openRunProcess } from '~/store/workflow-panel/workflow-panel-actions';
 import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
-import { UserResource } from '~/models/user';
+import { getUserFullname, User, UserResource } from '~/models/user';
 import { toggleIsActive, toggleIsAdmin } from '~/store/users/users-actions';
 import { LinkResource } from '~/models/link';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { withResourceData } from '~/views-components/data-explorer/with-resources';
 import { CollectionResource } from '~/models/collection';
 import { IllegalNamingWarning } from '~/components/warning/warning';
+import { loadResource } from '~/store/resources/resources-actions';
 
 const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
@@ -434,6 +435,35 @@ export const ResourceOwnerName = connect(
         return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
     })((props: { owner: string }) => renderOwner(props.owner));
 
+export const ResourceOwnerWithName =
+    compose(
+        connect(
+            (state: RootState, props: { uuid: string }) => {
+                let ownerName = '';
+                const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
+
+                if (resource) {
+                    ownerName = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
+                }
+
+                return { uuid: props.uuid, ownerName };
+            }),
+        withStyles({}, { withTheme: true }))
+        ((props: { uuid: string, ownerName: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
+            const { uuid, ownerName, dispatch, theme } = props;
+
+            if (ownerName === '') {
+                dispatch<any>(loadResource(uuid, false));
+                return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
+                    {uuid}
+                </Typography>;
+            }
+
+            return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
+                {ownerName} ({uuid})
+            </Typography>;
+        });
+
 const renderType = (type: string) =>
     <Typography noWrap>
         {resourceLabel(type)}
@@ -446,20 +476,20 @@ export const ResourceType = connect(
     })((props: { type: string }) => renderType(props.type));
 
 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
-        return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
-    })((props: { resource: GroupContentsResource }) =>
-        (props.resource && props.resource.kind === ResourceKind.COLLECTION)
+    return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
+})((props: { resource: GroupContentsResource }) =>
+    (props.resource && props.resource.kind === ResourceKind.COLLECTION)
         ? <CollectionStatus uuid={props.resource.uuid} />
         : <ProcessStatus uuid={props.resource.uuid} />
-    );
+);
 
 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
-        return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
-    })((props: { collection: CollectionResource }) =>
-        (props.collection.uuid !== props.collection.currentVersionUuid)
+    return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
+})((props: { collection: CollectionResource }) =>
+    (props.collection.uuid !== props.collection.currentVersionUuid)
         ? <Typography>version {props.collection.version}</Typography>
         : <Typography>head version</Typography>
-    );
+);
 
 export const ProcessStatus = compose(
     connect((state: RootState, props: { uuid: string }) => {
@@ -478,7 +508,7 @@ export const ProcessStatus = compose(
 export const ProcessStartDate = connect(
     (state: RootState, props: { uuid: string }) => {
         const process = getProcess(props.uuid)(state.resources);
-        return { date: ( process && process.container ) ? process.container.startedAt : '' };
+        return { date: (process && process.container) ? process.container.startedAt : '' };
     })((props: { date: string }) => renderDate(props.date));
 
 export const renderRunTime = (time: number) =>
index aa1b3a1d73532de76b07398bc177da7f0d57ea1f..0867f92d2d38a68074e2a3aa739bd318771dd5ae 100644 (file)
@@ -10,6 +10,7 @@ import { ResourceKind } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
 import { DetailsData } from "./details-data";
 import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
+import { ResourceOwnerWithName } from '../data-explorer/renderers';
 
 export class ProcessDetails extends DetailsData<ProcessResource> {
 
@@ -20,7 +21,8 @@ export class ProcessDetails extends DetailsData<ProcessResource> {
     getDetails() {
         return <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
-            <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} value={this.item.ownerUuid} />
+            <DetailsAttribute label='Owner' linkToUuid={this.item.ownerUuid} value={this.item.ownerUuid}
+                uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
 
             <DetailsAttribute label='Status' value={this.item.state} />
             <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
index b901abce8ba7a9d497ab17873152fae196b4c990..61797373b69a51ddb8dcd8039cf5180610ed5749 100644 (file)
@@ -17,6 +17,7 @@ import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { Dispatch } from 'redux';
 import { getPropertyChip } from '../resource-properties-form/property-chip';
+import { ResourceOwnerWithName } from '../data-explorer/renderers';
 
 export class ProjectDetails extends DetailsData<ProjectResource> {
     getIcon(className?: string) {
@@ -59,7 +60,8 @@ const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
     withStyles(styles)(
         ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
-            <DetailsAttribute label='Owner' linkToUuid={project.ownerUuid} lowercaseValue={true} />
+            <DetailsAttribute label='Owner' linkToUuid={project.ownerUuid}
+                uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
             <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
             <DetailsAttribute label='Created at' value={formatDate(project.createdAt)} />
             <DetailsAttribute label='Project UUID' linkToUuid={project.uuid} value={project.uuid} />
index 385afff7684c1ae8cca710422580de008f29c243..648547733409ea4eef8f1e25164cd457f77f326d 100644 (file)
@@ -2,48 +2,26 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from 'react';
-import { reduxForm, reset, InjectedFormProps } from 'redux-form';
-import { PROJECT_CREATE_PROPERTIES_FORM_NAME, addPropertyToCreateProjectForm } from '~/store/projects/project-create-actions';
-import { ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form';
-import { StyleRulesCallback, WithStyles, withStyles, Grid } from '@material-ui/core';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { PropertyKeyField } from '~/views-components/resource-properties-form/property-key-field';
-import { PropertyValueField } from '~/views-components/resource-properties-form/property-value-field';
-import { Button } from '~/views-components/resource-properties-form/resource-properties-form';
+import { reduxForm, reset } from 'redux-form';
+import { withStyles } from '@material-ui/core';
+import {
+    PROJECT_CREATE_PROPERTIES_FORM_NAME,
+    addPropertyToCreateProjectForm
+} from '~/store/projects/project-create-actions';
+import {
+    ResourcePropertiesForm,
+    ResourcePropertiesFormData
+} from '~/views-components/resource-properties-form/resource-properties-form';
 
-type CssRules = 'root';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
-        paddingTop: theme.spacing.unit,
-        margin: 0
-    }
-});
-
-type CreateProjectPropertiesFormProps = InjectedFormProps<ResourcePropertiesFormData> & WithStyles<CssRules>;
-
-const Form = withStyles(styles)(
-    ({ handleSubmit, submitting, invalid, classes }: CreateProjectPropertiesFormProps) =>
-        <Grid container spacing={16} className={classes.root}>
-            <Grid item xs={5}>
-                <PropertyKeyField />
-            </Grid>
-            <Grid item xs={5}>
-                <PropertyValueField />
-            </Grid>
-            <Grid item xs={2}>
-                <Button
-                    disabled={invalid}
-                    loading={submitting}
-                    color='primary'
-                    variant='contained'
-                    onClick={handleSubmit}>
-                    Add
-                </Button>
-            </Grid>
-        </Grid>
-);
+const Form = withStyles(
+    ({ spacing }) => (
+        { container:
+            {
+                paddingTop: spacing.unit,
+                margin: 0,
+            }
+        })
+    )(ResourcePropertiesForm);
 
 export const CreateProjectPropertiesForm = reduxForm<ResourcePropertiesFormData>({
     form: PROJECT_CREATE_PROPERTIES_FORM_NAME,
index 1d2050fe4c25343f2f903a7ab27a469bd972ec60..b1de75b24d0affddfaebdd38acd4ddf7c2617154 100644 (file)
@@ -5,10 +5,15 @@
 import * as React from 'react';
 import { connect } from 'react-redux';
 import { Dispatch } from 'redux';
-import { withStyles, StyleRulesCallback, WithStyles, Chip } from '@material-ui/core';
+import {
+    withStyles,
+    StyleRulesCallback,
+    WithStyles,
+} from '@material-ui/core';
 import { RootState } from '~/store/store';
 import { removePropertyFromCreateProjectForm, PROJECT_CREATE_FORM_SELECTOR, ProjectProperties } from '~/store/projects/project-create-actions';
 import { ArvadosTheme } from '~/common/custom-theme';
+import { getPropertyChip } from '../resource-properties-form/property-chip';
 
 type CssRules = 'tag';
 
@@ -24,7 +29,7 @@ interface CreateProjectPropertiesListDataProps {
 }
 
 interface CreateProjectPropertiesListActionProps {
-    handleDelete: (key: string) => void;
+    handleDelete: (key: string, value: string) => void;
 }
 
 const mapStateToProps = (state: RootState): CreateProjectPropertiesListDataProps => {
@@ -33,21 +38,28 @@ const mapStateToProps = (state: RootState): CreateProjectPropertiesListDataProps
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): CreateProjectPropertiesListActionProps => ({
-    handleDelete: (key: string) => dispatch<any>(removePropertyFromCreateProjectForm(key))
+    handleDelete: (key: string, value: string) => dispatch<any>(removePropertyFromCreateProjectForm(key, value))
 });
 
-type CreateProjectPropertiesListProps = CreateProjectPropertiesListDataProps & 
+type CreateProjectPropertiesListProps = CreateProjectPropertiesListDataProps &
     CreateProjectPropertiesListActionProps & WithStyles<CssRules>;
 
 const List = withStyles(styles)(
     ({ classes, handleDelete, properties }: CreateProjectPropertiesListProps) =>
         <div>
             {properties &&
-                Object.keys(properties).map(k => {
-                    return <Chip key={k} className={classes.tag}
-                        onDelete={() => handleDelete(k)}
-                        label={`${k}: ${properties[k]}`} />;
-                })}
+                Object.keys(properties).map(k =>
+                    Array.isArray(properties[k])
+                    ? (properties[k] as string[]).map((v: string) =>
+                        getPropertyChip(
+                            k, v,
+                            () => handleDelete(k, v),
+                            classes.tag))
+                    : getPropertyChip(
+                        k, (properties[k] as string),
+                        () => handleDelete(k, (properties[k] as string)),
+                        classes.tag))
+                }
         </div>
 );
 
index 1fba8a40a98b1dbdc1a22386a8f10ba41bdb8e58..b9a13fbbca52dbe3ab1f3543eb9f0ae099b33289 100644 (file)
@@ -51,7 +51,7 @@ export const PropertyChipComponent = connect(mapStateToProps, mapDispatchToProps
     }
 );
 
-export const getPropertyChip = (k:string, v:string, handleDelete:any, className:string) =>
+export const getPropertyChip = (k: string, v: string, handleDelete: any, className: string) =>
     <PropertyChipComponent
         key={`${k}-${v}`} className={className}
         onDelete={handleDelete}
index c8d0959a11f2afec7fd288ee83c8b692a53ef0a7..e8d2fc58e565616e30349d331c1528aad498d53d 100644 (file)
@@ -20,7 +20,7 @@ export interface ResourcePropertiesFormData {
 export type ResourcePropertiesFormProps = InjectedFormProps<ResourcePropertiesFormData> & WithStyles<GridClassKey>;
 
 export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, classes }: ResourcePropertiesFormProps ) =>
-    <form data-cy='collection-properties-form' onSubmit={handleSubmit}>
+    <form data-cy='resource-properties-form' onSubmit={handleSubmit}>
         <Grid container spacing={16} classes={classes}>
             <Grid item xs>
                 <PropertyKeyField />
index 685bb78bda561cd90392afc9eecca7078e7b0784..7d54992ebd222eca77df3bfea7f877f258de5e1f 100644 (file)
@@ -32,6 +32,7 @@ import { getProgressIndicator } from '~/store/progress-indicator/progress-indica
 import { COLLECTION_PANEL_LOAD_FILES, loadCollectionFiles, COLLECTION_PANEL_LOAD_FILES_THRESHOLD } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
 import { Link } from 'react-router-dom';
 import { Link as ButtonLink } from '@material-ui/core';
+import { ResourceOwnerWithName } from '~/views-components/data-explorer/renderers';
 
 type CssRules = 'root'
     | 'button'
@@ -297,7 +298,8 @@ export const CollectionDetailsAttributes = (props: { item: CollectionResource, t
         </Grid>
         <Grid item xs={12} md={mdSize}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                label='Owner' linkToUuid={item.ownerUuid} />
+                label='Owner' linkToUuid={item.ownerUuid}
+                uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
         </Grid>
         <Grid item xs={12} md={mdSize}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
@@ -309,7 +311,7 @@ export const CollectionDetailsAttributes = (props: { item: CollectionResource, t
             <DetailsAttribute
                 classLabel={classes.label} classValue={classes.value}
                 label='Version number'
-                value={ showVersionBrowser !== undefined
+                value={showVersionBrowser !== undefined
                     ? <Tooltip title="Open version browser"><ButtonLink underline='none' className={classes.button} onClick={() => showVersionBrowser()}>
                         {<span data-cy='collection-version-number'>{item.version}</span>}
                     </ButtonLink></Tooltip>