Merge branch '17109-keepweb-webdav-urls'
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 14 Jan 2021 13:58:01 +0000 (10:58 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 14 Jan 2021 13:58:01 +0000 (10:58 -0300)
Refs #17109

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

cypress/integration/collection-panel.spec.js
cypress/integration/delete-multiple-files.spec.js
cypress/support/commands.js
src/components/file-tree/file-thumbnail.test.tsx
src/components/file-tree/file-thumbnail.tsx
src/views-components/context-menu/actions/collection-file-viewer-action.tsx
src/views-components/context-menu/actions/file-viewer-action.tsx
src/views-components/context-menu/actions/helpers.test.ts
src/views-components/context-menu/actions/helpers.ts
src/views/workbench/workbench-loading-screen.tsx

index b86ff2a7d39143fe6c3d66c813abdfaec386085c..f70fe691ad4f2c5999b07a6eee9abe6f6c006a1c 100644 (file)
@@ -51,8 +51,11 @@ describe('Collection panel tests', function() {
                         tail_uuid: activeUser.user.uuid
                     })
                     cy.visit(`/collections/${this.testCollection.uuid}`);
-                    // Check that name & uuid are correct.
+
+                    cy.get('[data-cy=linear-progress]').should('exist');
                     cy.get('[data-cy=linear-progress]').should('not.exist');
+
+                    // Check that name & uuid are correct.
                     cy.get('[data-cy=collection-info-panel]')
                         .should('contain', this.testCollection.name)
                         .and('contain', this.testCollection.uuid)
@@ -139,6 +142,10 @@ describe('Collection panel tests', function() {
         .as('testCollection').then(function() {
             cy.loginAs(activeUser);
             cy.visit(`/collections/${this.testCollection.uuid}`);
+
+            cy.get('[data-cy=linear-progress]').should('exist');
+            cy.get('[data-cy=linear-progress]').should('not.exist');
+
             const nameTransitions = [
                 ['bar', '&'],
                 ['&', 'foo'],
@@ -147,7 +154,6 @@ describe('Collection panel tests', function() {
                 ['I ❤️ ⛵️', '...']
             ];
             nameTransitions.forEach(([from, to]) => {
-                cy.get('[data-cy=linear-progress]').should('not.exist');
                 cy.get('[data-cy=collection-files-panel]')
                     .contains(`${from}`).rightclick();
                 cy.get('[data-cy=context-menu]')
@@ -176,8 +182,11 @@ describe('Collection panel tests', function() {
         .as('testCollection').then(function() {
             cy.loginAs(activeUser);
             cy.visit(`/collections/${this.testCollection.uuid}`);
-            // Rename 'bar' to 'subdir/foo'
+
+            cy.get('[data-cy=linear-progress]').should('exist');
             cy.get('[data-cy=linear-progress]').should('not.exist');
+
+            // Rename 'bar' to 'subdir/foo'
             cy.get('[data-cy=collection-files-panel]')
                 .contains('bar').rightclick();
             cy.get('[data-cy=context-menu]')
@@ -214,7 +223,7 @@ describe('Collection panel tests', function() {
         });
     });
 
-    it('tries to rename a file with an illegal names', function() {
+    it('tries to rename a file with illegal names', function() {
         // Creates the collection using the admin token so we can set up
         // a bogus manifest text without block signatures.
         cy.createCollection(adminUser.token, {
@@ -224,6 +233,10 @@ describe('Collection panel tests', function() {
         .as('testCollection').then(function() {
             cy.loginAs(activeUser);
             cy.visit(`/collections/${this.testCollection.uuid}`);
+
+            cy.get('[data-cy=linear-progress]').should('exist');
+            cy.get('[data-cy=linear-progress]').should('not.exist');
+
             const illegalNamesFromUI = [
                 ['.', "Name cannot be '.' or '..'"],
                 ['..', "Name cannot be '.' or '..'"],
@@ -234,7 +247,6 @@ describe('Collection panel tests', function() {
                 ['//foo', 'Empty dir name not allowed']
             ]
             illegalNamesFromUI.forEach(([name, errMsg]) => {
-                cy.get('[data-cy=linear-progress]').should('not.exist');
                 cy.get('[data-cy=collection-files-panel]')
                     .contains('bar').rightclick();
                 cy.get('[data-cy=context-menu]')
@@ -299,6 +311,10 @@ describe('Collection panel tests', function() {
             // Check the old version displays as what it is.
             cy.loginAs(activeUser)
             cy.visit(`/collections/${oldVersionUuid}`);
+
+            cy.get('[data-cy=linear-progress]').should('exist');
+            cy.get('[data-cy=linear-progress]').should('not.exist');
+
             cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
             cy.get('[data-cy=read-only-icon]').should('exist');
             cy.get('[data-cy=collection-info-panel]').should('contain', colName);
@@ -320,6 +336,10 @@ describe('Collection panel tests', function() {
             // Visit collection, check basic information
             cy.loginAs(activeUser)
             cy.visit(`/collections/${this.collection.uuid}`);
+
+            cy.get('[data-cy=linear-progress]').should('exist');
+            cy.get('[data-cy=linear-progress]').should('not.exist');
+
             cy.get('[data-cy=collection-info-panel]').should('not.contain', 'This is an old version');
             cy.get('[data-cy=read-only-icon]').should('not.exist');
             cy.get('[data-cy=collection-version-number]').should('contain', '1');
index 2c7f48404abf02eb9962ca0d4874da2ba41e118d..0955f56a036f9aa8aad670c576a74e36d662a9a7 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-describe('Collection panel tests', function () {
+describe('Multi-file deletion tests', function () {
     let activeUser;
     let adminUser;
 
@@ -33,6 +33,10 @@ describe('Collection panel tests', function () {
             .as('testCollection').then(function () {
                 cy.loginAs(activeUser);
                 cy.visit(`/collections/${this.testCollection.uuid}`);
+
+                cy.get('[data-cy=linear-progress]').should('exist');
+                cy.get('[data-cy=linear-progress]').should('not.exist');
+
                 cy.get('[data-cy=collection-files-panel]').within(() => {
                     cy.get('[type="checkbox"]').first().check();
                     cy.get('[type="checkbox"]').last().check();
@@ -56,6 +60,9 @@ describe('Collection panel tests', function () {
                 cy.loginAs(activeUser);
                 cy.visit(`/collections/${this.testCollection.uuid}`);
 
+                cy.get('[data-cy=linear-progress]').should('exist');
+                cy.get('[data-cy=linear-progress]').should('not.exist');
+
                 cy.get('[data-cy=virtual-file-tree] > div > i').first().click();
                 cy.get('[data-cy=collection-files-panel]')
                     .should('contain', 'foo');
index 228e1cabb14e3b68fe43705add938c03cc45bb0d..c2b222a74e15734d60e24edb2f9114040672cd06 100644 (file)
@@ -166,6 +166,8 @@ Cypress.Commands.add(
 Cypress.Commands.add(
     "loginAs", (user) => {
         cy.visit(`/token/?api_token=${user.token}`);
+        cy.get('[data-cy=loading-spinner]').should('exist');
+        cy.get('[data-cy=loading-spinner]').should('not.exist');
         cy.url().should('contain', '/projects/');
         cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)');
         cy.get('div#root').should('not.contain', 'Your account is inactive');
index e0d5d2557d3820700fdb4943cb20a57578324529..a23bbcf9eb7e336b447d9a1c21c0855d6809030f 100644 (file)
@@ -3,10 +3,12 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { shallow, configure } from "enzyme";
+import { configure, mount } from "enzyme";
 import { FileThumbnail } from "./file-thumbnail";
 import { CollectionFileType } from '../../models/collection-file';
 import * as Adapter from 'enzyme-adapter-react-16';
+import { Provider } from "react-redux";
+import { combineReducers, createStore } from "redux";
 
 configure({ adapter: new Adapter() });
 
@@ -14,10 +16,22 @@ jest.mock('is-image', () => ({
     'default': () => true,
 }));
 
+let store;
+
 describe("<FileThumbnail />", () => {
     let file;
 
     beforeEach(() => {
+        const initialAuthState = {
+            config: {
+                keepWebServiceUrl: 'http://example.com/',
+                keepWebInlineServiceUrl: 'http://*.collections.example.com/',
+            }
+        }
+        store = createStore(combineReducers({
+            auth: (state: any = initialAuthState, action: any) => state,
+        }));
+
         file = {
             name: 'test-image',
             type: CollectionFileType.FILE,
@@ -27,7 +41,7 @@ describe("<FileThumbnail />", () => {
     });
 
     it("renders file thumbnail with proper src", () => {
-        const fileThumbnail = shallow(<FileThumbnail file={file} />);
-        expect(fileThumbnail.html()).toBe('<img class="Component-thumbnail-1" alt="test-image" src="http://example.com/c=zzzzz-4zz18-0123456789abcde/test-image.jpg?api_token=v2/zzzzz-gj3su-0123456789abcde/xxxxxxtokenxxxxx"/>');
+        const fileThumbnail = mount(<Provider store={store}><FileThumbnail file={file} /></Provider>);
+        expect(fileThumbnail.html()).toBe('<img class="Component-thumbnail-1" alt="test-image" src="http://zzzzz-4zz18-0123456789abcde.collections.example.com/test-image.jpg?api_token=v2/zzzzz-gj3su-0123456789abcde/xxxxxxtokenxxxxx">');
     });
 });
index 40631961a8a526143c7d7c6b3835945adb940284..a56dcf2f749a7d23dcaf467ccc248042d63bc9b7 100644 (file)
@@ -7,7 +7,9 @@ import isImage from 'is-image';
 import { withStyles, WithStyles } from '@material-ui/core';
 import { FileTreeData } from '~/components/file-tree/file-tree-data';
 import { CollectionFileType } from '~/models/collection-file';
-import { sanitizeToken } from "~/views-components/context-menu/actions/helpers";
+import { getInlineFileUrl, sanitizeToken } from "~/views-components/context-menu/actions/helpers";
+import { connect } from "react-redux";
+import { RootState } from "~/store/store";
 
 export interface FileThumbnailProps {
     file: FileTreeData;
@@ -28,10 +30,20 @@ const imageFileThumbnailStyle = withStyles<ImageFileThumbnailCssRules>(theme =>
     }
 }));
 
-const ImageFileThumbnail = imageFileThumbnailStyle(
-    ({ classes, file }: WithStyles<ImageFileThumbnailCssRules> & FileThumbnailProps) =>
+interface ImageFileThumbnailProps {
+    keepWebServiceUrl: string;
+    keepWebInlineServiceUrl: string;
+}
+
+const mapStateToProps = ({ auth }: RootState): ImageFileThumbnailProps => ({
+    keepWebServiceUrl: auth.config.keepWebServiceUrl,
+    keepWebInlineServiceUrl: auth.config.keepWebInlineServiceUrl,
+});
+
+const ImageFileThumbnail = connect(mapStateToProps)(imageFileThumbnailStyle(
+    ({ classes, file, keepWebServiceUrl, keepWebInlineServiceUrl }: WithStyles<ImageFileThumbnailCssRules> & FileThumbnailProps & ImageFileThumbnailProps) =>
         <img
             className={classes.thumbnail}
             alt={file.name}
-            src={sanitizeToken(file.url)} />
-);
+            src={sanitizeToken(getInlineFileUrl(file.url, keepWebServiceUrl, keepWebInlineServiceUrl))} />
+));
index dfc9d14acd7e5552e5daa23e9256b04ab819f764..aba355346bdbdf5fa9398a08d3faa53cd963818c 100644 (file)
@@ -7,6 +7,7 @@ import { RootState } from "../../../store/store";
 import { FileViewerAction } from '~/views-components/context-menu/actions/file-viewer-action';
 import { getNodeValue } from "~/models/tree";
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { getInlineFileUrl, sanitizeToken } from "./helpers";
 
 const mapStateToProps = (state: RootState) => {
     const { resource } = state.contextMenu;
@@ -16,8 +17,12 @@ const mapStateToProps = (state: RootState) => {
         resource.menuKind === ContextMenuKind.READONLY_COLLECTION_FILES_ITEM)) {
         const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
         if (file) {
+            const fileUrl = sanitizeToken(getInlineFileUrl(
+                file.url,
+                state.auth.config.keepWebServiceUrl,
+                state.auth.config.keepWebInlineServiceUrl), true);
             return {
-                href: file.url.replace(state.auth.config.keepWebServiceUrl, state.auth.config.keepWebInlineServiceUrl),
+                href: fileUrl,
                 kind: 'file',
                 currentCollectionUuid
             };
index a631424e67bde5f46dbca5e370c3e6bdaa70eb29..e31bb10d441b5b135a6bf7c79fde8de1726768a8 100644 (file)
@@ -3,22 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { connect } from 'react-redux';
 import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
 import { OpenIcon } from "~/components/icon/icon";
-import { sanitizeToken } from "./helpers";
-import { RootState } from "~/store/store";
 
 export const FileViewerAction = (props: any) => {
-    const {
-        keepWebServiceUrl,
-        keepWebInlineServiceUrl,
-    } = props;
-
     return props.href
         ? <a
             style={{ textDecoration: 'none' }}
-            href={sanitizeToken(props.href.replace(keepWebServiceUrl, keepWebInlineServiceUrl), true)}
+            href={props.href}
             target="_blank"
             onClick={props.onClick}>
             <ListItem button>
@@ -32,11 +24,3 @@ export const FileViewerAction = (props: any) => {
         </a>
         : null;
 };
-
-const mapStateToProps = ({ auth }: RootState): any => ({
-    keepWebServiceUrl: auth.config.keepWebServiceUrl,
-    keepWebInlineServiceUrl: auth.config.keepWebInlineServiceUrl,
-});
-
-
-export default connect(mapStateToProps, null)(FileViewerAction);
index 4234a8cc3a5ea594ef62f760a505dd5fab3255b8..c3a31691383764aaa5f33d47828bc72d4a961626 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { sanitizeToken, getClipboardUrl } from "./helpers";
+import { sanitizeToken, getClipboardUrl, getInlineFileUrl } from "./helpers";
 
 describe('helpers', () => {
     // given
@@ -27,4 +27,39 @@ describe('helpers', () => {
             expect(result).toBe('http://localhost?redirectTo=https://example.com/c=zzzzz/LIMS/1.html');
         });
     });
+
+    describe('getInlineFileUrl', () => {
+        it('should add the collection\'s uuid to the hostname', () => {
+            // when
+            const webDavUrlA = 'https://*.collections.example.com/';
+            const webDavUrlB = 'https://*--collections.example.com/';
+            const webDavDownloadUrl = 'https://example.com/';
+
+            // then
+            expect(getInlineFileUrl(url, webDavDownloadUrl, webDavUrlA))
+                .toBe('https://zzzzz.collections.example.com/t=v2/a/b/LIMS/1.html');
+            expect(getInlineFileUrl(url, webDavDownloadUrl, webDavUrlB))
+                .toBe('https://zzzzz--collections.example.com/t=v2/a/b/LIMS/1.html');
+        });
+
+        it('should keep the url the same when no inline url available', () => {
+            // when
+            const webDavUrl = '';
+            const webDavDownloadUrl = 'https://example.com/';
+            const result = getInlineFileUrl(url, webDavDownloadUrl, webDavUrl);
+
+            // then
+            expect(result).toBe('https://example.com/c=zzzzz/t=v2/a/b/LIMS/1.html');
+        });
+
+        it('should replace the url when available', () => {
+            // when
+            const webDavUrl = 'https://download.example.com/';
+            const webDavDownloadUrl = 'https://example.com/';
+            const result = getInlineFileUrl(url, webDavDownloadUrl, webDavUrl);
+
+            // then
+            expect(result).toBe('https://download.example.com/c=zzzzz/t=v2/a/b/LIMS/1.html');
+        });
+    });
 });
\ No newline at end of file
index 0ad2dc3c56c2c2ecd25a2f397467f377e2928bef..ce5dd8b1b2030f1e706f609ef381960760e184cf 100644 (file)
@@ -17,3 +17,27 @@ export const getClipboardUrl = (href: string, shouldSanitizeToken = true): strin
 
     return shouldSanitizeToken ? `${origin}?redirectTo=${url}` : `${origin}${url}`;
 };
+
+export const getInlineFileUrl = (url: string, keepWebSvcUrl: string, keepWebInlineSvcUrl: string): string => {
+    const collUuidMatch = url.match(/\/c=([a-z0-9-]+)\//);
+    if (collUuidMatch === null) { return ''; }
+    const collUuid = collUuidMatch[1];
+    let inlineUrl = keepWebInlineSvcUrl !== ""
+        ? url.replace(keepWebSvcUrl, keepWebInlineSvcUrl)
+        : url;
+    let uuidOnHostname = false;
+    // Inline URLs as 'https://*.collections.example.com' or
+    // 'https://*--collections.example.com' should get the uuid on their hostnames
+    // See: https://doc.arvados.org/v2.1/api/keep-web-urls.html
+    if (inlineUrl.indexOf('*.') > -1) {
+        inlineUrl = inlineUrl.replace('*.', `${collUuid}.`);
+        uuidOnHostname = true;
+    } else if (inlineUrl.indexOf('*--') > -1) {
+        inlineUrl = inlineUrl.replace('*--', `${collUuid}--`);
+        uuidOnHostname = true;
+    }
+    if (uuidOnHostname) {
+        inlineUrl = inlineUrl.replace(`/c=${collUuid}`, '');
+    }
+    return inlineUrl;
+};
\ No newline at end of file
index 30401034b6aaed23177b5cc002c9082409101e72..dd19fdf125a1151d5305c1298fda8e5e464c9226 100644 (file)
@@ -27,6 +27,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export const WorkbenchLoadingScreen = withStyles(styles)(({ classes }: WithStyles<CssRules>) =>
     <Grid container direction="column" alignItems='center' justify='center' className={classes.root}>
         <img src='/arvados_logo.png' className={classes.img} />
-        <CircularProgress />
+        <CircularProgress data-cy='loading-spinner' />
     </Grid>
 );