Merge remote-tracking branch 'origin/main' into 18169-cancel-button-not-working
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 19 Nov 2021 15:34:49 +0000 (16:34 +0100)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 19 Nov 2021 15:34:55 +0000 (16:34 +0100)
Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

24 files changed:
Makefile
README.md
cypress/integration/collection.spec.js
cypress/integration/favorites.spec.js
cypress/integration/side-panel.spec.js
docker/Dockerfile
src/common/config.ts
src/components/chips/chips.tsx
src/components/refresh-button/refresh-button.tsx
src/models/vocabulary.ts
src/services/collection-service/collection-service.test.ts
src/services/collection-service/collection-service.ts
src/services/common-service/common-resource-service.ts
src/services/vocabulary-service/vocabulary-service.ts
src/store/collection-panel/collection-panel-action.ts
src/store/collections/collection-update-actions.ts
src/store/vocabulary/vocabulary-actions.ts
src/views-components/main-content-bar/main-content-bar.tsx
src/views-components/resource-properties-form/property-key-field.tsx
src/views-components/resource-properties-form/property-value-field.tsx
src/views/collection-panel/collection-panel.tsx
tools/arvados_config.yml
tools/example-vocabulary.json [moved from public/vocabulary-example.json with 100% similarity]
tools/run-integration-tests.sh

index aaf2271c60fab7f10f30765f23cd5fcbae4c2437..ab58fc580cc53e7d11dadb020c51b202218a9712 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -97,8 +97,9 @@ $(DEB_FILE): build
         --url="https://arvados.org" \
         --license="GNU Affero General Public License, version 3.0" \
         --description="$(DESCRIPTION)" \
-        --config-files="etc/arvados/workbench2/workbench2.example.json" \
-       $(WORKSPACE)/build/=$(DEST_DIR)
+        --config-files="etc/arvados/$(APP_NAME)/workbench2.example.json" \
+       $(WORKSPACE)/build/=$(DEST_DIR) \
+       etc/arvados/workbench2/workbench2.example.json=/etc/arvados/$(APP_NAME)/workbench2.example.json
 
 $(RPM_FILE): build
        fpm \
@@ -112,8 +113,9 @@ $(RPM_FILE): build
         --url="https://arvados.org" \
         --license="GNU Affero General Public License, version 3.0" \
         --description="$(DESCRIPTION)" \
-        --config-files="etc/arvados/workbench2/workbench2.example.json" \
-        $(WORKSPACE)/build/=$(DEST_DIR)
+        --config-files="etc/arvados/$(APP_NAME)/workbench2.example.json" \
+        $(WORKSPACE)/build/=$(DEST_DIR) \
+       etc/arvados/workbench2/workbench2.example.json=/etc/arvados/$(APP_NAME)/workbench2.example.json
 
 copy: $(DEB_FILE) $(RPM_FILE)
        for target in $(TARGETS) ; do \
@@ -130,5 +132,16 @@ copy: $(DEB_FILE) $(RPM_FILE)
 # use FPM to create DEB and RPM
 packages: copy
 
+packages-in-docker: workbench2-build-image
+       docker run --env ci="true" \
+               --env ARVADOS_DIRECTORY=/tmp/arvados \
+               --env APP_NAME=${APP_NAME} \
+               --env ITERATION=${ITERATION} \
+               --env TARGETS="${TARGETS}" \
+               -w="/tmp/workbench2" \
+               -t -v ${WORKSPACE}:/tmp/workbench2 \
+               -v ${ARVADOS_DIRECTORY}:/tmp/arvados workbench2-build:latest \
+               make packages
+
 workbench2-build-image:
        (cd docker && docker build -t workbench2-build .)
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 31337e28a72237d2e9c2d5551d7b30327c434957..e46be0c366717da85f09f7e60d121393e91cfd65 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 9f4e2b84c1444812cf8715a2ecc83216b9b7ef86..13a2c4675f7492f2f872e63e38c2ba7df4c0da37 100644 (file)
@@ -150,7 +150,7 @@ describe('Favorites tests', function () {
         cy.getAll('@mySharedWritableProject', '@testTargetCollection')
             .then(function ([mySharedWritableProject, testTargetCollection]) {
                 cy.loginAs(adminUser);
-                
+
                 cy.get('[data-cy=side-panel-tree]').contains('My Favorites').click();
 
                 const newProjectName = `New project name ${mySharedWritableProject.name}`;
@@ -160,7 +160,7 @@ describe('Favorites tests', function () {
 
                 cy.testEditProjectOrCollection('main', mySharedWritableProject.name, newProjectName, newProjectDescription);
                 cy.testEditProjectOrCollection('main', testTargetCollection.name, newCollectionName, newCollectionDescription, false);
-                
+
                 cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
 
                 cy.get('main').contains(newProjectName).rightclick();
@@ -171,7 +171,7 @@ describe('Favorites tests', function () {
                 cy.get('[data-cy=side-panel-tree]').contains('Public Favorites').click();
 
                 cy.testEditProjectOrCollection('main', newProjectName, mySharedWritableProject.name, 'newProjectDescription');
-                cy.testEditProjectOrCollection('main', newCollectionName, testTargetCollection.name, 'newCollectionDescription', false); 
+                cy.testEditProjectOrCollection('main', newCollectionName, testTargetCollection.name, 'newCollectionDescription', false);
             });
     });
 
index 912e68ebfe229c394fdb7287226060f39956202f..f9d4dca34014554d954b1c5ddd788393ea65ce05 100644 (file)
@@ -114,4 +114,30 @@ describe('Side panel tests', function() {
             });
     });
 
+    it('side panel react to refresh when project data changes', () => {
+        const project = 'writableProject';
+
+        cy.createProject({
+            owningUser: activeUser,
+            targetUser: activeUser,
+            projectName: project,
+            canWrite: true,
+            addToFavorites: false
+        });
+
+        cy.getAll('@writableProject')
+            .then(function ([writableProject]) {
+                cy.loginAs(activeUser);
+                
+                cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+
+                cy.get('[data-cy=side-panel-tree]').contains(writableProject.name).should('exist');
+
+                cy.trashGroup(activeUser.token, writableProject.uuid);
+
+                cy.contains('Refresh').click();
+
+                cy.contains(writableProject.name).should('not.exist');
+            });
+    });
 })
index 729d62c4cc2efaaa838513dc1603a98c45337a4e..3bffcac4a8a4d910c1b2b10a68f9239d931fadf7 100644 (file)
@@ -11,8 +11,16 @@ RUN apt-get update && \
     libsecret-1-0 libsecret-1-dev rpm ruby ruby-dev rubygems build-essential \
     libpam0g-dev libgbm1 git && \
     apt-get clean
-RUN apt-get -yq --no-install-recommends -t buster-backports install golang-go && \
-    apt-get clean
+
+# Get Go 1.16.9
+RUN cd /usr/src && \
+    wget https://golang.org/dl/go1.16.9.linux-amd64.tar.gz && \
+    tar xzf go1.16.9.linux-amd64.tar.gz && \
+    ln -s /usr/src/go/bin/go /usr/local/bin/go-1.16.9 && \
+    ln -s /usr/src/go/bin/gofmt /usr/local/bin/gofmt-1.16.9 && \
+    ln -s /usr/local/bin/go-1.16.9 /usr/local/bin/go && \
+    ln -s /usr/local/bin/gofmt-1.16.9 /usr/local/bin/gofmt
+
 RUN gem install --no-ri --no-rdoc fpm
 RUN git clone https://git.arvados.org/arvados.git && cd arvados && \
     go mod download && \
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 2a6fafc3991e415ff38e52095f2a518deb540b62..eb68ed7a257942a6fb554f80b544089d93fcaa43 100644 (file)
@@ -43,10 +43,13 @@ export const Chips = withStyles(styles)(
             </Grid>;
         }
 
-        renderChip = (value: Value, index: number) =>
-            <Grid item key={index}>
-                <this.chip {...{ value }} />
+        renderChip = (value: Value, index: number) => {
+            const { deletable, getLabel } = this.props;
+            return <Grid item key={index}>
+                <Chip onDelete={deletable ? this.deleteValue(value) : undefined}
+                    label={getLabel !== undefined ? getLabel(value) : value} />
             </Grid>
+        }
 
         type = 'chip';
 
@@ -132,4 +135,4 @@ interface CollectedProps {
 
 interface DraggableChipProps<Value> {
     value: Value;
-}
\ No newline at end of file
+}
index f2c41d28f63769a74f45bfa38f2867d93a2dfefa..9971547bf092a5462b90f4db0e9ee66bb32f43a7 100644 (file)
@@ -22,13 +22,20 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     },
 });
 
-export const RefreshButton = ({ history, classes }: RouteComponentProps & WithStyles<CssRules>) =>
+interface RefreshButtonProps {
+    onClick?: () => void;
+}
+
+export const RefreshButton = ({ history, classes, onClick }: RouteComponentProps & WithStyles<CssRules> & RefreshButtonProps) =>
     <Button
         color="primary"
         size="small"
         variant="contained"
         onClick={() => {
             history.replace(window.location.pathname);
+            if (onClick) {
+                onClick();
+            }
         }}
         className={classNames(classes.buttonRight, classes.button)}>
         <ReRunProcessIcon />
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 061a45ec0111cfedb84405a699abe87017fbe404..c0aa85f1d3ab3e8df9912b993c978ef67d8c5834 100644 (file)
@@ -2,30 +2,53 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { AxiosInstance } from 'axios';
-import { WebDAV } from 'common/webdav';
-import { ApiActions } from '../api/api-actions';
+import axios, { AxiosInstance } from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { CollectionResource } from 'models/collection';
 import { AuthService } from '../auth-service/auth-service';
 import { CollectionService } from './collection-service';
 
 describe('collection-service', () => {
     let collectionService: CollectionService;
-    let serverApi;
+    let serverApi: AxiosInstance;
+    let axiosMock: MockAdapter;
     let webdavClient: any;
     let authService;
     let actions;
 
     beforeEach(() => {
-        serverApi = {} as AxiosInstance;
+        serverApi = axios.create();
+        axiosMock = new MockAdapter(serverApi);
         webdavClient = {
             delete: jest.fn(),
         } as any;
         authService = {} as AuthService;
-        actions = {} as ApiActions;
+        actions = {
+            progressFn: jest.fn(),
+        } as any;
         collectionService = new CollectionService(serverApi, webdavClient, authService, actions);
         collectionService.update = jest.fn();
     });
 
+    describe('update', () => {
+        it('should call put selecting updated fields + others', async () => {
+            serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+            const data: Partial<CollectionResource> = {
+                name: 'foo',
+            };
+            const expected = {
+                collection: {
+                    ...data,
+                    preserve_version: true,
+                },
+                select: ['uuid', 'name', 'version', 'modified_at'],
+            }
+            collectionService = new CollectionService(serverApi, webdavClient, authService, actions);
+            await collectionService.update('uuid', data);
+            expect(serverApi.put).toHaveBeenCalledWith('/collections/uuid', expected);
+        });
+    });
+
     describe('deleteFiles', () => {
         it('should remove no files', async () => {
             // given
index 52fbf1a5c50b8cf8e198b0b88b6739934e206651..48e797c500e8b7e3dade9fc15727773176805773 100644 (file)
@@ -33,7 +33,8 @@ export class CollectionService extends TrashableResourceService<CollectionResour
     }
 
     update(uuid: string, data: Partial<CollectionResource>) {
-        return super.update(uuid, { ...data, preserveVersion: true });
+        const select = [...Object.keys(data), 'version', 'modifiedAt'];
+        return super.update(uuid, { ...data, preserveVersion: true }, select);
     }
 
     async files(uuid: string) {
index 66e694a09b9f74d716416a223d7e8f5d76a98aa1..c6306779a9ee8cef4bb6eff287c485462f5e898a 100644 (file)
@@ -37,13 +37,16 @@ export class CommonResourceService<T extends Resource> extends CommonService<T>
         return super.create(payload);
     }
 
-    update(uuid: string, data: Partial<T>) {
+    update(uuid: string, data: Partial<T>, select?: string[]) {
         let payload: any;
         if (data !== undefined) {
             this.readOnlyFields.forEach( field => delete data[field] );
             payload = {
                 [this.resourceType.slice(0, -1)]: CommonService.mapKeys(snakeCase)(data),
             };
+            if (select !== undefined && select.length > 0) {
+                payload.select = ['uuid', ...select.map(field => snakeCase(field))];
+            };
         }
         return super.update(uuid, payload);
     }
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 ca9542c5b18d447854f7110af6fa40104548da00..ee476524256512c9fd5f24a48e5238cb558759cf 100644 (file)
@@ -17,6 +17,7 @@ import { SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 import { addProperty, deleteProperty } from "lib/resource-properties";
+import { getResource } from "store/resources/resources";
 
 export const collectionPanelActions = unionize({
     SET_COLLECTION: ofType<CollectionResource>(),
@@ -39,7 +40,6 @@ export const loadCollectionPanel = (uuid: string, forceReload = false) =>
         dispatch(resourcesActions.SET_RESOURCES([collection]));
         if (collection.fileCount <= COLLECTION_PANEL_LOAD_FILES_THRESHOLD &&
             !getState().collectionPanel.loadBigCollections) {
-            // dispatch<any>(loadCollectionFiles(collection.uuid));
         }
         return collection;
     };
@@ -52,11 +52,13 @@ export const createCollectionTag = (data: TagProperty) =>
         const properties = Object.assign({}, item.properties);
         const key = data.keyID || data.key;
         const value = data.valueID || data.value;
+        const cachedCollection = getResource<CollectionResource>(item.uuid)(getState().resources);
         services.collectionService.update(
             item.uuid, {
                 properties: addProperty(properties, key, value)
             }
         ).then(updatedCollection => {
+            updatedCollection = {...cachedCollection, ...updatedCollection};
             dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection));
             dispatch(resourcesActions.SET_RESOURCES([updatedCollection]));
             dispatch(snackbarActions.OPEN_SNACKBAR({
@@ -89,11 +91,13 @@ export const deleteCollectionTag = (key: string, value: string) =>
         if (!item) { return; }
 
         const properties = Object.assign({}, item.properties);
+        const cachedCollection = getResource<CollectionResource>(item.uuid)(getState().resources);
         services.collectionService.update(
             item.uuid, {
                 properties: deleteProperty(properties, key, value)
             }
         ).then(updatedCollection => {
+            updatedCollection = {...cachedCollection, ...updatedCollection};
             dispatch(collectionPanelActions.SET_COLLECTION(updatedCollection));
             dispatch(resourcesActions.SET_RESOURCES([updatedCollection]));
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Tag has been successfully deleted.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
index a9077cfb7455db4fd5c4b6a133502e9eb6da72ed..04f42b8d82f033b8a4d87690bfa988976ad06c40 100644 (file)
@@ -14,6 +14,7 @@ import { progressIndicatorActions } from "store/progress-indicator/progress-indi
 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import { updateResources } from "../resources/resources-actions";
 import { loadDetailsPanel } from "../details-panel/details-panel-action";
+import { getResource } from "store/resources/resources";
 
 export interface CollectionUpdateFormDialogData {
     uuid: string;
@@ -36,11 +37,13 @@ export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
         dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
         dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
 
+        const cachedCollection = getResource<CollectionResource>(collection.uuid)(getState().resources);
         services.collectionService.update(uuid, {
             name: collection.name,
             storageClassesDesired: collection.storageClassesDesired,
             description: collection.description }
         ).then(updatedCollection => {
+            updatedCollection = {...cachedCollection, ...updatedCollection};
             dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
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 6e1368c007d254eeedf4f179ba369cda153addb5..10ae1790da29cff215bb48de177f08bce707ad08 100644 (file)
@@ -12,6 +12,8 @@ import { RootState } from 'store/store';
 import * as Routes from 'routes/routes';
 import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
 import RefreshButton from "components/refresh-button/refresh-button";
+import { reloadProjectMatchingUuid } from "store/workbench/workbench-actions";
+import { loadSidePanelTreeProjects } from "store/side-panel-tree/side-panel-tree-actions";
 
 type CssRules = "infoTooltip";
 
@@ -55,10 +57,15 @@ const isButtonVisible = ({ router }: RootState) => {
 
 export const MainContentBar =
     connect((state: RootState) => ({
-        buttonVisible: isButtonVisible(state)
-    }), {
+        buttonVisible: isButtonVisible(state),
+        projectUuid: state.detailsPanel.resourceUuid,
+    }), (dispatch) => ({
             onDetailsPanelToggle: toggleDetailsPanel,
-        })(
+            onRefreshButtonClick: (id) => {
+                dispatch<any>(loadSidePanelTreeProjects(id));
+                dispatch<any>(reloadProjectMatchingUuid([id]));
+            }
+        }))(
             withStyles(styles)(
                 (props: MainContentBarProps & WithStyles<CssRules> & any) =>
                     <Toolbar>
@@ -67,7 +74,9 @@ export const MainContentBar =
                                 <Breadcrumbs />
                             </Grid>
                             <Grid item>
-                                <RefreshButton />
+                                <RefreshButton onClick={() => {
+                                    props.onRefreshButtonClick(props.projectUuid);
+                                }} />
                             </Grid>
                             <Grid item>
                                 {props.buttonVisible && <Tooltip title="Additional Info">
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 4270cbbdd490199105390a4bed2300e2243d4ede..e78b1f3d04c2b3494195479a9a3d959b9f5032cb 100644 (file)
@@ -120,7 +120,7 @@ export const CollectionPanel = withStyles(styles)(
                 isWritable = true;
             } else {
                 const itemOwner = getResource<GroupResource | UserResource>(item.ownerUuid)(state.resources);
-                if (itemOwner) {
+                if (itemOwner && itemOwner.writableBy) {
                     isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0;
                 }
             }
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} \