[ARVADOS-WORKBENCH2] updated: 2.2.1-57-g0f7f4ab0

Git user git at public.arvados.org
Thu Aug 26 19:44:19 UTC 2021


Summary of changes:
 cypress/integration/collection.spec.js             | 104 ++++--------------
 src/common/config.ts                               |  25 +----
 src/components/checkbox-field/checkbox-field.tsx   |  64 +----------
 src/store/collections/collection-create-actions.ts |   1 -
 src/store/collections/collection-update-actions.ts |   2 -
 src/store/context-menu/context-menu-actions.ts     |   1 -
 .../actions/collection-file-viewer-action.test.tsx | 117 +++++++++++++++++++++
 .../actions/collection-file-viewer-action.tsx      |   9 +-
 .../context-menu/actions/helpers.ts                |   9 +-
 .../details-panel/collection-details.tsx           |   6 +-
 .../details-panel/details-data.tsx                 |   7 +-
 .../details-panel/details-panel.tsx                |  20 +++-
 .../details-panel/file-details.tsx                 |   4 +-
 .../dialog-create/dialog-collection-create.tsx     |   7 +-
 .../dialog-forms/create-collection-dialog.ts       |   2 +-
 .../dialog-forms/update-collection-dialog.ts       |  12 +--
 .../dialog-update/dialog-collection-update.tsx     |   7 +-
 .../form-fields/collection-form-fields.tsx         |  24 -----
 .../collection-content-address-panel.tsx           |  28 ++---
 src/views/collection-panel/collection-panel.tsx    |   7 +-
 src/views/favorite-panel/favorite-panel.tsx        |   4 +-
 src/views/project-panel/project-panel.tsx          |   2 -
 .../public-favorites-panel.tsx                     |   2 -
 tools/arvados_config.yml                           |  16 +--
 24 files changed, 202 insertions(+), 278 deletions(-)
 create mode 100644 src/views-components/context-menu/actions/collection-file-viewer-action.test.tsx

  discards  2e83c4e79939937cb88c5fe9c02ce91459fc2655 (commit)
  discards  6d6ea46757e93939c12d026faf9fd3c4d7b83122 (commit)
  discards  dc63b1fd2a8cbf8c15c5521c27ff72bb5f94e9c7 (commit)
  discards  266f22d37cf94f56f857dbbb3e1e5fd2e9934175 (commit)
  discards  e31bd3f0af6e0b3d4166af144ef8aed5d110b5af (commit)
  discards  4da9314a82505cd016803f806f96e5714cc59f0a (commit)
  discards  81e046c27b73760acc5bffd51019516a2bbad94c (commit)
  discards  4541e7e78a5fbbad1e34cf215fa7f2fa4d40e88d (commit)
  discards  1f1c5dbbad160ef2cc7dc0a2cd736fefd5025d1e (commit)
  discards  ce576a6053b03f3222b30a129d77aa7f54fb1904 (commit)
  discards  76f6fc151e007fcce23c89a7414cfbd7b116897b (commit)
  discards  c2ca3c3f0e34c6226ae8f96b3f68f099e0a2f65f (commit)
  discards  f1568d57ebf1cbe4c8e649b80633459bea4ad327 (commit)
  discards  9deadbed533951f400aa2f626af04ccac21fe865 (commit)
  discards  7bdf07a2661affc4f1496d73e27d16f6470e4e4a (commit)
  discards  ec862c8c86eab36e998186f3138dd396d4ec13f6 (commit)
  discards  fd49462a5a09e107b7bb5c0ef8635db328b399b8 (commit)
  discards  4c35a8d1ba4aeda92e6be16dd389ee6189e32ad8 (commit)
  discards  ac8fd68834fc706b76fa32df82346d4a3df9026e (commit)
  discards  02370859e71ada20e2247df02958a9c532fe614b (commit)
  discards  b6a0a363d77d17455aab16e09eeb67b9f8067843 (commit)
  discards  77c5d854b58b46395c5db5531268494d40f27786 (commit)
  discards  2bf65b0d575c9c1e67270b66a7b32b0acd5aff56 (commit)
  discards  73122efb6429184611d17b3278ecbe9c2e26a6c2 (commit)
  discards  41a579060898b29e6d50000e6f95c93d97a7a433 (commit)
  discards  e07a6c34953c6546de1c3d13a0bacf031981c356 (commit)
  discards  64c1e51d2d973cb0ef0f982afbb80efdb2700ea0 (commit)
  discards  463581836f4fa5f1a6a54358998de32aa1dbd8e3 (commit)
  discards  c025baa77d3d077bc79283cce71e91c3aef06e4b (commit)
  discards  9df0dc3888f9d63ae0844e7425cd02b21af9da68 (commit)
  discards  8e3a9d4984273a913d472c4af63024c839d22ec6 (commit)
  discards  87fff7ceda76e813646e3fe1c2c41bb8ad567143 (commit)
  discards  83fd81b2e9518ac54f57e5463b6511da6257b2f7 (commit)
  discards  a15202e53207acc1878f02c00224d1df9540da8c (commit)
  discards  20063f6f7bb9ad7c6a9a0b49b3c5ba4b0abc532e (commit)
  discards  6ddbc50bc04b9bedce3f54cf69660fc819fce67a (commit)
  discards  8df73e82d637d7b2e81952fdc96a12a1011be99e (commit)
  discards  e5dc88dd1a6c54610d92854d527e5048543d93ec (commit)
  discards  ad956f33b83e55aadca7189d5352940aaeadd659 (commit)
  discards  40f03448780c8b22cba93d4eff9ca976cea45abf (commit)
  discards  3556e5483957f8479c5747e36c626e91a655cc21 (commit)
       via  0f7f4ab0fc5027f0de921eafa42046712c5a392c (commit)
       via  9e7204697d09dc040bc79832b9bc56cc16b0adfa (commit)
       via  c11e86550ab3732a9245589b741f14a80fdb92eb (commit)
       via  a798c09d9460952c7395838950fe7ffbfb23d1f5 (commit)
       via  f43c86f02dd7eff91d125945a2860e1151ac5262 (commit)
       via  de2ecc267f382654b48e7cd64f3365655af097be (commit)
       via  57056db59366e9af536d9796c3a9ca0e709e8231 (commit)
       via  13c8e3af81d583cce6bf1618b14e3174bc8ddfd4 (commit)
       via  91c0a082699fa9924c5e85826a107893d6de4ac8 (commit)
       via  c0bd8013a0941d5a8b0ce0478da025bb8f6bb6c5 (commit)
       via  49f9e629a1fb1806baecd4eb04b46917c13aa928 (commit)
       via  8650023dd84a06bb3353b6f0c94e111c0bd192cf (commit)
       via  41d10c5792a7a866cc5a8f2d49f6b22f1bed24e1 (commit)
       via  c154977f29cdd067f7f9316b472cc08c086d29c9 (commit)
       via  626b653fb1e9c8fffc8873d8e59f6e87f5a1cf83 (commit)
       via  57f62c90bb7b7a89ad8ac040812989f140fb637f (commit)
       via  eb9f55920f1a8ed73e0d931add118501c713d274 (commit)
       via  e16c61adea5e8eccd1236d8608a750cd7cb565df (commit)
       via  2352fda90d2bf2e7d3b09c3f50014b21a8a65ebb (commit)
       via  7edfda6a34df2bdd6cb21f22a952f3070de58866 (commit)
       via  fffe0e4cf71da4c8d2cdca2db958fa6403d0e5ec (commit)
       via  7ffd14cca42d5e2c75722f916f5362b32d9a07be (commit)
       via  8d954ff99ea4dfda2588c17314dd64f378871ee0 (commit)
       via  2953ffd61e451f6bb75bf51489ceeb983e774626 (commit)
       via  acf4d6c3197384510ab18f4aa776b283480458ea (commit)
       via  a8e2b7c927f403b8c6b497bbefd3106e3b6db7ef (commit)
       via  8451c622fdc4321f7c19c1c09975263a19a7023c (commit)
       via  f4cb3271191ef9a4978556caec65c146f0d21160 (commit)
       via  efbe3df438e43ac3f620a37da60ebc18ce3f495c (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (2e83c4e79939937cb88c5fe9c02ce91459fc2655)
            \
             N -- N -- N (0f7f4ab0fc5027f0de921eafa42046712c5a392c)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

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 0f7f4ab0fc5027f0de921eafa42046712c5a392c
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Aug 25 01:28:36 2021 -0400

    15159: Add unit tests for collection-file-viewer-action and TrustAllContent / secure URLs
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/context-menu/actions/collection-file-viewer-action.test.tsx b/src/views-components/context-menu/actions/collection-file-viewer-action.test.tsx
new file mode 100644
index 00000000..8b90f588
--- /dev/null
+++ b/src/views-components/context-menu/actions/collection-file-viewer-action.test.tsx
@@ -0,0 +1,117 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { mount, configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import configureMockStore from 'redux-mock-store'
+import { Provider } from 'react-redux';
+import { CollectionFileViewerAction } from './collection-file-viewer-action';
+import { ContextMenuKind } from 'views-components/context-menu/context-menu';
+import { createTree, initTreeNode, setNode, getNodeValue } from "models/tree";
+import { getInlineFileUrl, sanitizeToken } from "./helpers";
+
+const middlewares = [];
+const mockStore = configureMockStore(middlewares);
+
+configure({ adapter: new Adapter() });
+
+describe('CollectionFileViewerAction', () => {
+    let defaultStore;
+    const fileUrl = "https://download.host:12345/c=abcde-4zz18-abcdefghijklmno/t=v2/token2/token3/cat.jpg";
+    const insecureKeepInlineUrl = "https://download.host:12345/";
+    const secureKeepInlineUrl = "https://*.collections.host:12345/";
+
+    beforeEach(() => {
+        let filesTree = createTree();
+        let data = {id: "000", value: {"url": fileUrl}};
+        filesTree = setNode(initTreeNode(data))(filesTree);
+
+        defaultStore = {
+            auth: {
+                config: {
+                    keepWebServiceUrl: "https://download.host:12345/",
+                    keepWebInlineServiceUrl: insecureKeepInlineUrl,
+                    clusterConfig: {
+                        Collections: {
+                          TrustAllContent: false
+                        }
+                    }
+                }
+            },
+            contextMenu: {
+                resource: {
+                    uuid: "000",
+                    menuKind: ContextMenuKind.COLLECTION_FILE_ITEM,
+                }
+            },
+            collectionPanel: {
+                item: {
+                    uuid: ""
+                }
+            },
+            collectionPanelFiles: filesTree
+        };
+    });
+
+    it('should hide open in new tab when unsafe', () => {
+        // given
+        const store = mockStore(defaultStore);
+
+        // when
+        const wrapper = mount(<Provider store={store}>
+            <CollectionFileViewerAction />
+        </Provider>);
+
+        // then
+        expect(wrapper).not.toBeUndefined();
+
+        // and
+        expect(wrapper.find("a")).toHaveLength(0);
+    });
+
+    it('should show open in new tab when TrustAllContent=true', () => {
+        // given
+        let initialState = defaultStore;
+        initialState.auth.config.clusterConfig.Collections.TrustAllContent = true;
+        const store = mockStore(initialState);
+
+        // when
+        const wrapper = mount(<Provider store={store}>
+            <CollectionFileViewerAction />
+        </Provider>);
+
+        // then
+        expect(wrapper).not.toBeUndefined();
+
+        // and
+        expect(wrapper.find("a").prop("href"))
+            .toEqual(sanitizeToken(getInlineFileUrl(fileUrl,
+                initialState.auth.config.keepWebServiceUrl,
+                initialState.auth.config.keepWebInlineServiceUrl))
+            );
+    });
+
+    it('should show open in new tab when inline url is secure', () => {
+        // given
+        let initialState = defaultStore;
+        initialState.auth.config.keepWebInlineServiceUrl = secureKeepInlineUrl;
+        const store = mockStore(initialState);
+
+        // when
+        const wrapper = mount(<Provider store={store}>
+            <CollectionFileViewerAction />
+        </Provider>);
+
+        // then
+        expect(wrapper).not.toBeUndefined();
+
+        // and
+        expect(wrapper.find("a").prop("href"))
+            .toEqual(sanitizeToken(getInlineFileUrl(fileUrl,
+                initialState.auth.config.keepWebServiceUrl,
+                initialState.auth.config.keepWebInlineServiceUrl))
+            );
+    });
+});

commit 9e7204697d09dc040bc79832b9bc56cc16b0adfa
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Aug 24 14:57:33 2021 -0400

    15159: Update cypress tests to check clusterConfig and inlineUrl for hiding open in new tab
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index 1b9c0849..4690374d 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -122,12 +122,22 @@ describe('Collection panel tests', function () {
             }).as('sharedGroup').then(function () {
                 // Creates the collection using the admin token so we can set up
                 // a bogus manifest text without block signatures.
-                cy.createCollection(adminUser.token, {
-                    name: 'Test collection',
-                    owner_uuid: this.sharedGroup.uuid,
-                    properties: { someKey: 'someValue' },
-                    manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`
-                })
+                cy.doRequest('GET', '/arvados/v1/config', null, null)
+                    .its('body').should((clusterConfig) => {
+                      expect(clusterConfig.Collections, "clusterConfig").to.have.property("TrustAllContent", false);
+                      expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAV").have.property("ExternalURL");
+                      expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAVDownload").have.property("ExternalURL");
+                      const inlineUrl = clusterConfig.Services.WebDAV.ExternalURL !== ""
+                          ? clusterConfig.Services.WebDAV.ExternalURL
+                          : clusterConfig.Services.WebDAVDownload.ExternalURL;
+                      expect(inlineUrl).to.not.contain("*");
+                    })
+                    .createCollection(adminUser.token, {
+                      name: 'Test collection',
+                      owner_uuid: this.sharedGroup.uuid,
+                      properties: { someKey: 'someValue' },
+                      manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`
+                    })
                     .as('testCollection').then(function () {
                         // Share the group with active user.
                         cy.createLink(adminUser.token, {
@@ -189,6 +199,7 @@ describe('Collection panel tests', function () {
                             .contains(fileName).rightclick({ force: true });
                         cy.get('[data-cy=context-menu]')
                             .should('contain', 'Download')
+                            .and('not.contain', 'Open in new tab')
                             .and('contain', 'Copy to clipboard')
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
@@ -197,6 +208,7 @@ describe('Collection panel tests', function () {
                             .contains(subDirName).rightclick({ force: true });
                         cy.get('[data-cy=context-menu]')
                             .should('not.contain', 'Download')
+                            .and('not.contain', 'Open in new tab')
                             .and('contain', 'Copy to clipboard')
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');

commit c11e86550ab3732a9245589b741f14a80fdb92eb
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 23 09:31:21 2021 -0400

    15159: Hide file preview when not secure and trustallcontent is false
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/details-panel/collection-details.tsx b/src/views-components/details-panel/collection-details.tsx
index 7136c850..c449a2be 100644
--- a/src/views-components/details-panel/collection-details.tsx
+++ b/src/views-components/details-panel/collection-details.tsx
@@ -42,8 +42,8 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
         return ['Details', 'Versions'];
     }
 
-    getDetails(tabNumber: number) {
-        switch (tabNumber) {
+    getDetails({tabNr}) {
+        switch (tabNr) {
             case 0:
                 return this.getCollectionInfo();
             case 1:
diff --git a/src/views-components/details-panel/details-data.tsx b/src/views-components/details-panel/details-data.tsx
index 0fae2ac4..bcca325c 100644
--- a/src/views-components/details-panel/details-data.tsx
+++ b/src/views-components/details-panel/details-data.tsx
@@ -5,6 +5,11 @@
 import React from 'react';
 import { DetailsResource } from "models/details";
 
+interface GetDetailsParams {
+  tabNr?: number
+  showPreview?: boolean
+}
+
 export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
     constructor(protected item: T) { }
 
@@ -17,5 +22,5 @@ export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
     }
 
     abstract getIcon(className?: string): React.ReactElement<any>;
-    abstract getDetails(tabNr?: number): React.ReactElement<any>;
+    abstract getDetails({tabNr, showPreview}: GetDetailsParams): React.ReactElement<any>;
 }
diff --git a/src/views-components/details-panel/details-panel.tsx b/src/views-components/details-panel/details-panel.tsx
index 38ac163e..058db81b 100644
--- a/src/views-components/details-panel/details-panel.tsx
+++ b/src/views-components/details-panel/details-panel.tsx
@@ -20,6 +20,8 @@ import { ProcessDetails } from "./process-details";
 import { EmptyDetails } from "./empty-details";
 import { DetailsData } from "./details-data";
 import { DetailsResource } from "models/details";
+import { Config } from 'common/config';
+import { isInlineFileUrlSafe } from "../context-menu/actions/helpers";
 import { getResource } from 'store/resources/resources';
 import { toggleDetailsPanel, SLIDE_TIMEOUT, openDetailsPanel } from 'store/details-panel/details-panel-action';
 import { FileDetails } from 'views-components/details-panel/file-details';
@@ -77,12 +79,13 @@ const getItem = (res: DetailsResource): DetailsData => {
     }
 };
 
-const mapStateToProps = ({ detailsPanel, resources, collectionPanelFiles }: RootState) => {
+const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles }: RootState) => {
     const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource | undefined;
     const file = resource
         ? undefined
         : getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
     return {
+        authConfig: auth.config,
         isOpened: detailsPanel.isOpened,
         tabNr: detailsPanel.tabNr,
         res: resource || (file && file.value) || EMPTY_RESOURCE,
@@ -101,6 +104,7 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
 export interface DetailsPanelDataProps {
     onCloseDrawer: () => void;
     setActiveTab: (tabNr: number) => void;
+    authConfig: Config;
     isOpened: boolean;
     tabNr: number;
     res: DetailsResource;
@@ -143,7 +147,17 @@ export const DetailsPanel = withStyles(styles)(
             }
 
             renderContent() {
-                const { classes, onCloseDrawer, res, tabNr } = this.props;
+                const { classes, onCloseDrawer, res, tabNr, authConfig } = this.props;
+
+                let shouldShowInlinePreview = false;
+                if (!('kind' in res)) {
+                    shouldShowInlinePreview = isInlineFileUrlSafe(
+                      res ? res.url : "",
+                      authConfig.keepWebServiceUrl,
+                      authConfig.keepWebInlineServiceUrl
+                    ) || authConfig.clusterConfig.Collections.TrustAllContent;
+                }
+
                 const item = getItem(res);
                 return <Grid
                     container
@@ -183,7 +197,7 @@ export const DetailsPanel = withStyles(styles)(
                         </Tabs>
                     </Grid>
                     <Grid item xs className={this.props.classes.tabContainer} >
-                        {item.getDetails(tabNr)}
+                        {item.getDetails({tabNr, showPreview: shouldShowInlinePreview})}
                     </Grid>
                 </Grid >;
             }
diff --git a/src/views-components/details-panel/file-details.tsx b/src/views-components/details-panel/file-details.tsx
index 7c11eb8b..7b128c2c 100644
--- a/src/views-components/details-panel/file-details.tsx
+++ b/src/views-components/details-panel/file-details.tsx
@@ -18,13 +18,13 @@ export class FileDetails extends DetailsData<CollectionFile | CollectionDirector
         return <Icon className={className} />;
     }
 
-    getDetails() {
+    getDetails({showPreview}) {
         const { item } = this;
         return item.type === CollectionFileType.FILE
             ? <>
                 <DetailsAttribute label='Size' value={formatFileSize(item.size)} />
                 {
-                    isImage(item.url) && <>
+                    isImage(item.url) && showPreview && <>
                         <DetailsAttribute label='Preview' />
                         <FileThumbnail file={item} />
                     </>

commit a798c09d9460952c7395838950fe7ffbfb23d1f5
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 16 16:00:08 2021 -0400

    15159: Update cypress tests to not expect open file in new tab
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index 3b370b60..1b9c0849 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -189,7 +189,6 @@ describe('Collection panel tests', function () {
                             .contains(fileName).rightclick({ force: true });
                         cy.get('[data-cy=context-menu]')
                             .should('contain', 'Download')
-                            .and('contain', 'Open in new tab')
                             .and('contain', 'Copy to clipboard')
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
@@ -198,7 +197,6 @@ describe('Collection panel tests', function () {
                             .contains(subDirName).rightclick({ force: true });
                         cy.get('[data-cy=context-menu]')
                             .should('not.contain', 'Download')
-                            .and('contain', 'Open in new tab')
                             .and('contain', 'Copy to clipboard')
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
                             .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');

commit f43c86f02dd7eff91d125945a2860e1151ac5262
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 16 15:17:28 2021 -0400

    15159: Hide "open in new tab" if unsafe and TrustAllContent is false
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/common/config.ts b/src/common/config.ts
index f3d06840..d2ddb947 100644
--- a/src/common/config.ts
+++ b/src/common/config.ts
@@ -89,7 +89,8 @@ export interface ClusterConfigJSON {
                 Value: string,
                 Protected?: boolean,
             }
-        }
+        },
+        TrustAllContent: boolean
     };
 }
 
@@ -251,6 +252,7 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
     },
     Collections: {
         ForwardSlashNameSubstitution: "",
+        TrustAllContent: false,
     },
     ...config
 });
diff --git a/src/views-components/context-menu/actions/collection-file-viewer-action.tsx b/src/views-components/context-menu/actions/collection-file-viewer-action.tsx
index 27a65018..f736f0bf 100644
--- a/src/views-components/context-menu/actions/collection-file-viewer-action.tsx
+++ b/src/views-components/context-menu/actions/collection-file-viewer-action.tsx
@@ -7,7 +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";
+import { getInlineFileUrl, sanitizeToken, isInlineFileUrlSafe } from "./helpers";
 
 const mapStateToProps = (state: RootState) => {
     const { resource } = state.contextMenu;
@@ -18,7 +18,12 @@ const mapStateToProps = (state: RootState) => {
         ContextMenuKind.COLLECTION_DIRECTORY_ITEM,
         ContextMenuKind.READONLY_COLLECTION_DIRECTORY_ITEM ].indexOf(resource.menuKind as ContextMenuKind) > -1) {
         const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
-        if (file) {
+        const shouldShowInlineUrl = isInlineFileUrlSafe(
+                                file ? file.url : "",
+                                state.auth.config.keepWebServiceUrl,
+                                state.auth.config.keepWebInlineServiceUrl
+                              ) || state.auth.config.clusterConfig.Collections.TrustAllContent;
+        if (file && shouldShowInlineUrl) {
             const fileUrl = sanitizeToken(getInlineFileUrl(
                 file.url,
                 state.auth.config.keepWebServiceUrl,
diff --git a/src/views-components/context-menu/actions/helpers.ts b/src/views-components/context-menu/actions/helpers.ts
index dfa8d04f..159b1c18 100644
--- a/src/views-components/context-menu/actions/helpers.ts
+++ b/src/views-components/context-menu/actions/helpers.ts
@@ -43,4 +43,11 @@ export const getInlineFileUrl = (url: string, keepWebSvcUrl: string, keepWebInli
         inlineUrl = inlineUrl.replace(`/c=${collMatch[1]}`, '');
     }
     return inlineUrl;
-};
\ No newline at end of file
+};
+
+export const isInlineFileUrlSafe = (url: string, keepWebSvcUrl: string, keepWebInlineSvcUrl: string): boolean => {
+  let inlineUrl = keepWebInlineSvcUrl !== ""
+      ? url.replace(keepWebSvcUrl, keepWebInlineSvcUrl)
+      : url;
+  return inlineUrl.indexOf('*.') > -1;
+}
diff --git a/tools/arvados_config.yml b/tools/arvados_config.yml
index a287fed4..9b82948c 100644
--- a/tools/arvados_config.yml
+++ b/tools/arvados_config.yml
@@ -10,7 +10,7 @@ Clusters:
       CollectionVersioning: true
       PreserveVersionIfIdle: -1s
       BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
-      TrustAllContent: true
+      TrustAllContent: false
       ForwardSlashNameSubstitution: /
       ManagedProperties:
         original_owner_uuid: {Function: original_owner, Protected: true}

commit de2ecc267f382654b48e7cd64f3365655af097be
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Aug 12 13:59:51 2021 -0400

    17532: Add cypress test for collection history username
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index f1e337de..3b370b60 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -469,10 +469,14 @@ describe('Collection panel tests', function () {
                     .within(() => {
                         // Version 1: 6 bytes in size
                         cy.get('[data-cy=collection-version-browser-select-1]')
-                            .should('contain', '1').and('contain', '6 B');
+                            .should('contain', '1')
+                            .and('contain', '6 B')
+                            .and('contain', adminUser.user.uuid);
                         // Version 2: 3 bytes in size (one file removed)
                         cy.get('[data-cy=collection-version-browser-select-2]')
-                            .should('contain', '2').and('contain', '3 B');
+                            .should('contain', '2')
+                            .and('contain', '3 B')
+                            .and('contain', activeUser.user.full_name);
                         cy.get('[data-cy=collection-version-browser-select-3]')
                             .should('not.exist');
                         cy.get('[data-cy=collection-version-browser-select-1]')

commit 57056db59366e9af536d9796c3a9ca0e709e8231
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Aug 12 11:10:17 2021 -0400

    17532: Move collection details history modified by to its own row
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index f28f8aa0..3965e69d 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -465,9 +465,9 @@ export const UserNameFromID =
             if (userFullname === '') {
                 dispatch<any>(loadResource(uuid, false));
             }
-            return <Typography inline>
+            return <span>
                 {userFullname ? userFullname : uuid}
-            </Typography>;
+            </span>;
         });
 
 export const ResponsiblePerson =
diff --git a/src/views-components/details-panel/collection-details.tsx b/src/views-components/details-panel/collection-details.tsx
index 3c89a154..7136c850 100644
--- a/src/views-components/details-panel/collection-details.tsx
+++ b/src/views-components/details-panel/collection-details.tsx
@@ -17,7 +17,7 @@ import { Dispatch } from 'redux';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
 
-export type CssRules = 'versionBrowserHeader' | 'versionBrowserItem';
+export type CssRules = 'versionBrowserHeader' | 'versionBrowserItem' | 'versionBrowserField';
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     versionBrowserHeader: {
@@ -25,6 +25,9 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
         fontWeight: 'bold',
     },
     versionBrowserItem: {
+        flexWrap: 'wrap',
+    },
+    versionBrowserField: {
         textAlign: 'center',
     }
 });
@@ -107,17 +110,12 @@ const CollectionVersionBrowser = withStyles(styles)(
                             Nr
                         </Typography>
                     </Grid>
-                    <Grid item xs={2}>
+                    <Grid item xs={4}>
                         <Typography variant="caption" className={classes.versionBrowserHeader}>
                             Size
                         </Typography>
                     </Grid>
-                    <Grid item xs={3}>
-                        <Typography variant="caption" className={classes.versionBrowserHeader}>
-                            User
-                        </Typography>
-                    </Grid>
-                    <Grid item xs={5}>
+                    <Grid item xs={6}>
                         <Typography variant="caption" className={classes.versionBrowserHeader}>
                             Date
                         </Typography>
@@ -130,25 +128,26 @@ const CollectionVersionBrowser = withStyles(styles)(
                             key={item.version}
                             onClick={e => showVersion(item)}
                             onContextMenu={event => handleContextMenu(event, item)}
-                            selected={isSelectedVersion}>
+                            selected={isSelectedVersion}
+                            className={classes.versionBrowserItem}>
                             <Grid item xs={2}>
-                                <Typography variant="caption" className={classes.versionBrowserItem}>
+                                <Typography variant="caption" className={classes.versionBrowserField}>
                                     {item.version}
                                 </Typography>
                             </Grid>
-                            <Grid item xs={2}>
-                                <Typography variant="caption" className={classes.versionBrowserItem}>
+                            <Grid item xs={4}>
+                                <Typography variant="caption" className={classes.versionBrowserField}>
                                     {formatFileSize(item.fileSizeTotal)}
                                 </Typography>
                             </Grid>
-                            <Grid item xs={3}>
-                                <Typography variant="caption" className={classes.versionBrowserItem}>
-                                    <UserNameFromID uuid={item.modifiedByUserUuid} />
+                            <Grid item xs={6}>
+                                <Typography variant="caption" className={classes.versionBrowserField}>
+                                    {formatDate(item.modifiedAt)}
                                 </Typography>
                             </Grid>
-                            <Grid item xs={5}>
-                                <Typography variant="caption" className={classes.versionBrowserItem}>
-                                    {formatDate(item.modifiedAt)}
+                            <Grid item xs={12}>
+                                <Typography variant="caption" className={classes.versionBrowserField}>
+                                    Modified by: <UserNameFromID uuid={item.modifiedByUserUuid} />
                                 </Typography>
                             </Grid>
                         </ListItem>

commit 13c8e3af81d583cce6bf1618b14e3174bc8ddfd4
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Aug 10 10:45:09 2021 -0400

    17532: Remove unused code in renderers
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 4a56b142..f28f8aa0 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -458,10 +458,8 @@ export const ResourceOwnerWithName =
         });
 
 export const UserNameFromID =
-    compose(
-        userFromID,
-        withStyles({}, { withTheme: true }))
-        ((props: { uuid: string, userFullname: string, dispatch: Dispatch }) => {
+    compose(userFromID)(
+        (props: { uuid: string, userFullname: string, dispatch: Dispatch }) => {
             const { uuid, userFullname, dispatch } = props;
 
             if (userFullname === '') {

commit 91c0a082699fa9924c5e85826a107893d6de4ac8
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 9 21:26:41 2021 -0400

    17532: Add modifiedByUser to collection details version history table
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 314390e2..4a56b142 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -425,24 +425,27 @@ export const ResourceOwnerName = connect(
         return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
     })((props: { owner: string }) => renderOwner(props.owner));
 
-export const ResourceOwnerWithName =
-    compose(
-        connect(
-            (state: RootState, props: { uuid: string }) => {
-                let ownerName = '';
-                const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
+const userFromID =
+    connect(
+        (state: RootState, props: { uuid: string }) => {
+            let userFullname = '';
+            const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
+
+            if (resource) {
+                userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
+            }
 
-                if (resource) {
-                    ownerName = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
-                }
+            return { uuid: props.uuid, userFullname };
+        });
 
-                return { uuid: props.uuid, ownerName };
-            }),
+export const ResourceOwnerWithName =
+    compose(
+        userFromID,
         withStyles({}, { withTheme: true }))
-        ((props: { uuid: string, ownerName: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
-            const { uuid, ownerName, dispatch, theme } = props;
+        ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
+            const { uuid, userFullname, dispatch, theme } = props;
 
-            if (ownerName === '') {
+            if (userFullname === '') {
                 dispatch<any>(loadResource(uuid, false));
                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
                     {uuid}
@@ -450,7 +453,22 @@ export const ResourceOwnerWithName =
             }
 
             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
-                {ownerName} ({uuid})
+                {userFullname} ({uuid})
+            </Typography>;
+        });
+
+export const UserNameFromID =
+    compose(
+        userFromID,
+        withStyles({}, { withTheme: true }))
+        ((props: { uuid: string, userFullname: string, dispatch: Dispatch }) => {
+            const { uuid, userFullname, dispatch } = props;
+
+            if (userFullname === '') {
+                dispatch<any>(loadResource(uuid, false));
+            }
+            return <Typography inline>
+                {userFullname ? userFullname : uuid}
             </Typography>;
         });
 
diff --git a/src/views-components/details-panel/collection-details.tsx b/src/views-components/details-panel/collection-details.tsx
index 0e747fed..3c89a154 100644
--- a/src/views-components/details-panel/collection-details.tsx
+++ b/src/views-components/details-panel/collection-details.tsx
@@ -12,6 +12,7 @@ import { filterResources, getResource } from 'store/resources/resources';
 import { connect } from 'react-redux';
 import { Grid, ListItem, StyleRulesCallback, Typography, withStyles, WithStyles } from '@material-ui/core';
 import { formatDate, formatFileSize } from 'common/formatters';
+import { UserNameFromID } from '../data-explorer/renderers';
 import { Dispatch } from 'redux';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
@@ -106,12 +107,17 @@ const CollectionVersionBrowser = withStyles(styles)(
                             Nr
                         </Typography>
                     </Grid>
-                    <Grid item xs={4}>
+                    <Grid item xs={2}>
                         <Typography variant="caption" className={classes.versionBrowserHeader}>
                             Size
                         </Typography>
                     </Grid>
-                    <Grid item xs={6}>
+                    <Grid item xs={3}>
+                        <Typography variant="caption" className={classes.versionBrowserHeader}>
+                            User
+                        </Typography>
+                    </Grid>
+                    <Grid item xs={5}>
                         <Typography variant="caption" className={classes.versionBrowserHeader}>
                             Date
                         </Typography>
@@ -130,12 +136,17 @@ const CollectionVersionBrowser = withStyles(styles)(
                                     {item.version}
                                 </Typography>
                             </Grid>
-                            <Grid item xs={4}>
+                            <Grid item xs={2}>
                                 <Typography variant="caption" className={classes.versionBrowserItem}>
                                     {formatFileSize(item.fileSizeTotal)}
                                 </Typography>
                             </Grid>
-                            <Grid item xs={6}>
+                            <Grid item xs={3}>
+                                <Typography variant="caption" className={classes.versionBrowserItem}>
+                                    <UserNameFromID uuid={item.modifiedByUserUuid} />
+                                </Typography>
+                            </Grid>
+                            <Grid item xs={5}>
                                 <Typography variant="caption" className={classes.versionBrowserItem}>
                                     {formatDate(item.modifiedAt)}
                                 </Typography>
@@ -145,4 +156,4 @@ const CollectionVersionBrowser = withStyles(styles)(
                 })}
                 </Grid>
             </div>;
-        }));
\ No newline at end of file
+        }));

commit c0bd8013a0941d5a8b0ce0478da025bb8f6bb6c5
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Aug 10 15:19:13 2021 -0400

    17982: Trigger loading saved/recent queries when clicking empty search bar
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/search-bar/search-bar-view.tsx b/src/views-components/search-bar/search-bar-view.tsx
index 7f5c1566..28408347 100644
--- a/src/views-components/search-bar/search-bar-view.tsx
+++ b/src/views-components/search-bar/search-bar-view.tsx
@@ -128,10 +128,10 @@ const handleKeyDown = (e: React.KeyboardEvent, props: SearchBarViewProps) => {
 const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
     if (props.searchValue) {
         props.onSetView(SearchView.AUTOCOMPLETE);
-        props.openSearchView();
     } else {
         props.onSetView(SearchView.BASIC);
     }
+    props.openSearchView();
 };
 
 const handleDropdownClick = (e: React.MouseEvent, props: SearchBarViewProps) => {

commit 49f9e629a1fb1806baecd4eb04b46917c13aa928
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Tue Aug 10 14:27:05 2021 -0300

    17982: Updates integration test cluster's config file to make tests work again.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/views/ssh-key-panel/ssh-key-panel.tsx b/src/views/ssh-key-panel/ssh-key-panel.tsx
index 672b1bf3..24ca3468 100644
--- a/src/views/ssh-key-panel/ssh-key-panel.tsx
+++ b/src/views/ssh-key-panel/ssh-key-panel.tsx
@@ -11,7 +11,7 @@ import { SshKeyPanelRoot, SshKeyPanelRootDataProps, SshKeyPanelRootActionProps }
 
 const mapStateToProps = (state: RootState): SshKeyPanelRootDataProps => {
     const sshKeys = state.auth.sshKeys.filter((key) => {
-      return key.authorizedUserUuid == (state.auth.user ? state.auth.user.uuid : null);
+      return key.authorizedUserUuid === (state.auth.user ? state.auth.user.uuid : null);
     });
 
     return {

commit 8650023dd84a06bb3353b6f0c94e111c0bd192cf
Author: Stephen Smith <stephen at curii.com>
Date:   Sun Aug 8 22:45:17 2021 -0400

    17982: Open basic searchview on click if searchValue is empty
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/search-bar/search-bar-view.tsx b/src/views-components/search-bar/search-bar-view.tsx
index cab53403..7f5c1566 100644
--- a/src/views-components/search-bar/search-bar-view.tsx
+++ b/src/views-components/search-bar/search-bar-view.tsx
@@ -130,7 +130,7 @@ const handleInputClick = (e: React.MouseEvent, props: SearchBarViewProps) => {
         props.onSetView(SearchView.AUTOCOMPLETE);
         props.openSearchView();
     } else {
-        props.closeView();
+        props.onSetView(SearchView.BASIC);
     }
 };
 

commit 41d10c5792a7a866cc5a8f2d49f6b22f1bed24e1
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 2 16:45:55 2021 -0400

    17690: Add guard against undefined user in ssh key page
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/ssh-key-panel/ssh-key-panel.tsx b/src/views/ssh-key-panel/ssh-key-panel.tsx
index 0c86b364..672b1bf3 100644
--- a/src/views/ssh-key-panel/ssh-key-panel.tsx
+++ b/src/views/ssh-key-panel/ssh-key-panel.tsx
@@ -11,7 +11,7 @@ import { SshKeyPanelRoot, SshKeyPanelRootDataProps, SshKeyPanelRootActionProps }
 
 const mapStateToProps = (state: RootState): SshKeyPanelRootDataProps => {
     const sshKeys = state.auth.sshKeys.filter((key) => {
-      return key.authorizedUserUuid == state.auth.user.uuid;
+      return key.authorizedUserUuid == (state.auth.user ? state.auth.user.uuid : null);
     });
 
     return {

commit c154977f29cdd067f7f9316b472cc08c086d29c9
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 2 15:26:05 2021 -0400

    17690: Fix stray accidental copy+paste
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/ssh-key-panel/ssh-key-panel.tsx b/src/views/ssh-key-panel/ssh-key-panel.tsx
index f2b7dd3c..0c86b364 100644
--- a/src/views/ssh-key-panel/ssh-key-panel.tsx
+++ b/src/views/ssh-key-panel/ssh-key-panel.tsx
@@ -10,7 +10,7 @@ import { openSshKeyContextMenu } from 'store/context-menu/context-menu-actions';
 import { SshKeyPanelRoot, SshKeyPanelRootDataProps, SshKeyPanelRootActionProps } from 'views/ssh-key-panel/ssh-key-panel-root';
 
 const mapStateToProps = (state: RootState): SshKeyPanelRootDataProps => {
-    const sshKeys = state.auth.sshKeys = state.auth.sshKeys.filter((key) => {
+    const sshKeys = state.auth.sshKeys.filter((key) => {
       return key.authorizedUserUuid == state.auth.user.uuid;
     });
 

commit 626b653fb1e9c8fffc8873d8e59f6e87f5a1cf83
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 2 15:16:03 2021 -0400

    17690: Filter ssh keys shown in user keys to only current user
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/ssh-key-panel/ssh-key-panel.tsx b/src/views/ssh-key-panel/ssh-key-admin-panel.tsx
similarity index 92%
copy from src/views/ssh-key-panel/ssh-key-panel.tsx
copy to src/views/ssh-key-panel/ssh-key-admin-panel.tsx
index 4d896f3d..72a8c4cb 100644
--- a/src/views/ssh-key-panel/ssh-key-panel.tsx
+++ b/src/views/ssh-key-panel/ssh-key-admin-panel.tsx
@@ -28,4 +28,4 @@ const mapDispatchToProps = (dispatch: Dispatch): SshKeyPanelRootActionProps => (
     }
 });
 
-export const SshKeyPanel = connect(mapStateToProps, mapDispatchToProps)(SshKeyPanelRoot);
+export const SshKeyAdminPanel = connect(mapStateToProps, mapDispatchToProps)(SshKeyPanelRoot);
diff --git a/src/views/ssh-key-panel/ssh-key-panel.tsx b/src/views/ssh-key-panel/ssh-key-panel.tsx
index 4d896f3d..f2b7dd3c 100644
--- a/src/views/ssh-key-panel/ssh-key-panel.tsx
+++ b/src/views/ssh-key-panel/ssh-key-panel.tsx
@@ -10,9 +10,13 @@ import { openSshKeyContextMenu } from 'store/context-menu/context-menu-actions';
 import { SshKeyPanelRoot, SshKeyPanelRootDataProps, SshKeyPanelRootActionProps } from 'views/ssh-key-panel/ssh-key-panel-root';
 
 const mapStateToProps = (state: RootState): SshKeyPanelRootDataProps => {
+    const sshKeys = state.auth.sshKeys = state.auth.sshKeys.filter((key) => {
+      return key.authorizedUserUuid == state.auth.user.uuid;
+    });
+
     return {
-        sshKeys: state.auth.sshKeys,
-        hasKeys: state.auth.sshKeys!.length > 0
+        sshKeys: sshKeys,
+        hasKeys: sshKeys!.length > 0
     };
 };
 
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index b708355c..9ce93bf2 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -45,6 +45,7 @@ import SplitterLayout from 'react-splitter-layout';
 import { WorkflowPanel } from 'views/workflow-panel/workflow-panel';
 import { SearchResultsPanel } from 'views/search-results-panel/search-results-panel';
 import { SshKeyPanel } from 'views/ssh-key-panel/ssh-key-panel';
+import { SshKeyAdminPanel } from 'views/ssh-key-panel/ssh-key-admin-panel';
 import { SiteManagerPanel } from "views/site-manager-panel/site-manager-panel";
 import { MyAccountPanel } from 'views/my-account-panel/my-account-panel';
 import { SharingDialog } from 'views-components/sharing-dialog/sharing-dialog';
@@ -164,7 +165,7 @@ let routes = <>
     <Route path={Routes.VIRTUAL_MACHINES_ADMIN} component={VirtualMachineAdminPanel} />
     <Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
     <Route path={Routes.SSH_KEYS_USER} component={SshKeyPanel} />
-    <Route path={Routes.SSH_KEYS_ADMIN} component={SshKeyPanel} />
+    <Route path={Routes.SSH_KEYS_ADMIN} component={SshKeyAdminPanel} />
     <Route path={Routes.SITE_MANAGER} component={SiteManagerPanel} />
     <Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
     <Route path={Routes.USERS} component={UserPanel} />

commit 57f62c90bb7b7a89ad8ac040812989f140fb637f
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Aug 3 16:29:00 2021 -0400

    17564: Change file size unit base from 1000 to 1024
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/common/formatters.ts b/src/common/formatters.ts
index eeab703d..779809f1 100644
--- a/src/common/formatters.ts
+++ b/src/common/formatters.ts
@@ -66,19 +66,19 @@ export function formatUploadSpeed(prevLoaded: number, loaded: number, prevTime:
 
 const FILE_SIZES = [
     {
-        base: 1000000000000,
+        base: 1099511627776,
         unit: "TB"
     },
     {
-        base: 1000000000,
+        base: 1073741824,
         unit: "GB"
     },
     {
-        base: 1000000,
+        base: 1048576,
         unit: "MB"
     },
     {
-        base: 1000,
+        base: 1024,
         unit: "KB"
     },
     {

commit eb9f55920f1a8ed73e0d931add118501c713d274
Author: Ward Vandewege <ward at curii.com>
Date:   Wed Aug 4 12:58:10 2021 -0400

    Update the package distribution target list: add debian11, remove
    debian8, ubuntu1404, ubuntu1604.
    
    No issue #
    
    Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward at curii.com>

diff --git a/Makefile b/Makefile
index 3df3c78c..aaf2271c 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ VERSION?=$(shell ./version-at-commit.sh HEAD)
 # changes in the package. (i.e. example config files externally added
 ITERATION?=1
 
-TARGETS?=centos7 debian8 debian10 ubuntu1404 ubuntu1604 ubuntu1804 ubuntu2004
+TARGETS?=centos7 debian10 debian11 ubuntu1804 ubuntu2004
 
 ARVADOS_DIRECTORY?=unset
 

commit e16c61adea5e8eccd1236d8608a750cd7cb565df
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 2 14:25:10 2021 -0400

    17691: Add unit tests for isRsaKey
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/validators/is-rsa-key.test.tsx b/src/validators/is-rsa-key.test.tsx
new file mode 100644
index 00000000..067d7744
--- /dev/null
+++ b/src/validators/is-rsa-key.test.tsx
@@ -0,0 +1,39 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { isRsaKey } from './is-rsa-key';
+
+describe('rsa-key-validator', () => {
+    const rsaKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDPpavAS1wUq2+j7PgwkDS+9lm43AkdGxZo+T8qm6ZcB009EUEXya3lQolA52gg/i5aGZg4LT3t1OKxbsaClMd7sNZXYrMW9vd/utvGgAlNEbE/yXsEl2kpxt8lz7RI1XLnoWcV+aKyrsiKdrMKnZyG8CBxKdtzxHzWRl4N1BGrFJf/RnUWJv2VvM/h4/O+KXIjFokPkJ1F8yQChp5OKGkBKGXQ1vV4LjXqEXGVlgiQFM4U2NvCA8hXQR8mYm1vOsTYJzoSsnb+ewbXlVH5d7XsR5S2ULOr88vuYN/P4DF/Q3pEBi7BOyee61P3eHvhCNtb+jQMt59Vj/96y5C/reTMRo2R3B4bmX+Zxr3+DCC5tO1y+U5V39fu7cweimKXc78QDGGAVN0kz4P6P137b5WkCYIozeiBvWRsbGIlHjlGu9+0WuotdluD+OrTguuZ2zr8f32ijddO6y0J+aIdmTxQPxtmcQuRtpRfquoJGLhWAJH6mNZKbWkqqVfd5BA0TYs=';
+    const badKey = 'ssh-rsa bad'
+
+    const ERROR_MESSAGE = 'Public key is invalid';
+
+    describe('rsaKeyValidation', () => {
+        it('should accept keys with comment', () => {
+            // then
+            expect(isRsaKey(rsaKey + " firstlast at example.com")).toBeUndefined();
+        });
+
+        it('should accept keys without comment', () => {
+            // then
+            expect(isRsaKey(rsaKey)).toBeUndefined();
+        });
+
+        it('should reject keys with trailing whitespace', () => {
+            // then
+            expect(isRsaKey(rsaKey + " ")).toBe(ERROR_MESSAGE);
+            expect(isRsaKey(rsaKey + "\n")).toBe(ERROR_MESSAGE);
+            expect(isRsaKey(rsaKey + "\r\n")).toBe(ERROR_MESSAGE);
+            expect(isRsaKey(rsaKey + "\t")).toBe(ERROR_MESSAGE);
+        });
+
+        it('should reject invalid keys', () => {
+            // then
+            expect(isRsaKey(badKey)).toBe(ERROR_MESSAGE);
+        });
+
+    });
+
+});

commit 2352fda90d2bf2e7d3b09c3f50014b21a8a65ebb
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 2 09:35:17 2021 -0400

    17691: Relax ssh key frontend validation to accept keys without comment
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/validators/is-rsa-key.tsx b/src/validators/is-rsa-key.tsx
index 7620a801..d41b0929 100644
--- a/src/validators/is-rsa-key.tsx
+++ b/src/validators/is-rsa-key.tsx
@@ -6,5 +6,5 @@
 const ERROR_MESSAGE = 'Public key is invalid';
 
 export const isRsaKey = (value: any) => {
-    return value.match(/ssh-rsa AAAA[0-9A-Za-z+/]+[=]{0,3} ([^@]+@[^@]+)/i) ? undefined : ERROR_MESSAGE;
+    return value.match(/ssh-rsa AAAA[0-9A-Za-z+/]+[=]{0,3}(( [^@]+@[^@]+)|$)/i) ? undefined : ERROR_MESSAGE;
 };

commit 7edfda6a34df2bdd6cb21f22a952f3070de58866
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 2 12:38:59 2021 -0400

    17526: Remove filename placeholder from webdav dialog curl command
    
    Also add a note to add filenames at the end of the curl URL
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index 49283813..8e9edac1 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -148,7 +148,7 @@ export const WebDavS3InfoDialog = compose(
         }
 
         const wgetCommand = `wget --http-user=${props.data.username} --http-passwd=${props.data.token} --mirror --no-parent --no-host --cut-dirs=0 ${winDav.toString()}`;
-        const curlCommand = `curl -O -u ${props.data.username}:${props.data.token} ${winDav.toString()}<FILENAME>`;
+        const curlCommand = `curl -O -u ${props.data.username}:${props.data.token} ${winDav.toString()}`;
 
         return <Dialog
             open={props.open}
@@ -228,7 +228,7 @@ export const WebDavS3InfoDialog = compose(
                         Download Cyber/Mountain Duck bookmark
                     </Button>
 
-                    <h3>Gnome</h3>
+                    <h3>GNOME</h3>
                     <ol>
                         <li>Open Files</li>
                         <li>Select +Other Locations</li>
@@ -278,6 +278,11 @@ export const WebDavS3InfoDialog = compose(
                             lines={[curlCommand]} />
                     </DetailsAttribute>
 
+                    <p>
+                      Note: This curl command downloads single files.
+                      Append the desired filename to the end of the URL.
+                    </p>
+
                 </TabPanel>
 
             </div>

commit fffe0e4cf71da4c8d2cdca2db958fa6403d0e5ec
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Jul 29 11:07:14 2021 -0400

    17526: Remove redundant user/pass from webdav wget tab and add curl command
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index 7255e756..49283813 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -148,6 +148,7 @@ export const WebDavS3InfoDialog = compose(
         }
 
         const wgetCommand = `wget --http-user=${props.data.username} --http-passwd=${props.data.token} --mirror --no-parent --no-host --cut-dirs=0 ${winDav.toString()}`;
+        const curlCommand = `curl -O -u ${props.data.username}:${props.data.token} ${winDav.toString()}<FILENAME>`;
 
         return <Dialog
             open={props.open}
@@ -270,14 +271,12 @@ export const WebDavS3InfoDialog = compose(
                     </DetailsAttribute>
 
                     <DetailsAttribute
-                        label='Username'
-                        value={props.data.username}
-                        copyValue={props.data.username} />
-
-                    <DetailsAttribute
-                        label='Password'
-                        value={props.data.token}
-                        copyValue={props.data.token} />
+                        label='Curl command'
+                        copyValue={curlCommand}
+                        classValue={props.classes.detailsAttrValWithCode}>
+                        <DefaultCodeSnippet
+                            lines={[curlCommand]} />
+                    </DetailsAttribute>
 
                 </TabPanel>
 

commit 7ffd14cca42d5e2c75722f916f5362b32d9a07be
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Jul 29 10:50:49 2021 -0400

    17526: Use codesnippet component on webdav dialog wget command for monospace
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index 73c89621..7255e756 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -10,8 +10,9 @@ import { WithDialogProps } from 'store/dialog/with-dialog';
 import { compose } from 'redux';
 import { DetailsAttribute } from "components/details-attribute/details-attribute";
 import { DownloadIcon } from "components/icon/icon";
+import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
 
-export type CssRules = 'details' | 'downloadButton';
+export type CssRules = 'details' | 'downloadButton' | 'detailsAttrValWithCode';
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     details: {
@@ -20,6 +21,10 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     },
     downloadButton: {
         marginTop: theme.spacing.unit * 2,
+    },
+    detailsAttrValWithCode: {
+        display: "flex",
+        alignItems: "center",
     }
 });
 
@@ -258,8 +263,11 @@ export const WebDavS3InfoDialog = compose(
 
                     <DetailsAttribute
                         label='Wget command'
-                        value={wgetCommand}
-                        copyValue={wgetCommand} />
+                        copyValue={wgetCommand}
+                        classValue={props.classes.detailsAttrValWithCode}>
+                        <DefaultCodeSnippet
+                            lines={[wgetCommand]} />
+                    </DetailsAttribute>
 
                     <DetailsAttribute
                         label='Username'

commit 8d954ff99ea4dfda2588c17314dd64f378871ee0
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Jul 28 16:55:01 2021 -0400

    17526: Fix webdav dialog > wget tab command copy button only copying url
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index 50d50944..73c89621 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -142,6 +142,8 @@ export const WebDavS3InfoDialog = compose(
             activeTab = 2;
         }
 
+        const wgetCommand = `wget --http-user=${props.data.username} --http-passwd=${props.data.token} --mirror --no-parent --no-host --cut-dirs=0 ${winDav.toString()}`;
+
         return <Dialog
             open={props.open}
             maxWidth="md"
@@ -256,8 +258,8 @@ export const WebDavS3InfoDialog = compose(
 
                     <DetailsAttribute
                         label='Wget command'
-                        value={`wget --http-user=${props.data.username} --http-passwd=${props.data.token} --mirror --no-parent --no-host --cut-dirs=0 ${winDav.toString()}`}
-                        copyValue={winDav.toString()} />
+                        value={wgetCommand}
+                        copyValue={wgetCommand} />
 
                     <DetailsAttribute
                         label='Username'

commit 2953ffd61e451f6bb75bf51489ceeb983e774626
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Jul 28 16:45:17 2021 -0400

    17526: Rename supportsWebdav to isCollection
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index 16fe2526..50d50944 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -135,10 +135,10 @@ export const WebDavS3InfoDialog = compose(
             tokenSecret = tokenUuid;
         }
 
-        const supportsWebdav = (props.data.uuid.indexOf("-4zz18-") === 5);
+        const isCollection = (props.data.uuid.indexOf("-4zz18-") === 5);
 
         let activeTab = props.data.activeTab;
-        if (!supportsWebdav) {
+        if (!isCollection) {
             activeTab = 2;
         }
 
@@ -151,10 +151,10 @@ export const WebDavS3InfoDialog = compose(
                 title={`Open with 3rd party client`} />
             <div className={props.classes.details} >
                 <Tabs value={activeTab} onChange={props.data.setActiveTab}>
-                    {supportsWebdav && <Tab value={0} key="cyberduck" label="WebDAV" />}
-                    {supportsWebdav && <Tab value={1} key="windows" label="Windows or MacOS" />}
+                    {isCollection && <Tab value={0} key="cyberduck" label="WebDAV" />}
+                    {isCollection && <Tab value={1} key="windows" label="Windows or MacOS" />}
                     <Tab value={2} key="s3" label="S3 bucket" />
-                    {supportsWebdav && <Tab value={3} key="cli" label="wget / curl" />}
+                    {isCollection && <Tab value={3} key="cli" label="wget / curl" />}
                 </Tabs>
 
                 <TabPanel index={1} value={activeTab}>

commit acf4d6c3197384510ab18f4aa776b283480458ea
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Jul 28 16:43:05 2021 -0400

    17526: Change webdav dialog title to "Open" to preserve ordering
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index 49a79d1c..f1e337de 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -43,7 +43,7 @@ describe('Collection panel tests', function () {
             cy.goToPath(`/collections/${testCollection.uuid}`);
 
             cy.get('[data-cy=collection-panel-options-btn]').click();
-            cy.get('[data-cy=context-menu]').contains('Access with 3rd party client').click();
+            cy.get('[data-cy=context-menu]').contains('Open with 3rd party client').click();
             cy.get('[data-cy=download-button').click();
 
             const filename = path.join(downloadsFolder, `${testCollection.name}.duck`);
diff --git a/src/store/collections/collection-info-actions.ts b/src/store/collections/collection-info-actions.ts
index 4838481e..6107c409 100644
--- a/src/store/collections/collection-info-actions.ts
+++ b/src/store/collections/collection-info-actions.ts
@@ -29,7 +29,7 @@ export const openWebDavS3InfoDialog = (uuid: string, activeTab?: number) =>
         dispatch(dialogActions.OPEN_DIALOG({
             id: COLLECTION_WEBDAV_S3_DIALOG_NAME,
             data: {
-                title: 'Access with 3rd party client',
+                title: 'Open with 3rd party client',
                 token: getState().auth.extraApiToken || getState().auth.apiToken,
                 downloadUrl: getState().auth.config.keepWebServiceUrl,
                 collectionsUrl: getState().auth.config.keepWebInlineServiceUrl,
diff --git a/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts
index c4c8788f..9b0efac0 100644
--- a/src/views-components/context-menu/action-sets/collection-action-set.ts
+++ b/src/views-components/context-menu/action-sets/collection-action-set.ts
@@ -90,7 +90,7 @@ export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
     toggleFavoriteAction,
     {
         icon: FolderSharedIcon,
-        name: "Access with 3rd party client",
+        name: "Open with 3rd party client",
         execute: (dispatch, resource) => {
             dispatch<any>(openWebDavS3InfoDialog(resource.uuid));
         }
diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts
index 02a6731e..a079bf4f 100644
--- a/src/views-components/context-menu/action-sets/project-action-set.ts
+++ b/src/views-components/context-menu/action-sets/project-action-set.ts
@@ -59,7 +59,7 @@ export const readOnlyProjectActionSet: ContextMenuActionSet = [[
     },
     {
         icon: FolderSharedIcon,
-        name: "Access with 3rd party client",
+        name: "Open with 3rd party client",
         execute: (dispatch, resource) => {
             dispatch<any>(openWebDavS3InfoDialog(resource.uuid));
         }
diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index d6055852..16fe2526 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -148,7 +148,7 @@ export const WebDavS3InfoDialog = compose(
             onClose={props.closeDialog}
             style={{ alignSelf: 'stretch' }}>
             <CardHeader
-                title={`Access with 3rd party client`} />
+                title={`Open with 3rd party client`} />
             <div className={props.classes.details} >
                 <Tabs value={activeTab} onChange={props.data.setActiveTab}>
                     {supportsWebdav && <Tab value={0} key="cyberduck" label="WebDAV" />}

commit a8e2b7c927f403b8c6b497bbefd3106e3b6db7ef
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Jul 28 10:17:38 2021 -0400

    17526: Add wget/curl tab to webdav dialog
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index 267d4412..d6055852 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -154,6 +154,7 @@ export const WebDavS3InfoDialog = compose(
                     {supportsWebdav && <Tab value={0} key="cyberduck" label="WebDAV" />}
                     {supportsWebdav && <Tab value={1} key="windows" label="Windows or MacOS" />}
                     <Tab value={2} key="s3" label="S3 bucket" />
+                    {supportsWebdav && <Tab value={3} key="cli" label="wget / curl" />}
                 </Tabs>
 
                 <TabPanel index={1} value={activeTab}>
@@ -251,6 +252,25 @@ export const WebDavS3InfoDialog = compose(
 
                 </TabPanel>
 
+                <TabPanel index={3} value={activeTab}>
+
+                    <DetailsAttribute
+                        label='Wget command'
+                        value={`wget --http-user=${props.data.username} --http-passwd=${props.data.token} --mirror --no-parent --no-host --cut-dirs=0 ${winDav.toString()}`}
+                        copyValue={winDav.toString()} />
+
+                    <DetailsAttribute
+                        label='Username'
+                        value={props.data.username}
+                        copyValue={props.data.username} />
+
+                    <DetailsAttribute
+                        label='Password'
+                        value={props.data.token}
+                        copyValue={props.data.token} />
+
+                </TabPanel>
+
             </div>
             <DialogActions>
                 <Button

commit 8451c622fdc4321f7c19c1c09975263a19a7023c
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Jul 28 09:56:57 2021 -0400

    17526: Rename cyberduck tab to webdav, mention credentials in mac/win tab
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index 8e82619c..267d4412 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -151,7 +151,7 @@ export const WebDavS3InfoDialog = compose(
                 title={`Access with 3rd party client`} />
             <div className={props.classes.details} >
                 <Tabs value={activeTab} onChange={props.data.setActiveTab}>
-                    {supportsWebdav && <Tab value={0} key="cyberduck" label="Cyberduck/Mountain Duck or Gnome Files" />}
+                    {supportsWebdav && <Tab value={0} key="cyberduck" label="WebDAV" />}
                     {supportsWebdav && <Tab value={1} key="windows" label="Windows or MacOS" />}
                     <Tab value={2} key="s3" label="S3 bucket" />
                 </Tabs>
@@ -179,12 +179,14 @@ export const WebDavS3InfoDialog = compose(
                         <li>Open File Explorer</li>
                         <li>Click on "This PC", then go to Computer → Add a Network Location</li>
                         <li>Click Next, then choose "Add a custom network location", then click Next</li>
+                        <li>Use the "internet address" and credentials listed under Settings, above</li>
                     </ol>
 
                     <h3>MacOS</h3>
                     <ol>
                         <li>Open Finder</li>
                         <li>Click Go → Connect to server</li>
+                        <li>Use the "internet address" and credentials listed under Settings, above</li>
                     </ol>
                 </TabPanel>
 
@@ -204,6 +206,8 @@ export const WebDavS3InfoDialog = compose(
                         value={props.data.token}
                         copyValue={props.data.token} />
 
+                    <h3>Cyberduck/Mountain Duck</h3>
+
                     <Button
                         data-cy='download-button'
                         className={props.classes.downloadButton}

commit f4cb3271191ef9a4978556caec65c146f0d21160
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Jul 28 09:55:45 2021 -0400

    17526: Rename webdav dialong to Access with 3rd party client
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js
index f3b63218..49a79d1c 100644
--- a/cypress/integration/collection.spec.js
+++ b/cypress/integration/collection.spec.js
@@ -43,7 +43,7 @@ describe('Collection panel tests', function () {
             cy.goToPath(`/collections/${testCollection.uuid}`);
 
             cy.get('[data-cy=collection-panel-options-btn]').click();
-            cy.get('[data-cy=context-menu]').contains('Open as network folder or S3 bucket').click();
+            cy.get('[data-cy=context-menu]').contains('Access with 3rd party client').click();
             cy.get('[data-cy=download-button').click();
 
             const filename = path.join(downloadsFolder, `${testCollection.name}.duck`);
diff --git a/src/store/collections/collection-info-actions.ts b/src/store/collections/collection-info-actions.ts
index 9f82975f..4838481e 100644
--- a/src/store/collections/collection-info-actions.ts
+++ b/src/store/collections/collection-info-actions.ts
@@ -29,7 +29,7 @@ export const openWebDavS3InfoDialog = (uuid: string, activeTab?: number) =>
         dispatch(dialogActions.OPEN_DIALOG({
             id: COLLECTION_WEBDAV_S3_DIALOG_NAME,
             data: {
-                title: 'Access Collection using WebDAV or S3',
+                title: 'Access with 3rd party client',
                 token: getState().auth.extraApiToken || getState().auth.apiToken,
                 downloadUrl: getState().auth.config.keepWebServiceUrl,
                 collectionsUrl: getState().auth.config.keepWebInlineServiceUrl,
diff --git a/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts
index 5c66f128..c4c8788f 100644
--- a/src/views-components/context-menu/action-sets/collection-action-set.ts
+++ b/src/views-components/context-menu/action-sets/collection-action-set.ts
@@ -90,7 +90,7 @@ export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
     toggleFavoriteAction,
     {
         icon: FolderSharedIcon,
-        name: "Open as network folder or S3 bucket",
+        name: "Access with 3rd party client",
         execute: (dispatch, resource) => {
             dispatch<any>(openWebDavS3InfoDialog(resource.uuid));
         }
diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts
index c8471138..02a6731e 100644
--- a/src/views-components/context-menu/action-sets/project-action-set.ts
+++ b/src/views-components/context-menu/action-sets/project-action-set.ts
@@ -59,7 +59,7 @@ export const readOnlyProjectActionSet: ContextMenuActionSet = [[
     },
     {
         icon: FolderSharedIcon,
-        name: "Open as network folder or S3 bucket",
+        name: "Access with 3rd party client",
         execute: (dispatch, resource) => {
             dispatch<any>(openWebDavS3InfoDialog(resource.uuid));
         }
diff --git a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
index 39c1068e..8e82619c 100644
--- a/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
+++ b/src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
@@ -148,7 +148,7 @@ export const WebDavS3InfoDialog = compose(
             onClose={props.closeDialog}
             style={{ alignSelf: 'stretch' }}>
             <CardHeader
-                title={`Open as Network Folder or S3 Bucket`} />
+                title={`Access with 3rd party client`} />
             <div className={props.classes.details} >
                 <Tabs value={activeTab} onChange={props.data.setActiveTab}>
                     {supportsWebdav && <Tab value={0} key="cyberduck" label="Cyberduck/Mountain Duck or Gnome Files" />}

commit efbe3df438e43ac3f620a37da60ebc18ce3f495c
Author: Stephen Smith <stephen at curii.com>
Date:   Fri Jul 30 01:40:17 2021 -0400

    17951: Remove compute node ui and associated code
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/index.tsx b/src/index.tsx
index b1eca99e..2d62194b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -51,7 +51,6 @@ import { keepServiceActionSet } from 'views-components/context-menu/action-sets/
 import { loadVocabulary } from 'store/vocabulary/vocabulary-actions';
 import { virtualMachineActionSet } from 'views-components/context-menu/action-sets/virtual-machine-action-set';
 import { userActionSet } from 'views-components/context-menu/action-sets/user-action-set';
-import { computeNodeActionSet } from 'views-components/context-menu/action-sets/compute-node-action-set';
 import { apiClientAuthorizationActionSet } from 'views-components/context-menu/action-sets/api-client-authorization-action-set';
 import { groupActionSet } from 'views-components/context-menu/action-sets/group-action-set';
 import { groupMemberActionSet } from 'views-components/context-menu/action-sets/group-member-action-set';
@@ -92,7 +91,6 @@ addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet);
 addMenuActionSet(ContextMenuKind.KEEP_SERVICE, keepServiceActionSet);
 addMenuActionSet(ContextMenuKind.USER, userActionSet);
 addMenuActionSet(ContextMenuKind.LINK, linkActionSet);
-addMenuActionSet(ContextMenuKind.NODE, computeNodeActionSet);
 addMenuActionSet(ContextMenuKind.API_CLIENT_AUTHORIZATION, apiClientAuthorizationActionSet);
 addMenuActionSet(ContextMenuKind.GROUPS, groupActionSet);
 addMenuActionSet(ContextMenuKind.GROUP_MEMBER, groupMemberActionSet);
diff --git a/src/models/resource.ts b/src/models/resource.ts
index 371278e5..c94c4b25 100644
--- a/src/models/resource.ts
+++ b/src/models/resource.ts
@@ -32,7 +32,6 @@ export enum ResourceKind {
     GROUP = "arvados#group",
     LINK = "arvados#link",
     LOG = "arvados#log",
-    NODE = "arvados#node",
     PROCESS = "arvados#containerRequest",
     PROJECT = "arvados#group",
     REPOSITORY = "arvados#repository",
@@ -57,8 +56,7 @@ export enum ResourceObjectType {
     VIRTUAL_MACHINE = '2x53u',
     WORKFLOW = '7fd4e',
     SSH_KEY = 'fngyi',
-    KEEP_SERVICE = 'bi6l4',
-    NODE = '7ekkf'
+    KEEP_SERVICE = 'bi6l4'
 }
 
 export const RESOURCE_UUID_PATTERN = '[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}';
@@ -101,8 +99,6 @@ export const extractUuidKind = (uuid: string = '') => {
             return ResourceKind.SSH_KEY;
         case ResourceObjectType.KEEP_SERVICE:
             return ResourceKind.KEEP_SERVICE;
-        case ResourceObjectType.NODE:
-            return ResourceKind.NODE;
         case ResourceObjectType.API_CLIENT_AUTHORIZATION:
             return ResourceKind.API_CLIENT_AUTHORIZATION;
         case ResourceObjectType.LINK:
diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts
index 6d171e04..70f65cb4 100644
--- a/src/routes/route-change-handlers.ts
+++ b/src/routes/route-change-handlers.ts
@@ -39,7 +39,6 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const sshKeysAdminMatch = Routes.matchSshKeysAdminRoute(pathname);
     const siteManagerMatch = Routes.matchSiteManagerRoute(pathname);
     const keepServicesMatch = Routes.matchKeepServicesRoute(pathname);
-    const computeNodesMatch = Routes.matchComputeNodesRoute(pathname);
     const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname);
     const myAccountMatch = Routes.matchMyAccountRoute(pathname);
     const linkAccountMatch = Routes.matchLinkAccountRoute(pathname);
@@ -98,8 +97,6 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(WorkbenchActions.loadSiteManager);
     } else if (keepServicesMatch) {
         store.dispatch(WorkbenchActions.loadKeepServices);
-    } else if (computeNodesMatch) {
-        store.dispatch(WorkbenchActions.loadComputeNodes);
     } else if (apiClientAuthorizationsMatch) {
         store.dispatch(WorkbenchActions.loadApiClientAuthorizations);
     } else if (myAccountMatch) {
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index d9da0234..528a0376 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -39,7 +39,6 @@ export const Routes = {
     MY_ACCOUNT: '/my-account',
     LINK_ACCOUNT: '/link_account',
     KEEP_SERVICES: `/keep-services`,
-    COMPUTE_NODES: `/nodes`,
     USERS: '/users',
     API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations`,
     GROUPS: '/groups',
@@ -176,9 +175,6 @@ export const matchFedTokenRoute = (route: string) =>
 export const matchUsersRoute = (route: string) =>
     matchPath(route, { path: Routes.USERS });
 
-export const matchComputeNodesRoute = (route: string) =>
-    matchPath(route, { path: Routes.COMPUTE_NODES });
-
 export const matchApiClientAuthorizationsRoute = (route: string) =>
     matchPath(route, { path: Routes.API_CLIENT_AUTHORIZATIONS });
 
diff --git a/src/services/node-service/node-service.ts b/src/services/node-service/node-service.ts
deleted file mode 100644
index 0cf1a83b..00000000
--- a/src/services/node-service/node-service.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { AxiosInstance } from "axios";
-import { CommonResourceService } from "services/common-service/common-resource-service";
-import { NodeResource } from 'models/node';
-import { ApiActions } from 'services/api/api-actions';
-
-export class NodeService extends CommonResourceService<NodeResource> {
-    constructor(serverApi: AxiosInstance, actions: ApiActions) {
-        super(serverApi, "nodes", actions);
-    }
-} 
\ No newline at end of file
diff --git a/src/services/services.ts b/src/services/services.ts
index b9118981..2afb843f 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -29,7 +29,6 @@ import { VirtualMachinesService } from "services/virtual-machines-service/virtua
 import { RepositoriesService } from 'services/repositories-service/repositories-service';
 import { AuthorizedKeysService } from 'services/authorized-keys-service/authorized-keys-service';
 import { VocabularyService } from 'services/vocabulary-service/vocabulary-service';
-import { NodeService } from 'services/node-service/node-service';
 import { FileViewersConfigService } from 'services/file-viewers-config-service/file-viewers-config-service';
 import { LinkAccountService } from "./link-account-service/link-account-service";
 import parse from "parse-duration";
@@ -69,7 +68,6 @@ export const createServices = (config: Config, actions: ApiActions, useApiClient
     const keepService = new KeepService(apiClient, actions);
     const linkService = new LinkService(apiClient, actions);
     const logService = new LogService(apiClient, actions);
-    const nodeService = new NodeService(apiClient, actions);
     const permissionService = new PermissionService(apiClient, actions);
     const projectService = new ProjectService(apiClient, actions);
     const repositoriesService = new RepositoriesService(apiClient, actions);
@@ -106,7 +104,6 @@ export const createServices = (config: Config, actions: ApiActions, useApiClient
         keepService,
         linkService,
         logService,
-        nodeService,
         permissionService,
         projectService,
         repositoriesService,
diff --git a/src/store/advanced-tab/advanced-tab.tsx b/src/store/advanced-tab/advanced-tab.tsx
index cf30669d..0f8bf3cb 100644
--- a/src/store/advanced-tab/advanced-tab.tsx
+++ b/src/store/advanced-tab/advanced-tab.tsx
@@ -21,7 +21,6 @@ import { VirtualMachinesResource } from 'models/virtual-machines';
 import { UserResource } from 'models/user';
 import { LinkResource } from 'models/link';
 import { KeepServiceResource } from 'models/keep-services';
-import { NodeResource } from 'models/node';
 import { ApiClientAuthorization } from 'models/api-client-authorization';
 import React from 'react';
 
@@ -76,7 +75,6 @@ enum ResourcePrefix {
     AUTORIZED_KEYS = 'authorized_keys',
     VIRTUAL_MACHINES = 'virtual_machines',
     KEEP_SERVICES = 'keep_services',
-    COMPUTE_NODES = 'nodes',
     USERS = 'users',
     API_CLIENT_AUTHORIZATIONS = 'api_client_authorizations',
     LINKS = 'links'
@@ -92,11 +90,6 @@ enum UserData {
     USERNAME = 'username'
 }
 
-enum ComputeNodeData {
-    COMPUTE_NODE = 'node',
-    PROPERTIES = 'properties'
-}
-
 enum ApiClientAuthorizationsData {
     API_CLIENT_AUTHORIZATION = 'api_client_authorization',
     DEFAULT_OWNER_UUID = 'default_owner_uuid'
@@ -107,9 +100,9 @@ enum LinkData {
     PROPERTIES = 'properties'
 }
 
-type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData | ApiClientAuthorizationsData | UserData | LinkData;
+type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ApiClientAuthorizationsData | UserData | LinkData;
 type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix;
-type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | ApiClientAuthorization | UserResource | LinkResource | undefined;
+type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | ApiClientAuthorization | UserResource | LinkResource | undefined;
 
 export const openAdvancedTabDialog = (uuid: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
@@ -241,22 +234,6 @@ export const openAdvancedTabDialog = (uuid: string) =>
                 });
                 dispatch<any>(initAdvancedTabDialog(advanceDataUser));
                 break;
-            case ResourceKind.NODE:
-                const computeNodeResources = getState().resources;
-                const dataComputeNode = getResource<NodeResource>(uuid)(computeNodeResources);
-                const advanceDataComputeNode = advancedTabData({
-                    uuid,
-                    metadata: '',
-                    user: '',
-                    apiResponseKind: computeNodeApiResponse,
-                    data: dataComputeNode,
-                    resourceKind: ComputeNodeData.COMPUTE_NODE,
-                    resourcePrefix: ResourcePrefix.COMPUTE_NODES,
-                    resourceKindProperty: ComputeNodeData.PROPERTIES,
-                    property: dataComputeNode ? dataComputeNode.properties : {}
-                });
-                dispatch<any>(initAdvancedTabDialog(advanceDataComputeNode));
-                break;
             case ResourceKind.API_CLIENT_AUTHORIZATION:
                 const apiClientAuthorizationResources = getState().resources;
                 const dataApiClientAuthorization = getResource<ApiClientAuthorization>(uuid)(apiClientAuthorizationResources);
@@ -578,32 +555,6 @@ const userApiResponse = (apiResponse: UserResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const computeNodeApiResponse = (apiResponse: NodeResource) => {
-    const {
-        uuid, slotNumber, hostname, domain, ipAddress, firstPingAt, lastPingAt, jobUuid,
-        ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid,
-        properties, info
-    } = apiResponse;
-    const response = `
-"uuid": "${uuid}",
-"owner_uuid": "${ownerUuid}",
-"modified_by_client_uuid": ${stringify(modifiedByClientUuid)},
-"modified_by_user_uuid": ${stringify(modifiedByUserUuid)},
-"modified_at": ${stringify(modifiedAt)},
-"created_at": "${createdAt}",
-"slot_number": "${stringify(slotNumber)}",
-"hostname": "${stringify(hostname)}",
-"domain": "${stringify(domain)}",
-"ip_address": "${stringify(ipAddress)}",
-"first_ping_at": "${stringify(firstPingAt)}",
-"last_ping_at": "${stringify(lastPingAt)}",
-"job_uuid": "${stringify(jobUuid)}",
-"properties": "${JSON.stringify(properties, null, 2)}",
-"info": "${JSON.stringify(info, null, 2)}"`;
-
-    return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
-};
-
 const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization) => {
     const {
         uuid, ownerUuid, apiToken, apiClientId, userId, createdByIpAddress, lastUsedByIpAddress,
diff --git a/src/store/compute-nodes/compute-nodes-actions.ts b/src/store/compute-nodes/compute-nodes-actions.ts
deleted file mode 100644
index 25d3bad3..00000000
--- a/src/store/compute-nodes/compute-nodes-actions.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Dispatch } from "redux";
-import { RootState } from 'store/store';
-import { setBreadcrumbs } from 'store/breadcrumbs/breadcrumbs-actions';
-import { dialogActions } from 'store/dialog/dialog-actions';
-import {snackbarActions, SnackbarKind} from 'store/snackbar/snackbar-actions';
-import { navigateToRootProject } from 'store/navigation/navigation-action';
-import { bindDataExplorerActions } from 'store/data-explorer/data-explorer-action';
-import { getResource } from 'store/resources/resources';
-import { ServiceRepository } from "services/services";
-import { NodeResource } from 'models/node';
-
-export const COMPUTE_NODE_PANEL_ID = "computeNodeId";
-export const computeNodesActions = bindDataExplorerActions(COMPUTE_NODE_PANEL_ID);
-
-export const COMPUTE_NODE_REMOVE_DIALOG = 'computeNodeRemoveDialog';
-export const COMPUTE_NODE_ATTRIBUTES_DIALOG = 'computeNodeAttributesDialog';
-
-export const loadComputeNodesPanel = () =>
-    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const user = getState().auth.user;
-        if (user && user.isAdmin) {
-            try {
-                dispatch(setBreadcrumbs([{ label: 'Compute Nodes' }]));
-                dispatch(computeNodesActions.REQUEST_ITEMS());
-            } catch (e) {
-                return;
-            }
-        } else {
-            dispatch(navigateToRootProject);
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "You don't have permissions to view this page", hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        }
-    };
-
-export const openComputeNodeAttributesDialog = (uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        const { resources } = getState();
-        const computeNode = getResource<NodeResource>(uuid)(resources);
-        dispatch(dialogActions.OPEN_DIALOG({ id: COMPUTE_NODE_ATTRIBUTES_DIALOG, data: { computeNode } }));
-    };
-
-export const openComputeNodeRemoveDialog = (uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        dispatch(dialogActions.OPEN_DIALOG({
-            id: COMPUTE_NODE_REMOVE_DIALOG,
-            data: {
-                title: 'Remove compute node',
-                text: 'Are you sure you want to remove this compute node?',
-                confirmButtonLabel: 'Remove',
-                uuid
-            }
-        }));
-    };
-
-export const removeComputeNode = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
-        try {
-            await services.nodeService.delete(uuid);
-            dispatch(computeNodesActions.REQUEST_ITEMS());
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Compute node has been successfully removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-        } catch (e) {
-            return;
-        }
-    };
\ No newline at end of file
diff --git a/src/store/compute-nodes/compute-nodes-middleware-service.ts b/src/store/compute-nodes/compute-nodes-middleware-service.ts
deleted file mode 100644
index bdd728aa..00000000
--- a/src/store/compute-nodes/compute-nodes-middleware-service.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ServiceRepository } from 'services/services';
-import { MiddlewareAPI, Dispatch } from 'redux';
-import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from 'store/data-explorer/data-explorer-middleware-service';
-import { RootState } from 'store/store';
-import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-import { DataExplorer, getDataExplorer } from 'store/data-explorer/data-explorer-reducer';
-import { updateResources } from 'store/resources/resources-actions';
-import { getSortColumn } from "store/data-explorer/data-explorer-reducer";
-import { computeNodesActions } from 'store/compute-nodes/compute-nodes-actions';
-import { OrderDirection, OrderBuilder } from 'services/api/order-builder';
-import { ListResults } from 'services/common-service/common-service';
-import { NodeResource } from 'models/node';
-import { SortDirection } from 'components/data-table/data-column';
-import { ComputeNodePanelColumnNames } from 'views/compute-node-panel/compute-node-panel-root';
-
-export class ComputeNodeMiddlewareService extends DataExplorerMiddlewareService {
-    constructor(private services: ServiceRepository, id: string) {
-        super(id);
-    }
-
-    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
-        const state = api.getState();
-        const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
-        try {
-            const response = await this.services.nodeService.list(getParams(dataExplorer));
-            api.dispatch(updateResources(response.items));
-            api.dispatch(setItems(response));
-        } catch {
-            api.dispatch(couldNotFetchLinks());
-        }
-    }
-}
-
-export const getParams = (dataExplorer: DataExplorer) => ({
-    ...dataExplorerToListParams(dataExplorer),
-    order: getOrder(dataExplorer)
-});
-
-const getOrder = (dataExplorer: DataExplorer) => {
-    const sortColumn = getSortColumn(dataExplorer);
-    const order = new OrderBuilder<NodeResource>();
-    if (sortColumn) {
-        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
-            ? OrderDirection.ASC
-            : OrderDirection.DESC;
-
-        const columnName = sortColumn && sortColumn.name === ComputeNodePanelColumnNames.UUID ? "uuid" : "modifiedAt";
-        return order
-            .addOrder(sortDirection, columnName)
-            .getOrder();
-    } else {
-        return order.getOrder();
-    }
-};
-
-export const setItems = (listResults: ListResults<NodeResource>) =>
-    computeNodesActions.SET_ITEMS({
-        ...listResultsToDataExplorerItemsMeta(listResults),
-        items: listResults.items.map(resource => resource.uuid),
-    });
-
-const couldNotFetchLinks = () =>
-    snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch compute nodes.',
-        kind: SnackbarKind.ERROR
-    });
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 038b31e2..874e840c 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -119,17 +119,6 @@ export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>,
         }));
     };
 
-export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
-    (dispatch: Dispatch) => {
-        dispatch<any>(openContextMenu(event, {
-            name: '',
-            uuid: resourceUuid,
-            ownerUuid: '',
-            kind: ResourceKind.NODE,
-            menuKind: ContextMenuKind.NODE
-        }));
-    };
-
 export const openApiClientAuthorizationContextMenu =
     (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
         (dispatch: Dispatch) => {
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 21a26a3a..97082e5a 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -136,8 +136,6 @@ export const navigateToLinkAccount = push(Routes.LINK_ACCOUNT);
 
 export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
 
-export const navigateToComputeNodes = push(Routes.COMPUTE_NODES);
-
 export const navigateToUsers = push(Routes.USERS);
 
 export const navigateToApiClientAuthorizations = push(Routes.API_CLIENT_AUTHORIZATIONS);
diff --git a/src/store/store.ts b/src/store/store.ts
index d0f1af87..59a0cb12 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -55,8 +55,6 @@ import { GroupDetailsPanelMiddlewareService } from 'store/group-details-panel/gr
 import { GROUP_DETAILS_PANEL_ID } from 'store/group-details-panel/group-details-panel-actions';
 import { LINK_PANEL_ID } from 'store/link-panel/link-panel-actions';
 import { LinkMiddlewareService } from 'store/link-panel/link-panel-middleware-service';
-import { COMPUTE_NODE_PANEL_ID } from 'store/compute-nodes/compute-nodes-actions';
-import { ComputeNodeMiddlewareService } from 'store/compute-nodes/compute-nodes-middleware-service';
 import { API_CLIENT_AUTHORIZATION_PANEL_ID } from 'store/api-client-authorizations/api-client-authorizations-actions';
 import { ApiClientAuthorizationMiddlewareService } from 'store/api-client-authorizations/api-client-authorizations-middleware-service';
 import { PublicFavoritesMiddlewareService } from 'store/public-favorites-panel/public-favorites-middleware-service';
@@ -124,9 +122,6 @@ export function configureStore(history: History, services: ServiceRepository, co
     const linkPanelMiddleware = dataExplorerMiddleware(
         new LinkMiddlewareService(services, LINK_PANEL_ID)
     );
-    const computeNodeMiddleware = dataExplorerMiddleware(
-        new ComputeNodeMiddlewareService(services, COMPUTE_NODE_PANEL_ID)
-    );
     const apiClientAuthorizationMiddlewareService = dataExplorerMiddleware(
         new ApiClientAuthorizationMiddlewareService(services, API_CLIENT_AUTHORIZATION_PANEL_ID)
     );
@@ -164,7 +159,6 @@ export function configureStore(history: History, services: ServiceRepository, co
         groupsPanelMiddleware,
         groupDetailsPanelMiddleware,
         linkPanelMiddleware,
-        computeNodeMiddleware,
         apiClientAuthorizationMiddlewareService,
         publicFavoritesMiddleware,
         collectionsContentAddress,
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 3aa59802..6ea30855 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -81,10 +81,8 @@ import { loadRepositoriesPanel } from 'store/repositories/repositories-actions';
 import { loadKeepServicesPanel } from 'store/keep-services/keep-services-actions';
 import { loadUsersPanel, userBindedActions } from 'store/users/users-actions';
 import { linkPanelActions, loadLinkPanel } from 'store/link-panel/link-panel-actions';
-import { computeNodesActions, loadComputeNodesPanel } from 'store/compute-nodes/compute-nodes-actions';
 import { linkPanelColumns } from 'views/link-panel/link-panel-root';
 import { userPanelColumns } from 'views/user-panel/user-panel';
-import { computeNodePanelColumns } from 'views/compute-node-panel/compute-node-panel-root';
 import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from 'store/api-client-authorizations/api-client-authorizations-actions';
 import { apiClientAuthorizationPanelColumns } from 'views/api-client-authorization-panel/api-client-authorization-panel-root';
 import * as groupPanelActions from 'store/groups-panel/groups-panel-actions';
@@ -140,7 +138,6 @@ export const loadWorkbench = () =>
             dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
             dispatch(groupDetailsPanelActions.GroupDetailsPanelActions.SET_COLUMNS({ columns: groupDetailsPanelColumns }));
             dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
-            dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
             dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
             dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
@@ -517,11 +514,6 @@ export const loadUsers = handleFirstTimeLoad(
         dispatch(setBreadcrumbs([{ label: 'Users' }]));
     });
 
-export const loadComputeNodes = handleFirstTimeLoad(
-    async (dispatch: Dispatch<any>) => {
-        await dispatch(loadComputeNodesPanel());
-    });
-
 export const loadApiClientAuthorizations = handleFirstTimeLoad(
     async (dispatch: Dispatch<any>) => {
         await dispatch(loadApiClientAuthorizationsPanel());
diff --git a/src/views-components/compute-nodes-dialog/attributes-dialog.tsx b/src/views-components/compute-nodes-dialog/attributes-dialog.tsx
deleted file mode 100644
index 0c937b19..00000000
--- a/src/views-components/compute-nodes-dialog/attributes-dialog.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { compose } from 'redux';
-import {
-    withStyles, Dialog, DialogTitle, DialogContent, DialogActions,
-    Button, StyleRulesCallback, WithStyles, Grid
-} from '@material-ui/core';
-import { WithDialogProps, withDialog } from "store/dialog/with-dialog";
-import { COMPUTE_NODE_ATTRIBUTES_DIALOG } from 'store/compute-nodes/compute-nodes-actions';
-import { ArvadosTheme } from 'common/custom-theme';
-import { NodeResource, NodeProperties, NodeInfo } from 'models/node';
-import classnames from "classnames";
-
-type CssRules = 'root' | 'grid';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
-        fontSize: '0.875rem',
-        '& div:nth-child(odd):not(.nestedRoot)': {
-            textAlign: 'right',
-            color: theme.palette.grey["500"]
-        },
-        '& div:nth-child(even)': {
-            overflowWrap: 'break-word'
-        }
-    },
-    grid: {
-        padding: '8px 0 0 0'
-    }
-});
-
-interface AttributesComputeNodeDialogDataProps {
-    computeNode: NodeResource;
-}
-
-export const AttributesComputeNodeDialog = compose(
-    withDialog(COMPUTE_NODE_ATTRIBUTES_DIALOG),
-    withStyles(styles))(
-        ({ open, closeDialog, data, classes }: WithDialogProps<AttributesComputeNodeDialogDataProps> & WithStyles<CssRules>) =>
-            <Dialog open={open} onClose={closeDialog} fullWidth maxWidth='sm'>
-                <DialogTitle>Attributes</DialogTitle>
-                <DialogContent>
-                    {data.computeNode && <div>
-                        {renderPrimaryInfo(data.computeNode, classes)}
-                        {renderInfo(data.computeNode.info, classes)}
-                        {renderProperties(data.computeNode.properties, classes)}
-                    </div>}
-                </DialogContent>
-                <DialogActions>
-                    <Button
-                        variant='text'
-                        color='primary'
-                        onClick={closeDialog}>
-                        Close
-                    </Button>
-                </DialogActions>
-            </Dialog>
-    );
-
-const renderPrimaryInfo = (computeNode: NodeResource, classes: any) => {
-    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid } = computeNode;
-    return (
-        <Grid container direction="row" spacing={16} className={classes.root}>
-            <Grid item xs={5}>UUID</Grid>
-            <Grid item xs={7}>{uuid}</Grid>
-            <Grid item xs={5}>Owner uuid</Grid>
-            <Grid item xs={7}>{ownerUuid}</Grid>
-            <Grid item xs={5}>Created at</Grid>
-            <Grid item xs={7}>{createdAt}</Grid>
-            <Grid item xs={5}>Modified at</Grid>
-            <Grid item xs={7}>{modifiedAt}</Grid>
-            <Grid item xs={5}>Modified by user uuid</Grid>
-            <Grid item xs={7}>{modifiedByUserUuid}</Grid>
-            <Grid item xs={5}>Modified by client uuid</Grid>
-            <Grid item xs={7}>{modifiedByClientUuid || '(none)'}</Grid>
-        </Grid>
-    );
-};
-
-const renderInfo = (info: NodeInfo, classes: any) => {
-    const { last_action, ping_secret, ec2_instance_id, slurm_state } = info;
-    return (
-        <Grid container direction="row" spacing={16} className={classnames([classes.root, classes.grid])}>
-            <Grid item xs={5}>Info - Last action</Grid>
-            <Grid item xs={7}>{last_action || '(none)'}</Grid>
-            <Grid item xs={5}>Info - Ping secret</Grid>
-            <Grid item xs={7}>{ping_secret || '(none)'}</Grid>
-            <Grid item xs={5}>Info - ec2 instance id</Grid>
-            <Grid item xs={7}>{ec2_instance_id || '(none)'}</Grid>
-            <Grid item xs={5}>Info - Slurm state</Grid>
-            <Grid item xs={7}>{slurm_state || '(none)'}</Grid>
-        </Grid>
-    );
-};
-
-const renderProperties = (properties: NodeProperties, classes: any) => {
-    const { total_ram_mb, total_cpu_cores, total_scratch_mb, cloud_node } = properties;
-    return (
-        <Grid container direction="row" spacing={16} className={classnames([classes.root, classes.grid])}>
-            <Grid item xs={5}>Properties - Total ram mb</Grid>
-            <Grid item xs={7}>{total_ram_mb || '(none)'}</Grid>
-            <Grid item xs={5}>Properties - Total scratch mb</Grid>
-            <Grid item xs={7}>{total_scratch_mb || '(none)'}</Grid>
-            <Grid item xs={5}>Properties - Total cpu cores</Grid>
-            <Grid item xs={7}>{total_cpu_cores || '(none)'}</Grid>
-            <Grid item xs={5}>Properties - Cloud node size </Grid>
-            <Grid item xs={7}>{cloud_node ? cloud_node.size : '(none)'}</Grid>
-            <Grid item xs={5}>Properties - Cloud node price</Grid>
-            <Grid item xs={7}>{cloud_node ? cloud_node.price : '(none)'}</Grid>
-        </Grid>
-    );
-};
\ No newline at end of file
diff --git a/src/views-components/compute-nodes-dialog/remove-dialog.tsx b/src/views-components/compute-nodes-dialog/remove-dialog.tsx
deleted file mode 100644
index 60ca0f92..00000000
--- a/src/views-components/compute-nodes-dialog/remove-dialog.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Dispatch, compose } from 'redux';
-import { connect } from "react-redux";
-import { ConfirmationDialog } from "components/confirmation-dialog/confirmation-dialog";
-import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
-import { COMPUTE_NODE_REMOVE_DIALOG, removeComputeNode } from 'store/compute-nodes/compute-nodes-actions';
-
-const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
-    onConfirm: () => {
-        props.closeDialog();
-        dispatch<any>(removeComputeNode(props.data.uuid));
-    }
-});
-
-export const  RemoveComputeNodeDialog = compose(
-    withDialog(COMPUTE_NODE_REMOVE_DIALOG),
-    connect(null, mapDispatchToProps)
-)(ConfirmationDialog);
\ No newline at end of file
diff --git a/src/views-components/context-menu/action-sets/compute-node-action-set.ts b/src/views-components/context-menu/action-sets/compute-node-action-set.ts
deleted file mode 100644
index e09ec9e0..00000000
--- a/src/views-components/context-menu/action-sets/compute-node-action-set.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { openComputeNodeRemoveDialog, openComputeNodeAttributesDialog } from 'store/compute-nodes/compute-nodes-actions';
-import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
-
-export const computeNodeActionSet: ContextMenuActionSet = [[{
-    name: "Attributes",
-    icon: AttributesIcon,
-    execute: (dispatch, { uuid }) => {
-        dispatch<any>(openComputeNodeAttributesDialog(uuid));
-    }
-}, {
-    name: "Advanced",
-    icon: AdvancedIcon,
-    execute: (dispatch, { uuid }) => {
-        dispatch<any>(openAdvancedTabDialog(uuid));
-    }
-}, {
-    name: "Remove",
-    icon: RemoveIcon,
-    execute: (dispatch, { uuid }) => {
-        dispatch<any>(openComputeNodeRemoveDialog(uuid));
-    }
-}]];
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index 7fd66c2c..603ee90b 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -96,7 +96,6 @@ export enum ContextMenuKind {
     VIRTUAL_MACHINE = "VirtualMachine",
     KEEP_SERVICE = "KeepService",
     USER = "User",
-    NODE = "Node",
     GROUPS = "Group",
     GROUP_MEMBER = "GroupMember",
     LINK = "Link",
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index dccd2786..314390e2 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -232,11 +232,6 @@ export const TokenScopes = withResourceData('scopes', renderCommonData);
 
 export const TokenUserId = withResourceData('userId', renderCommonData);
 
-// Compute Node Resources
-const renderNodeInfo = (data: string) => {
-    return <Typography>{JSON.stringify(data, null, 4)}</Typography>;
-};
-
 const clusterColors = [
     ['#f44336', '#fff'],
     ['#2196f3', '#fff'],
@@ -262,20 +257,6 @@ export const ResourceCluster = (props: { uuid: string }) => {
     }}>{clusterId}</span>;
 };
 
-export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
-
-export const ComputeNodeDomain = withResourceData('domain', renderCommonData);
-
-export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderCommonDate);
-
-export const ComputeNodeHostname = withResourceData('hostname', renderCommonData);
-
-export const ComputeNodeIpAddress = withResourceData('ipAddress', renderCommonData);
-
-export const ComputeNodeJobUuid = withResourceData('jobUuid', renderCommonData);
-
-export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderCommonDate);
-
 // Links Resources
 const renderLinkName = (item: { name: string }) =>
     <Typography noWrap>{item.name || '(none)'}</Typography>;
diff --git a/src/views-components/main-app-bar/admin-menu.tsx b/src/views-components/main-app-bar/admin-menu.tsx
index ab5c2ead..198306b5 100644
--- a/src/views-components/main-app-bar/admin-menu.tsx
+++ b/src/views-components/main-app-bar/admin-menu.tsx
@@ -38,7 +38,6 @@ export const AdminMenu = connect(mapStateToProps)(
                 <MenuItem onClick={() => dispatch(NavigationAction.navigateToApiClientAuthorizations)}>Api Tokens</MenuItem>
                 <MenuItem onClick={() => dispatch(openUserPanel())}>Users</MenuItem>
                 <MenuItem onClick={() => dispatch(NavigationAction.navigateToGroups)}>Groups</MenuItem>
-                <MenuItem onClick={() => dispatch(NavigationAction.navigateToComputeNodes)}>Compute Nodes</MenuItem>
                 <MenuItem onClick={() => dispatch(NavigationAction.navigateToKeepServices)}>Keep Services</MenuItem>
                 <MenuItem onClick={() => dispatch(NavigationAction.navigateToLinks)}>Links</MenuItem>
             </DropdownMenu>
diff --git a/src/views/compute-node-panel/compute-node-panel-root.tsx b/src/views/compute-node-panel/compute-node-panel-root.tsx
deleted file mode 100644
index 1060197a..00000000
--- a/src/views/compute-node-panel/compute-node-panel-root.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { ShareMeIcon } from 'components/icon/icon';
-import { DataExplorer } from 'views-components/data-explorer/data-explorer';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
-import { COMPUTE_NODE_PANEL_ID } from 'store/compute-nodes/compute-nodes-actions';
-import { DataColumns } from 'components/data-table/data-table';
-import { SortDirection } from 'components/data-table/data-column';
-import { createTree } from 'models/tree';
-import {
-    ComputeNodeInfo, ComputeNodeDomain, ComputeNodeHostname, ComputeNodeJobUuid,
-    ComputeNodeFirstPingAt, ComputeNodeLastPingAt, ComputeNodeIpAddress, CommonUuid
-} from 'views-components/data-explorer/renderers';
-import { ResourcesState } from 'store/resources/resources';
-
-export enum ComputeNodePanelColumnNames {
-    INFO = 'Info',
-    UUID = 'UUID',
-    DOMAIN = 'Domain',
-    FIRST_PING_AT = 'First ping at',
-    HOSTNAME = 'Hostname',
-    IP_ADDRESS = 'IP Address',
-    JOB = 'Job',
-    LAST_PING_AT = 'Last ping at'
-}
-
-export const computeNodePanelColumns: DataColumns<string> = [
-    {
-        name: ComputeNodePanelColumnNames.INFO,
-        selected: true,
-        configurable: true,
-        filters: createTree(),
-        render: uuid => <ComputeNodeInfo uuid={uuid} />
-    },
-    {
-        name: ComputeNodePanelColumnNames.UUID,
-        selected: true,
-        configurable: true,
-        sortDirection: SortDirection.NONE,
-        filters: createTree(),
-        render: uuid => <CommonUuid uuid={uuid} />
-    },
-    {
-        name: ComputeNodePanelColumnNames.DOMAIN,
-        selected: true,
-        configurable: true,
-        filters: createTree(),
-        render: uuid => <ComputeNodeDomain uuid={uuid} />
-    },
-    {
-        name: ComputeNodePanelColumnNames.FIRST_PING_AT,
-        selected: true,
-        configurable: true,
-        filters: createTree(),
-        render: uuid => <ComputeNodeFirstPingAt uuid={uuid} />
-    },
-    {
-        name: ComputeNodePanelColumnNames.HOSTNAME,
-        selected: true,
-        configurable: true,
-        filters: createTree(),
-        render: uuid => <ComputeNodeHostname uuid={uuid} />
-    },
-    {
-        name: ComputeNodePanelColumnNames.IP_ADDRESS,
-        selected: true,
-        configurable: true,
-        filters: createTree(),
-        render: uuid => <ComputeNodeIpAddress uuid={uuid} />
-    },
-    {
-        name: ComputeNodePanelColumnNames.JOB,
-        selected: true,
-        configurable: true,
-        filters: createTree(),
-        render: uuid => <ComputeNodeJobUuid uuid={uuid} />
-    },
-    {
-        name: ComputeNodePanelColumnNames.LAST_PING_AT,
-        selected: true,
-        configurable: true,
-        filters: createTree(),
-        render: uuid => <ComputeNodeLastPingAt uuid={uuid} />
-    }
-];
-
-const DEFAULT_MESSAGE = 'Your compute node list is empty.';
-
-export interface ComputeNodePanelRootActionProps {
-    onItemClick: (item: string) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
-    onItemDoubleClick: (item: string) => void;
-}
-
-export interface ComputeNodePanelRootDataProps {
-    resources: ResourcesState;
-}
-
-type ComputeNodePanelRootProps = ComputeNodePanelRootActionProps & ComputeNodePanelRootDataProps;
-
-export const ComputeNodePanelRoot = (props: ComputeNodePanelRootProps) => {
-    return <DataExplorer
-        id={COMPUTE_NODE_PANEL_ID}
-        onRowClick={props.onItemClick}
-        onRowDoubleClick={props.onItemDoubleClick}
-        onContextMenu={props.onContextMenu}
-        contextMenuColumn={true}
-        hideColumnSelector
-        hideSearchInput
-        dataTableDefaultView={
-            <DataTableDefaultView
-                icon={ShareMeIcon}
-                messages={[DEFAULT_MESSAGE]} />
-        } />;
-};
diff --git a/src/views/compute-node-panel/compute-node-panel.tsx b/src/views/compute-node-panel/compute-node-panel.tsx
deleted file mode 100644
index 308be4d6..00000000
--- a/src/views/compute-node-panel/compute-node-panel.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { RootState } from 'store/store';
-import { Dispatch } from 'redux';
-import { connect } from 'react-redux';
-import {
-    ComputeNodePanelRoot,
-    ComputeNodePanelRootDataProps,
-    ComputeNodePanelRootActionProps
-} from 'views/compute-node-panel/compute-node-panel-root';
-import { openComputeNodeContextMenu } from 'store/context-menu/context-menu-actions';
-
-const mapStateToProps = (state: RootState): ComputeNodePanelRootDataProps => {
-    return {
-        resources: state.resources
-    };
-};
-
-const mapDispatchToProps = (dispatch: Dispatch): ComputeNodePanelRootActionProps => ({
-    onContextMenu: (event, resourceUuid) => {
-        dispatch<any>(openComputeNodeContextMenu(event, resourceUuid));
-    },
-    onItemClick: (resourceUuid: string) => { return; },
-    onItemDoubleClick: uuid => { return; }
-});
-
-export const ComputeNodePanel = connect(mapStateToProps, mapDispatchToProps)(ComputeNodePanelRoot);
\ No newline at end of file
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index e680e271..b708355c 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -56,7 +56,6 @@ import { VirtualMachineAdminPanel } from 'views/virtual-machine-panel/virtual-ma
 import { ProjectPropertiesDialog } from 'views-components/project-properties-dialog/project-properties-dialog';
 import { RepositoriesPanel } from 'views/repositories-panel/repositories-panel';
 import { KeepServicePanel } from 'views/keep-service-panel/keep-service-panel';
-import { ComputeNodePanel } from 'views/compute-node-panel/compute-node-panel';
 import { ApiClientAuthorizationPanel } from 'views/api-client-authorization-panel/api-client-authorization-panel';
 import { LinkPanel } from 'views/link-panel/link-panel';
 import { RepositoriesSampleGitDialog } from 'views-components/repositories-sample-git-dialog/repositories-sample-git-dialog';
@@ -66,13 +65,11 @@ import { RemoveRepositoryDialog } from 'views-components/repository-remove-dialo
 import { CreateSshKeyDialog } from 'views-components/dialog-forms/create-ssh-key-dialog';
 import { PublicKeyDialog } from 'views-components/ssh-keys-dialog/public-key-dialog';
 import { RemoveApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/remove-dialog';
-import { RemoveComputeNodeDialog } from 'views-components/compute-nodes-dialog/remove-dialog';
 import { RemoveKeepServiceDialog } from 'views-components/keep-services-dialog/remove-dialog';
 import { RemoveLinkDialog } from 'views-components/links-dialog/remove-dialog';
 import { RemoveSshKeyDialog } from 'views-components/ssh-keys-dialog/remove-dialog';
 import { RemoveVirtualMachineDialog } from 'views-components/virtual-machines-dialog/remove-dialog';
 import { AttributesApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/attributes-dialog';
-import { AttributesComputeNodeDialog } from 'views-components/compute-nodes-dialog/attributes-dialog';
 import { AttributesKeepServiceDialog } from 'views-components/keep-services-dialog/attributes-dialog';
 import { AttributesLinkDialog } from 'views-components/links-dialog/attributes-dialog';
 import { AttributesSshKeyDialog } from 'views-components/ssh-keys-dialog/attributes-dialog';
@@ -171,7 +168,6 @@ let routes = <>
     <Route path={Routes.SITE_MANAGER} component={SiteManagerPanel} />
     <Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
     <Route path={Routes.USERS} component={UserPanel} />
-    <Route path={Routes.COMPUTE_NODES} component={ComputeNodePanel} />
     <Route path={Routes.API_CLIENT_AUTHORIZATIONS} component={ApiClientAuthorizationPanel} />
     <Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
     <Route path={Routes.GROUPS} component={GroupsPanel} />
@@ -218,7 +214,6 @@ export const WorkbenchPanel =
             <AddGroupMembersDialog />
             <AdvancedTabDialog />
             <AttributesApiClientAuthorizationDialog />
-            <AttributesComputeNodeDialog />
             <AttributesKeepServiceDialog />
             <AttributesLinkDialog />
             <AttributesSshKeyDialog />
@@ -250,7 +245,6 @@ export const WorkbenchPanel =
             <ProjectPropertiesDialog />
             <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />
-            <RemoveComputeNodeDialog />
             <RemoveGroupDialog />
             <RemoveGroupMemberDialog />
             <RemoveKeepServiceDialog />

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list