[ARVADOS-WORKBENCH2] updated: 2.3.2.1-3-g775d6d6f

Git user git at public.arvados.org
Mon Feb 21 19:49:40 UTC 2022


Summary of changes:
 cypress/integration/collection.spec.js             | 63 +++++++++++++++++---
 cypress/integration/create-workflow.spec.js        |  3 -
 cypress/integration/project.spec.js                | 67 +++++++++++++++++++++-
 cypress/integration/side-panel.spec.js             |  1 -
 .../collection-panel-files.tsx                     | 20 ++-----
 src/index.tsx                                      |  2 +-
 src/models/vocabulary.test.ts                      | 56 ++++++++++++++++--
 src/models/vocabulary.ts                           | 51 ++++++++++++++--
 src/store/advanced-tab/advanced-tab.tsx            |  4 +-
 .../advanced-tab-dialog/advanced-tab-dialog.tsx    |  7 ++-
 .../advanced-tab-dialog/metadataTab.tsx            |  4 +-
 .../property-key-field.tsx                         | 22 +++++--
 .../property-value-field.tsx                       | 13 ++++-
 src/views/collection-panel/collection-panel.tsx    |  2 +-
 src/websocket/websocket.ts                         |  7 +++
 tools/arvados_config.yml                           |  2 +
 16 files changed, 269 insertions(+), 55 deletions(-)

       via  775d6d6fe17c0dafac02d25f515bf8143ac2ef4b (commit)
       via  5eedce18197d1e8299fd89714c61c509b2d68ea9 (commit)
       via  4cfd1b381bd0bd82760ab7062185f8fa5e56ba9a (commit)
      from  6b368635bf1a768e89237c011de76a4230dc9d6d (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 775d6d6fe17c0dafac02d25f515bf8143ac2ef4b
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Fri Feb 18 19:01:55 2022 -0300

    Merge branch '18560-wb2-vocabulary-picking'. Closes #18560
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/create-workflow.spec.js b/cypress/integration/create-workflow.spec.js
index 4da74757..b1ea5dbf 100644
--- a/cypress/integration/create-workflow.spec.js
+++ b/cypress/integration/create-workflow.spec.js
@@ -64,9 +64,6 @@ describe('Multi-file deletion tests', function () {
         cy.get('@testWorkflow').then(() => {
             cy.loginAs(adminUser);
 
-            cy.get('[data-cy=linear-progress]').should('exist');
-            cy.get('[data-cy=linear-progress]').should('not.exist');
-
             cy.get('[data-cy=side-panel-button]').click();
             cy.get('[data-cy=side-panel-run-process]').click();
 
diff --git a/src/models/vocabulary.test.ts b/src/models/vocabulary.test.ts
index 18e2f19f..761c785b 100644
--- a/src/models/vocabulary.test.ts
+++ b/src/models/vocabulary.test.ts
@@ -18,7 +18,8 @@ describe('Vocabulary', () => {
                     strict: false,
                     labels: [
                         {label: "Animal" },
-                        {label: "Creature"}
+                        {label: "Creature"},
+                        {label: "Beast"},
                     ],
                     values: {
                         IDVALANIMALS1: {
@@ -39,13 +40,13 @@ describe('Vocabulary', () => {
                     labels: [{label: "Sizes"}],
                     values: {
                         IDVALSIZES1: {
-                            labels: [{label: "Small"}]
+                            labels: [{label: "Small"}, {label: "S"}, {label: "Little"}]
                         },
                         IDVALSIZES2: {
-                            labels: [{label: "Medium"}]
+                            labels: [{label: "Medium"}, {label: "M"}]
                         },
                         IDVALSIZES3: {
-                            labels: [{label: "Large"}]
+                            labels: [{label: "Large"}, {label: "L"}]
                         },
                         IDVALSIZES4: {
                             labels: []
@@ -61,23 +62,70 @@ describe('Vocabulary', () => {
         // Alphabetically ordered by label
         expect(tagKeys).toEqual([
             {id: "IDKEYANIMALS", label: "Animal"},
+            {id: "IDKEYANIMALS", label: "Beast"},
             {id: "IDKEYANIMALS", label: "Creature"},
             {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT"},
             {id: "IDKEYSIZES", label: "Sizes"},
         ]);
     });
 
+    it('returns the list of preferred tag keys', () => {
+        const preferredTagKeys = Vocabulary.getPreferredTags(vocabulary);
+        // Alphabetically ordered by label
+        expect(preferredTagKeys).toEqual([
+            {id: "IDKEYANIMALS", label: "Animal", synonyms: []},
+            {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT", synonyms: []},
+            {id: "IDKEYSIZES", label: "Sizes", synonyms: []},
+        ]);
+    });
+
+    it('returns the list of preferred tag keys with matching synonyms', () => {
+        const preferredTagKeys = Vocabulary.getPreferredTags(vocabulary, 'creat');
+        // Alphabetically ordered by label
+        expect(preferredTagKeys).toEqual([
+            {id: "IDKEYANIMALS", label: "Animal", synonyms: ["Creature"]},
+            {id: "IDKEYCOMMENT", label: "IDKEYCOMMENT", synonyms: []},
+            {id: "IDKEYSIZES", label: "Sizes", synonyms: []},
+        ]);
+    });
+
     it('returns the tag values for a given key', () => {
         const tagValues = Vocabulary.getTagValues('IDKEYSIZES', vocabulary);
         // Alphabetically ordered by label
         expect(tagValues).toEqual([
             {id: "IDVALSIZES4", label: "IDVALSIZES4"},
+            {id: "IDVALSIZES3", label: "L"},
             {id: "IDVALSIZES3", label: "Large"},
+            {id: "IDVALSIZES1", label: "Little"},
+            {id: "IDVALSIZES2", label: "M"},
             {id: "IDVALSIZES2", label: "Medium"},
+            {id: "IDVALSIZES1", label: "S"},
             {id: "IDVALSIZES1", label: "Small"},
         ])
     });
 
+    it('returns the preferred tag values for a given key', () => {
+        const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary);
+        // Alphabetically ordered by label
+        expect(preferredTagValues).toEqual([
+            {id: "IDVALSIZES4", label: "IDVALSIZES4", synonyms: []},
+            {id: "IDVALSIZES3", label: "Large", synonyms: []},
+            {id: "IDVALSIZES2", label: "Medium", synonyms: []},
+            {id: "IDVALSIZES1", label: "Small", synonyms: []},
+        ])
+    });
+
+    it('returns the preferred tag values with matching synonyms for a given key', () => {
+        const preferredTagValues = Vocabulary.getPreferredTagValues('IDKEYSIZES', vocabulary, 'litt');
+        // Alphabetically ordered by label
+        expect(preferredTagValues).toEqual([
+            {id: "IDVALSIZES4", label: "IDVALSIZES4", synonyms: []},
+            {id: "IDVALSIZES3", label: "Large", synonyms: []},
+            {id: "IDVALSIZES2", label: "Medium", synonyms: []},
+            {id: "IDVALSIZES1", label: "Small", synonyms: ["Little"]},
+        ])
+    });
+
     it('returns an empty list of values for an non-existent key', () => {
         const tagValues = Vocabulary.getTagValues('IDNONSENSE', vocabulary);
         expect(tagValues).toEqual([]);
diff --git a/src/models/vocabulary.ts b/src/models/vocabulary.ts
index 3c542844..6c629059 100644
--- a/src/models/vocabulary.ts
+++ b/src/models/vocabulary.ts
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { escapeRegExp } from 'common/regexp';
 import { isObject, has, every } from 'lodash/fp';
 
 export interface Vocabulary {
@@ -27,6 +28,7 @@ export interface Tag {
 export interface PropFieldSuggestion {
     id: string;
     label: string;
+    synonyms?: string[];
 }
 
 const VOCABULARY_VALIDATORS = [
@@ -64,9 +66,9 @@ const compare = (a: PropFieldSuggestion, b: PropFieldSuggestion) => {
     return 0;
 };
 
-export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => {
+export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary): PropFieldSuggestion[] => {
     const tag = vocabulary.tags[tagKeyID];
-    const ret = tag && tag.values
+    return tag && tag.values
         ? Object.keys(tag.values).map(
             tagValueID => tag.values![tagValueID].labels && tag.values![tagValueID].labels.length > 0
                 ? tag.values![tagValueID].labels.map(
@@ -75,11 +77,30 @@ export const getTagValues = (tagKeyID: string, vocabulary: Vocabulary) => {
             .reduce((prev, curr) => [...prev, ...curr], [])
             .sort(compare)
         : [];
-    return ret;
 };
 
-export const getTags = ({ tags }: Vocabulary) => {
-    const ret = tags && Object.keys(tags)
+export const getPreferredTagValues = (tagKeyID: string, vocabulary: Vocabulary, withMatch?: string): PropFieldSuggestion[] => {
+    const tag = vocabulary.tags[tagKeyID];
+    const regex = !!withMatch ? new RegExp(escapeRegExp(withMatch), 'i') : undefined;
+    return tag && tag.values
+        ? Object.keys(tag.values).map(
+            tagValueID => tag.values![tagValueID].labels && tag.values![tagValueID].labels.length > 0
+                ? {
+                    "id": tagValueID,
+                    "label": tag.values![tagValueID].labels[0].label,
+                    "synonyms": !!withMatch && tag.values![tagValueID].labels.length > 1
+                        ? tag.values![tagValueID].labels.slice(1)
+                            .filter(l => !!regex ? regex.test(l.label) : true)
+                            .map(l => l.label)
+                        : []
+                }
+                : {"id": tagValueID, "label": tagValueID, "synonyms": []})
+            .sort(compare)
+        : [];
+};
+
+export const getTags = ({ tags }: Vocabulary): PropFieldSuggestion[] => {
+    return tags && Object.keys(tags)
         ? Object.keys(tags).map(
             tagID => tags[tagID].labels && tags[tagID].labels.length > 0
                 ? tags[tagID].labels.map(
@@ -88,7 +109,25 @@ export const getTags = ({ tags }: Vocabulary) => {
             .reduce((prev, curr) => [...prev, ...curr], [])
             .sort(compare)
         : [];
-    return ret;
+};
+
+export const getPreferredTags = ({ tags }: Vocabulary, withMatch?: string): PropFieldSuggestion[] => {
+    const regex = !!withMatch ? new RegExp(escapeRegExp(withMatch), 'i') : undefined;
+    return tags && Object.keys(tags)
+        ? Object.keys(tags).map(
+            tagID => tags[tagID].labels && tags[tagID].labels.length > 0
+                ? {
+                    "id": tagID,
+                    "label": tags[tagID].labels[0].label,
+                    "synonyms": !!withMatch && tags[tagID].labels.length > 1
+                        ? tags[tagID].labels.slice(1)
+                                .filter(l => !!regex ? regex.test(l.label) : true)
+                                .map(lbl => lbl.label)
+                        : []
+                }
+                : {"id": tagID, "label": tagID, "synonyms": []})
+            .sort(compare)
+        : [];
 };
 
 export const getTagKeyID = (tagKeyLabel:string, vocabulary: Vocabulary) =>
diff --git a/src/views-components/resource-properties-form/property-key-field.tsx b/src/views-components/resource-properties-form/property-key-field.tsx
index 791949f5..0be4527a 100644
--- a/src/views-components/resource-properties-form/property-key-field.tsx
+++ b/src/views-components/resource-properties-form/property-key-field.tsx
@@ -6,7 +6,14 @@ import React from 'react';
 import { WrappedFieldProps, Field, FormName, reset, change, WrappedFieldInputProps, WrappedFieldMetaProps } from 'redux-form';
 import { memoize } from 'lodash';
 import { Autocomplete } from 'components/autocomplete/autocomplete';
-import { Vocabulary, getTags, getTagKeyID, getTagKeyLabel } from 'models/vocabulary';
+import {
+    Vocabulary,
+    getTags,
+    getTagKeyID,
+    getTagKeyLabel,
+    getPreferredTags,
+    PropFieldSuggestion
+} from 'models/vocabulary';
 import {
     handleSelect,
     handleBlur,
@@ -36,8 +43,14 @@ export const PropertyKeyField = connectVocabulary(
 const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) =>
     <FormName children={data => (
         <Autocomplete
+            {...buildProps(props)}
             label='Key'
             suggestions={getSuggestions(props.input.value, vocabulary)}
+            renderSuggestion={
+                (s: PropFieldSuggestion) => s.synonyms && s.synonyms.length > 0
+                    ? `${s.label} (${s.synonyms.join('; ')})`
+                    : s.label
+            }
             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
             onBlur={() => {
                 // Case-insensitive search for the key in the vocabulary
@@ -51,7 +64,6 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula
                 const newValue = e.currentTarget.value;
                 handleChange(data.form, props.input, props.meta, newValue);
             }}
-            {...buildProps(props)}
         />
     )} />;
 
@@ -67,9 +79,11 @@ const matchTags = (vocabulary: Vocabulary) =>
             ? undefined
             : 'Incorrect key';
 
-const getSuggestions = (value: string, vocabulary: Vocabulary) => {
+const getSuggestions = (value: string, vocabulary: Vocabulary): PropFieldSuggestion[] => {
     const re = new RegExp(escapeRegExp(value), "i");
-    return getTags(vocabulary).filter(tag => re.test(tag.label) && tag.label !== value);
+    return getPreferredTags(vocabulary, value).filter(
+        tag => (tag.label !== value && re.test(tag.label)) ||
+            (tag.synonyms && tag.synonyms.some(s => re.test(s))));
 };
 
 const handleChange = (
diff --git a/src/views-components/resource-properties-form/property-value-field.tsx b/src/views-components/resource-properties-form/property-value-field.tsx
index b023e412..b8e525bf 100644
--- a/src/views-components/resource-properties-form/property-value-field.tsx
+++ b/src/views-components/resource-properties-form/property-value-field.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 import { WrappedFieldProps, Field, formValues, FormName, WrappedFieldInputProps, WrappedFieldMetaProps, change } from 'redux-form';
 import { compose } from 'redux';
 import { Autocomplete } from 'components/autocomplete/autocomplete';
-import { Vocabulary, isStrictTag, getTagValues, getTagValueID, getTagValueLabel } from 'models/vocabulary';
+import { Vocabulary, isStrictTag, getTagValues, getTagValueID, getTagValueLabel, PropFieldSuggestion, getPreferredTagValues } from 'models/vocabulary';
 import { PROPERTY_KEY_FIELD_ID, PROPERTY_KEY_FIELD_NAME } from 'views-components/resource-properties-form/property-key-field';
 import {
     handleSelect,
@@ -56,9 +56,15 @@ export const PropertyValueField = connectVocabularyAndPropertyKey(
 const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...props }: WrappedFieldProps & PropertyValueFieldProps) =>
     <FormName children={data => (
         <Autocomplete
+            {...buildProps(props)}
             label='Value'
             disabled={props.disabled}
             suggestions={getSuggestions(props.input.value, propertyKeyId, vocabulary)}
+            renderSuggestion={
+                (s: PropFieldSuggestion) => s.synonyms && s.synonyms.length > 0
+                    ? `${s.label} (${s.synonyms.join('; ')})`
+                    : s.label
+            }
             onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
             onBlur={() => {
                 // Case-insensitive search for the value in the vocabulary
@@ -73,7 +79,6 @@ const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...pro
                 const tagValueID = getTagValueID(propertyKeyId, newValue, vocabulary);
                 handleChange(data.form, tagValueID, props.input, props.meta, newValue);
             }}
-            {...buildProps(props)}
         />
     )} />;
 
@@ -90,7 +95,9 @@ const matchTagValues = ({ vocabulary, propertyKeyId }: PropertyValueFieldProps)
 
 const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary) => {
     const re = new RegExp(escapeRegExp(value), "i");
-    return getTagValues(tagName, vocabulary).filter(v => re.test(v.label) && v.label !== value);
+    return getPreferredTagValues(tagName, vocabulary, value).filter(
+        val => (val.label !== value && re.test(val.label)) ||
+            (val.synonyms && val.synonyms.some(s => re.test(s))));
 };
 
 const handleChange = (

commit 5eedce18197d1e8299fd89714c61c509b2d68ea9
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Wed Feb 16 17:36:47 2022 -0300

    Merge branch '18315-collection-panel-refresh'. Closes #18315
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index bd211b1a..f91dbb5b 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -642,6 +642,34 @@ describe('Collection panel tests', function () {
             });
     });
 
+    it('automatically updates the collection UI contents without using the Refresh button', function () {
+        const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
+        const fileName = 'foobar'
+
+        cy.createCollection(adminUser.token, {
+            name: collName,
+            owner_uuid: activeUser.user.uuid,
+        }).as('testCollection');
+
+        cy.getAll('@testCollection').then(function ([testCollection]) {
+            cy.loginAs(activeUser);
+            cy.goToPath(`/collections/${testCollection.uuid}`);
+            cy.get('[data-cy=collection-files-panel]').should('contain', 'This collection is empty');
+            cy.get('[data-cy=collection-files-panel]').should('not.contain', fileName);
+            cy.get('[data-cy=collection-info-panel]').should('contain', collName);
+
+            cy.updateCollection(adminUser.token, testCollection.uuid, {
+                name: `${collName + ' updated'}`,
+                manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
+            }).as('updatedCollection');
+            cy.getAll('@updatedCollection').then(function ([updatedCollection]) {
+                expect(updatedCollection.name).to.equal(`${collName + ' updated'}`);
+                cy.get('[data-cy=collection-info-panel]').should('contain', updatedCollection.name);
+                cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
+            });
+        });
+    })
+
     it('makes a copy of an existing collection', function() {
         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
         const copyName = `Copy of: ${collName}`;
@@ -848,17 +876,15 @@ describe('Collection panel tests', function () {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
             owner_uuid: activeUser.user.uuid,
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-        })
-            .as('testCollection1');
+        }).as('testCollection1');
 
         cy.createCollection(adminUser.token, {
             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
             owner_uuid: adminUser.user.uuid,
             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-        })
-            .as('testCollection2').then(function (testCollection2) {
-                cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, 'can_write');
-            });
+        }).as('testCollection2').then(function (testCollection2) {
+            cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, 'can_write');
+        });
 
         cy.getAll('@testCollection1', '@testCollection2')
             .then(function ([testCollection1, testCollection2]) {
@@ -880,8 +906,29 @@ describe('Collection panel tests', function () {
                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
                 owner_uuid: activeUser.user.uuid,
                 manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
-            })
-                .as('testCollection1');
+            }).as('testCollection1');
+        });
+
+        it('uploads a file and checks the collection UI to be fresh', () => {
+            cy.getAll('@testCollection1')
+                .then(function([testCollection1]) {
+                    cy.loginAs(activeUser);
+                    cy.goToPath(`/collections/${testCollection1.uuid}`);
+                    cy.get('[data-cy=upload-button]').click();
+                    cy.get('[data-cy=collection-files-panel]')
+                        .contains('5mb_a.bin').should('not.exist');
+                    cy.get('[data-cy=collection-file-count]').should('contain', '1');
+                    cy.fixture('files/5mb.bin', 'base64').then(content => {
+                        cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
+                        cy.get('[data-cy=form-submit-btn]').click();
+                        cy.get('[data-cy=form-submit-btn]').should('not.exist');
+                    });
+                    // Confirm that the file browser has been updated.
+                    cy.get('[data-cy=collection-files-panel]')
+                        .contains('5mb_a.bin').should('exist');
+                    // Confirm that the collection panel has been updated.
+                    cy.get('[data-cy=collection-file-count]').should('contain', '2');
+                });
         });
 
         it('allows to cancel running upload', () => {
diff --git a/cypress/integration/side-panel.spec.js b/cypress/integration/side-panel.spec.js
index f9d4dca3..78087386 100644
--- a/cypress/integration/side-panel.spec.js
+++ b/cypress/integration/side-panel.spec.js
@@ -82,7 +82,6 @@ describe('Side panel tests', function() {
             group_class: 'filter',
             properties: {filters: []},
         }).as('myFavoriteFilterGroup').then(function (myFavoriteFilterGroup) {
-            cy.contains('Refresh').click();
             cy.goToPath(`/projects/${myFavoriteFilterGroup.uuid}`);
             cy.get('[data-cy=breadcrumb-last]').should('contain', 'my-favorite-filter-group');
 
diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx
index a7001a61..dd28d0fc 100644
--- a/src/components/collection-panel-files/collection-panel-files.tsx
+++ b/src/components/collection-panel-files/collection-panel-files.tsx
@@ -202,7 +202,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
     const [path, setPath]: any = React.useState([]);
     const [pathData, setPathData]: any = React.useState({});
     const [isLoading, setIsLoading] = React.useState(false);
-    const [collectionAutofetchEnabled, setCollectionAutofetchEnabled] = React.useState(false);
     const [leftSearch, setLeftSearch] = React.useState('');
     const [rightSearch, setRightSearch] = React.useState('');
 
@@ -279,13 +278,12 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
         }
     }, [rightKey]); // eslint-disable-line react-hooks/exhaustive-deps
 
+    const currentPDH = (collectionPanel.item || {}).portableDataHash;
     React.useEffect(() => {
-        const hash = (collectionPanel.item || {}).portableDataHash;
-
-        if (hash && collectionAutofetchEnabled) {
+        if (currentPDH) {
             fetchData([leftKey, rightKey], true);
         }
-    }, [(collectionPanel.item || {}).portableDataHash]); // eslint-disable-line react-hooks/exhaustive-deps
+    }, [currentPDH]); // eslint-disable-line react-hooks/exhaustive-deps
 
     React.useEffect(() => {
         if (rightData) {
@@ -316,10 +314,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
 
             if (id) {
                 onItemMenuOpen(event, item, isWritable);
-
-                if (!collectionAutofetchEnabled) {
-                    setCollectionAutofetchEnabled(true);
-                }
             }
         },
         [onItemMenuOpen, isWritable, rightData] // eslint-disable-line react-hooks/exhaustive-deps
@@ -446,9 +440,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                     <IconButton
                         data-cy='collection-files-panel-options-btn'
                         onClick={(ev) => {
-                            if (!collectionAutofetchEnabled) {
-                                setCollectionAutofetchEnabled(true);
-                            }
                             onOptionsMenuOpen(ev, isWritable);
                         }}>
                         <CustomizeTableIcon />
@@ -518,9 +509,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                             className={classes.uploadButton}
                             data-cy='upload-button'
                             onClick={() => {
-                                if (!collectionAutofetchEnabled) {
-                                    setCollectionAutofetchEnabled(true);
-                                }
                                 onUploadDataClick();
                             }}
                             variant='contained'
@@ -568,7 +556,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
                                                     </div>
                                                 }
                                             }
-                                        </FixedSizeList> : <div className={classes.rowEmpty}>No data available</div>
+                                        </FixedSizeList> : <div className={classes.rowEmpty}>This collection is empty</div>
                                     }}
                                 </AutoSizer> : <div className={classes.row}><CircularProgress className={classes.loader} size={30} /></div>
                         }
diff --git a/src/index.tsx b/src/index.tsx
index 6ad22a55..b75b325b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -181,7 +181,7 @@ const initListener = (history: History, store: RootStore, services: ServiceRepos
     let initialized = false;
     return async () => {
         const { router, auth } = store.getState();
-        if (router.location && auth.user && !initialized) {
+        if (router.location && auth.user && services.authService.getApiToken() && !initialized) {
             initialized = true;
             initWebSocket(config, services.authService, store);
             await store.dispatch(loadWorkbench());
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index e78b1f3d..4a3cb4d3 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -336,7 +336,7 @@ export const CollectionDetailsAttributes = (props: { item: CollectionResource, t
         </Grid>
         <Grid item xs={12} md={mdSize}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                label='Number of files' value={item.fileCount} />
+                label='Number of files' value={<span data-cy='collection-file-count'>{item.fileCount}</span>} />
         </Grid>
         <Grid item xs={12} md={mdSize}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
diff --git a/src/websocket/websocket.ts b/src/websocket/websocket.ts
index b1265808..7c8e0171 100644
--- a/src/websocket/websocket.ts
+++ b/src/websocket/websocket.ts
@@ -15,6 +15,7 @@ import { subprocessPanelActions } from "store/subprocess-panel/subprocess-panel-
 import { projectPanelActions } from "store/project-panel/project-panel-action";
 import { getProjectPanelCurrentUuid } from 'store/project-panel/project-panel-action';
 import { allProcessesPanelActions } from 'store/all-processes-panel/all-processes-panel-action';
+import { loadCollection } from 'store/workbench/workbench-actions';
 
 export const initWebSocket = (config: Config, authService: AuthService, store: RootStore) => {
     if (config.websocketUrl) {
@@ -29,6 +30,12 @@ export const initWebSocket = (config: Config, authService: AuthService, store: R
 const messageListener = (store: RootStore) => (message: ResourceEventMessage) => {
     if (message.eventType === LogEventType.CREATE || message.eventType === LogEventType.UPDATE) {
         switch (message.objectKind) {
+            case ResourceKind.COLLECTION:
+                const currentCollection = store.getState().collectionPanel.item;
+                if (currentCollection && currentCollection.uuid === message.objectUuid) {
+                    store.dispatch(loadCollection(message.objectUuid));
+                }
+                return;
             case ResourceKind.CONTAINER_REQUEST:
                 if (store.getState().processPanel.containerRequestUuid === message.objectUuid) {
                     store.dispatch(loadProcess(message.objectUuid));
diff --git a/tools/arvados_config.yml b/tools/arvados_config.yml
index 55dc8a02..b9bcfbe0 100644
--- a/tools/arvados_config.yml
+++ b/tools/arvados_config.yml
@@ -15,6 +15,8 @@ Clusters:
       ForwardSlashNameSubstitution: /
       ManagedProperties:
         original_owner_uuid: {Function: original_owner, Protected: true}
+      WebDAVCache:
+        UUIDTTL: 0s
     Login:
       PAM:
         Enable: true

commit 4cfd1b381bd0bd82760ab7062185f8fa5e56ba9a
Author: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
Date:   Wed Feb 16 16:02:09 2022 +0100

    Merge branch '18594-Collection-Advanced-Menu-is-trying-to-fetch-User-record-with-collection-UUID' into main
    closes #18594
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla at contractors.roche.com>

diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index 1c175952..8372132a 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -194,4 +194,69 @@ describe('Project tests', function() {
             cy.contains(testRootProject.uuid).should('exist');
         });
     });
-});
\ No newline at end of file
+
+    it('clears search input when changing project', () => {
+        cy.createGroup(activeUser.token, {
+            name: `Test root project ${Math.floor(Math.random() * 999999)}`,
+            group_class: 'project',
+        }).as('testProject1').then((testProject1) => {
+            cy.shareWith(adminUser.token, activeUser.user.uuid, testProject1.uuid, 'can_write');
+        });
+
+        cy.getAll('@testProject1').then(function([testProject1]) {
+            cy.loginAs(activeUser);
+
+            cy.get('[data-cy=side-panel-tree]').contains(testProject1.name).click();
+
+            cy.get('[data-cy=search-input] input').type('test123');
+
+            cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+
+            cy.get('[data-cy=search-input] input').should('not.have.value', 'test123');
+        });
+    });
+
+    it('opens advanced popup for project with username', () => {
+        const projectName = `Test project ${Math.floor(Math.random() * 999999)}`;
+
+        cy.createGroup(adminUser.token, {
+            name: projectName,
+            group_class: 'project',
+        }).as('mainProject')
+
+        cy.getAll('@mainProject')
+            .then(function ([mainProject]) {
+                cy.loginAs(adminUser);
+
+                cy.get('[data-cy=side-panel-tree]').contains('Groups').click();
+
+                cy.get('[data-cy=uuid]').eq(0).invoke('text').then(uuid => {
+                    cy.createLink(adminUser.token, {
+                        name: 'can_write',
+                        link_class: 'permission',
+                        head_uuid: mainProject.uuid,
+                        tail_uuid: uuid
+                    });
+
+                    cy.createLink(adminUser.token, {
+                        name: 'can_write',
+                        link_class: 'permission',
+                        head_uuid: mainProject.uuid,
+                        tail_uuid: activeUser.user.uuid
+                    });
+
+                    cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+
+                    cy.get('main').contains(projectName).rightclick();
+
+                    cy.get('[data-cy=context-menu]').contains('Advanced').click();
+
+                    cy.get('[role=tablist]').contains('METADATA').click();
+
+                    cy.get('td').contains(uuid).should('exist');
+
+                    cy.get('td').contains(activeUser.user.uuid).should('exist');
+                });
+        });
+    });
+});
diff --git a/src/store/advanced-tab/advanced-tab.tsx b/src/store/advanced-tab/advanced-tab.tsx
index 25d90195..61fd705a 100644
--- a/src/store/advanced-tab/advanced-tab.tsx
+++ b/src/store/advanced-tab/advanced-tab.tsx
@@ -280,8 +280,8 @@ const getDataForAdvancedTab = (uuid: string) =>
                 .addEqual('head_uuid', uuid)
                 .getFilters()
         });
-        const user = metadata.itemsAvailable && await services.userService.get(metadata.items[0].tailUuid || '');
-        return { data, metadata, user };
+
+        return { data, metadata };
     };
 
 const initAdvancedTabDialog = (data: AdvancedTabDialogData) => dialogActions.OPEN_DIALOG({ id: ADVANCED_TAB_DIALOG, data });
diff --git a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
index b631a74c..f493df33 100644
--- a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
+++ b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
@@ -61,7 +61,6 @@ export const AdvancedTabDialog = compose(
                 curlHeader,
                 curlExample,
                 uuid,
-                user
             } = this.props.data;
             return <Dialog
                 open={open}
@@ -78,7 +77,11 @@ export const AdvancedTabDialog = compose(
                 </Tabs>
                 <DialogContent className={classes.content}>
                     {value === 0 && <div>{dialogContentExample(apiResponse, classes)}</div>}
-                    {value === 1 && <div>{metadata !== '' && metadata.items.length > 0 ? <MetadataTab items={metadata.items} uuid={uuid} user={user} /> : dialogContentHeader('(No metadata links found)')}</div>}
+                    {value === 1 && <div>
+                        {metadata !== '' && metadata.items.length > 0 ?
+                            <MetadataTab items={metadata.items} uuid={uuid} />
+                            : dialogContentHeader('(No metadata links found)')}
+                    </div>}
                     {value === 2 && dialogContent(pythonHeader, pythonExample, classes)}
                     {value === 3 && <div>
                         {dialogContent(cliGetHeader, cliGetExample, classes)}
diff --git a/src/views-components/advanced-tab-dialog/metadataTab.tsx b/src/views-components/advanced-tab-dialog/metadataTab.tsx
index 9f08d1e3..1b950d24 100644
--- a/src/views-components/advanced-tab-dialog/metadataTab.tsx
+++ b/src/views-components/advanced-tab-dialog/metadataTab.tsx
@@ -4,7 +4,6 @@
 
 import React from "react";
 import { Table, TableHead, TableCell, TableRow, TableBody, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-import { UserResource, getUserDisplayName } from "models/user";
 
 type CssRules = 'cell';
 
@@ -25,7 +24,6 @@ interface MetadataTable {
 
 interface MetadataProps {
     items: MetadataTable[];
-    user: UserResource;
     uuid: string;
 }
 
@@ -47,7 +45,7 @@ export const MetadataTab = withStyles(styles)((props: MetadataProps & WithStyles
                     <TableCell className={props.classes.cell}>{it.uuid}</TableCell>
                     <TableCell className={props.classes.cell}>{it.linkClass}</TableCell>
                     <TableCell className={props.classes.cell}>{it.name}</TableCell>
-                    <TableCell className={props.classes.cell}>{props.user && `User: ${getUserDisplayName(props.user)}`}</TableCell>
+                    <TableCell className={props.classes.cell}>{it.tailUuid}</TableCell>
                     <TableCell className={props.classes.cell}>{it.headUuid === props.uuid ? 'this' : it.headUuid}</TableCell>
                     <TableCell className={props.classes.cell}>{JSON.stringify(it.properties)}</TableCell>
                 </TableRow>

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list