[ARVADOS-WORKBENCH2] created: 2.2.2

Git user git at public.arvados.org
Fri Jan 7 19:47:27 UTC 2022


        at  0f7f4ab0fc5027f0de921eafa42046712c5a392c (commit)


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