[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