Merge branch '17944-vocabulary-endpoint-retrieval' into main. Closes #17944
authorLucas Di Pentima <lucas.dipentima@curii.com>
Thu, 11 Nov 2021 17:53:00 +0000 (14:53 -0300)
committerLucas Di Pentima <lucas.dipentima@curii.com>
Thu, 11 Nov 2021 17:53:00 +0000 (14:53 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima@curii.com>

README.md
cypress/integration/collection.spec.js
src/common/config.ts
src/models/vocabulary.ts
src/services/vocabulary-service/vocabulary-service.ts
src/store/vocabulary/vocabulary-actions.ts
src/views-components/resource-properties-form/property-key-field.tsx
src/views-components/resource-properties-form/property-value-field.tsx
tools/arvados_config.yml
tools/example-vocabulary.json [moved from public/vocabulary-example.json with 100% similarity]
tools/run-integration-tests.sh

index 8bb50dbeb91c90aa06c801e8d036fa74a31578ea..4ec4bd1cf8418b02b62bd31f5058aba22d27536f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -82,7 +82,6 @@ Currently this configuration schema is supported:
 ```
 {
     "API_HOST": "string",
-    "VOCABULARY_URL": "string",
     "FILE_VIEWERS_CONFIG_URL": "string",
 }
 ```
@@ -93,12 +92,6 @@ The Arvados base URL.
 
 The `REACT_APP_ARVADOS_API_HOST` environment variable can be used to set the default URL if the run time configuration is unreachable.
 
-### VOCABULARY_URL
-Local path, or any URL that allows cross-origin requests. See
-[Vocabulary JSON file example](public/vocabulary-example.json).
-
-To use the URL defined in the Arvados cluster configuration, remove the entire `VOCABULARY_URL` entry from the runtime configuration. Found in `/config.json` by default.
-
 ## FILE_VIEWERS_CONFIG_URL
 Local path, or any URL that allows cross-origin requests. See:
 
index fb126af6a2a3577926cacb8ec659412f554aa8dd..3e06d7e5a0b2bb03b3de36e2cecfe1241b4dbdd0 100644 (file)
@@ -97,14 +97,37 @@ describe('Collection panel tests', function () {
                 });
                 // Confirm proper vocabulary labels are displayed on the UI.
                 cy.get('[data-cy=collection-properties-panel]')
-                    .should('contain', 'Color')
-                    .and('contain', 'Magenta');
+                    .should('contain', 'Color: Magenta');
                 // Confirm proper vocabulary IDs were saved on the backend.
                 cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
                     .its('body').as('collection')
                     .then(function () {
                         expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3');
                     });
+
+                // Case-insensitive on-blur auto-selection test
+                // Key: Size (IDTAGSIZES) - Value: Small (IDVALSIZES2)
+                cy.get('[data-cy=resource-properties-form]').within(() => {
+                    cy.get('[data-cy=property-field-key]').within(() => {
+                        cy.get('input').type('sIzE');
+                    });
+                    cy.get('[data-cy=property-field-value]').within(() => {
+                        cy.get('input').type('sMaLL');
+                    });
+                    // Cannot "type()" TAB on Cypress so let's click another field
+                    // to trigger the onBlur event.
+                    cy.get('[data-cy=property-field-key]').click();
+                    cy.root().submit();
+                });
+                // Confirm proper vocabulary labels are displayed on the UI.
+                cy.get('[data-cy=collection-properties-panel]')
+                    .should('contain', 'Size: S');
+                // Confirm proper vocabulary IDs were saved on the backend.
+                cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
+                    .its('body').as('collection')
+                    .then(function () {
+                        expect(this.collection.properties.IDTAGSIZES).to.equal('IDVALSIZES2');
+                    });
             });
     });
 
index 56f7c4884b19542ee057b937b5ff420809477a9e..2518c95eda2e799a9156388f8f4d6f46a628203b 100644 (file)
@@ -51,7 +51,6 @@ export interface ClusterConfigJSON {
     };
     Workbench: {
         ArvadosDocsite: string;
-        VocabularyURL: string;
         FileViewersConfigURL: string;
         WelcomePageHTML: string;
         InactivePageHTML: string;
@@ -204,15 +203,10 @@ remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`);
                 }
                 config.fileViewersConfigUrl = fileViewerConfigUrl;
 
-                let vocabularyUrl;
                 if (workbenchConfig.VOCABULARY_URL !== undefined) {
-                    warnLocalConfig("VOCABULARY_URL");
-                    vocabularyUrl = workbenchConfig.VOCABULARY_URL;
+                    console.warn(`A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`)
                 }
-                else {
-                    vocabularyUrl = config.clusterConfig.Workbench.VocabularyURL || "/vocabulary-example.json";
-                }
-                config.vocabularyUrl = vocabularyUrl;
+                config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
 
                 return { config, apiHost: workbenchConfig.API_HOST };
             });
@@ -240,7 +234,6 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
     },
     Workbench: {
         ArvadosDocsite: "",
-        VocabularyURL: "",
         FileViewersConfigURL: "",
         WelcomePageHTML: "",
         InactivePageHTML: "",
@@ -315,5 +308,7 @@ const getDefaultConfig = (): WorkbenchConfig => {
 
 export const ARVADOS_API_PATH = "arvados/v1";
 export const CLUSTER_CONFIG_PATH = "arvados/v1/config";
+export const VOCABULARY_PATH = "arvados/v1/vocabulary";
 export const DISCOVERY_DOC_PATH = "discovery/v1/apis/arvados/v1/rest";
-export const getClusterConfigURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`;
+export const getClusterConfigURL = (apiHost: string) => `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`;
+export const getVocabularyURL = (apiHost: string) => `https://${apiHost}/${VOCABULARY_PATH}?nocache=${(new Date()).getTime()}`;
index 03f28c07bf9c5edc21de1e1996c52f28156c5f89..3c5428446cf589ec183a29793a6a0bbf219f48fe 100644 (file)
@@ -47,7 +47,7 @@ export const getTagValueID = (tagKeyID:string, tagValueLabel:string, vocabulary:
     (tagKeyID && vocabulary.tags[tagKeyID] && vocabulary.tags[tagKeyID].values)
     ? Object.keys(vocabulary.tags[tagKeyID].values!).find(
         k => vocabulary.tags[tagKeyID].values![k].labels.find(
-            l => l.label === tagValueLabel) !== undefined) || ''
+            l => l.label.toLowerCase() === tagValueLabel.toLowerCase()) !== undefined) || ''
     : '';
 
 export const getTagValueLabel = (tagKeyID:string, tagValueID:string, vocabulary: Vocabulary) =>
@@ -94,7 +94,7 @@ export const getTags = ({ tags }: Vocabulary) => {
 export const getTagKeyID = (tagKeyLabel:string, vocabulary: Vocabulary) =>
     Object.keys(vocabulary.tags).find(
         k => vocabulary.tags[k].labels.find(
-            l => l.label === tagKeyLabel) !== undefined
+            l => l.label.toLowerCase() === tagKeyLabel.toLowerCase()) !== undefined
         ) || '';
 
 export const getTagKeyLabel = (tagKeyID:string, vocabulary: Vocabulary) =>
index ff2de1596443e14f8b818407f1632ba095c3880b..38163f77860d2d41af57c1526df3f752009df4a7 100644 (file)
@@ -10,9 +10,9 @@ export class VocabularyService {
         private url: string
     ) { }
 
-    getVocabulary() {
-        return Axios
-            .get<Vocabulary>(this.url)
-            .then(response => response.data);
+    async getVocabulary() {
+        const response = await Axios
+            .get<Vocabulary>(this.url);
+        return response.data;
     }
 }
index 2ca344bb2cf28883355802dee8734141e959484c..d73c01fe909f14aedc28d8a25b6a7229c7ae8afe 100644 (file)
@@ -10,7 +10,6 @@ import { isVocabulary } from 'models/vocabulary';
 
 export const loadVocabulary = async (dispatch: Dispatch, _: {}, { vocabularyService }: ServiceRepository) => {
     const vocabulary = await vocabularyService.getVocabulary();
-
     dispatch(propertiesActions.SET_PROPERTY({
         key: VOCABULARY_PROPERTY_NAME,
         value: isVocabulary(vocabulary)
index 029d44cc130d40ea7ac83d8d74f573964b80dd50..791949f543fab120457550a0781fa40068127357 100644 (file)
@@ -6,7 +6,7 @@ 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 } from 'models/vocabulary';
+import { Vocabulary, getTags, getTagKeyID, getTagKeyLabel } from 'models/vocabulary';
 import {
     handleSelect,
     handleBlur,
@@ -39,7 +39,14 @@ const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & Vocabula
             label='Key'
             suggestions={getSuggestions(props.input.value, vocabulary)}
             onSelect={handleSelect(PROPERTY_KEY_FIELD_ID, data.form, props.input, props.meta)}
-            onBlur={handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, getTagKeyID(props.input.value, vocabulary))}
+            onBlur={() => {
+                // Case-insensitive search for the key in the vocabulary
+                const foundKeyID = getTagKeyID(props.input.value, vocabulary);
+                if (foundKeyID !== '') {
+                    props.input.value = getTagKeyLabel(foundKeyID, vocabulary);
+                }
+                handleBlur(PROPERTY_KEY_FIELD_ID, data.form, props.meta, props.input, foundKeyID)();
+            }}
             onChange={(e: ChangeEvent<HTMLInputElement>) => {
                 const newValue = e.currentTarget.value;
                 handleChange(data.form, props.input, props.meta, newValue);
index a2b53b3cd0281533b2860cbb7117bd75bb74f591..b023e412ff533571bfae33443d8061ca9f3201cc 100644 (file)
@@ -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 } from 'models/vocabulary';
+import { Vocabulary, isStrictTag, getTagValues, getTagValueID, getTagValueLabel } from 'models/vocabulary';
 import { PROPERTY_KEY_FIELD_ID, PROPERTY_KEY_FIELD_NAME } from 'views-components/resource-properties-form/property-key-field';
 import {
     handleSelect,
@@ -60,7 +60,14 @@ const PropertyValueInput = ({ vocabulary, propertyKeyId, propertyKeyName, ...pro
             disabled={props.disabled}
             suggestions={getSuggestions(props.input.value, propertyKeyId, vocabulary)}
             onSelect={handleSelect(PROPERTY_VALUE_FIELD_ID, data.form, props.input, props.meta)}
-            onBlur={handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, getTagValueID(propertyKeyId, props.input.value, vocabulary))}
+            onBlur={() => {
+                // Case-insensitive search for the value in the vocabulary
+                const foundValueID =  getTagValueID(propertyKeyId, props.input.value, vocabulary);
+                if (foundValueID !== '') {
+                    props.input.value = getTagValueLabel(propertyKeyId, foundValueID, vocabulary);
+                }
+                handleBlur(PROPERTY_VALUE_FIELD_ID, data.form, props.meta, props.input, foundValueID)();
+            }}
             onChange={(e: ChangeEvent<HTMLInputElement>) => {
                 const newValue = e.currentTarget.value;
                 const tagValueID = getTagValueID(propertyKeyId, newValue, vocabulary);
index 369046e68c0e2ff04a458c992faa472bab88b821..55dc8a020b2361769d85a1155af946cbd5c34bc5 100644 (file)
@@ -4,6 +4,7 @@ Clusters:
     SystemRootToken: systemusertesttoken1234567890aoeuidhtnsqjkxbmwvzpy
     API:
       RequestTimeout: 30s
+      VocabularyPath: ""
     TLS:
       Insecure: true
     Collections:
index 159bfc1cb3abf5439adc94a3141e0738718ca3e2..bf4c3ba4c1478c0d315df52992464ee5fee48ed5 100755 (executable)
@@ -70,6 +70,7 @@ echo "ARVADOS_DIR is ${ARVADOS_DIR}"
 
 ARVADOS_LOG=${ARVADOS_DIR}/arvados.log
 ARVADOS_CONF=${WB2_DIR}/tools/arvados_config.yml
+VOCABULARY_CONF=${WB2_DIR}/tools/example-vocabulary.json
 
 if [ ! -f "${WB2_DIR}/src/index.tsx" ]; then
     echo "ERROR: '${WB2_DIR}' isn't workbench2's directory"
@@ -104,6 +105,9 @@ echo "Installing dev dependencies..."
 ~/go/bin/arvados-server install -type test || exit 1
 
 echo "Launching arvados in test mode..."
+VOC_DIR=$(mktemp -d | cut -d \/ -f3) # Removes the /tmp/ part
+cp ${VOCABULARY_CONF} /tmp/${VOC_DIR}/voc.json
+sed -i "s/VocabularyPath: \".*\"/VocabularyPath: \"\/tmp\/${VOC_DIR}\/voc.json\"/" ${ARVADOS_CONF}
 coproc arvboot (~/go/bin/arvados-server boot \
     -type test \
     -config ${ARVADOS_CONF} \