[ARVADOS-WORKBENCH2] created: 1.4.1-293-g14304aae

Git user git at public.arvados.org
Fri Jan 7 19:46:32 UTC 2022


        at  14304aae3f34e0cf0a5c5275416abc26113d65e0 (commit)


commit 14304aae3f34e0cf0a5c5275416abc26113d65e0
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Fri Aug 14 15:41:31 2020 -0400

    16683: Take count out from list params. Fix getPublicGroupUuid
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/store/data-explorer/data-explorer-middleware-service.ts b/src/store/data-explorer/data-explorer-middleware-service.ts
index a7c32f42..64e6e50b 100644
--- a/src/store/data-explorer/data-explorer-middleware-service.ts
+++ b/src/store/data-explorer/data-explorer-middleware-service.ts
@@ -35,8 +35,7 @@ export const getDataExplorerColumnFilters = <T>(columns: DataColumns<T>, columnN
 
 export const dataExplorerToListParams = (dataExplorer: DataExplorer) => ({
     limit: dataExplorer.rowsPerPage,
-    offset: dataExplorer.page * dataExplorer.rowsPerPage,
-    count: "none"
+    offset: dataExplorer.page * dataExplorer.rowsPerPage
 });
 
 export const listResultsToDataExplorerItemsMeta = <R>({ itemsAvailable, offset, limit }: ListResults<R>) => ({
diff --git a/src/store/workflow-panel/workflow-panel-actions.ts b/src/store/workflow-panel/workflow-panel-actions.ts
index 3f91c102..bfd899fd 100644
--- a/src/store/workflow-panel/workflow-panel-actions.ts
+++ b/src/store/workflow-panel/workflow-panel-actions.ts
@@ -50,11 +50,11 @@ export const openRunProcess = (uuid: string) =>
     };
 
 export const getPublicUserUuid = (state: RootState) => {
-    const prefix = getProperty<string>(UUID_PREFIX_PROPERTY_NAME)(state.properties);
+    const prefix = state.auth.localCluster;
     return `${prefix}-tpzed-anonymouspublic`;
 };
 export const getPublicGroupUuid = (state: RootState) => {
-    const prefix = getProperty<string>(UUID_PREFIX_PROPERTY_NAME)(state.properties);
+    const prefix = state.auth.localCluster;
     return `${prefix}-j7d0g-anonymouspublic`;
 };
 

commit be94d8139ea574dc9e07766cad5408a8e9587b91
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Fri Aug 14 14:58:44 2020 -0400

    16683: Remove reference to "klingenc"
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/views-components/current-token-dialog/current-token-dialog.tsx b/src/views-components/current-token-dialog/current-token-dialog.tsx
index bc0071af..e5208d44 100644
--- a/src/views-components/current-token-dialog/current-token-dialog.tsx
+++ b/src/views-components/current-token-dialog/current-token-dialog.tsx
@@ -57,7 +57,7 @@ export const CurrentTokenDialog =
                         </Typography>
                     </Typography>
                     <Typography  paragraph={true}>
-                        Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your klingenc account.
+                        Paste the following lines at a shell prompt to set up the necessary environment for Arvados SDKs to authenticate to your account.
                             </Typography>
                     <DefaultCodeSnippet lines={[getSnippet(data)]} />
                     <Typography >

commit a1ec1954154e42a374f28c2f625ebc1da9d4dd07
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Fri Aug 14 14:49:03 2020 -0400

    16683: Tweak other calls to user listing to work with federated users
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
index dc0f2c52..13c460f8 100644
--- a/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
+++ b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
@@ -76,18 +76,16 @@ export class CollectionsWithSameContentAddressMiddlewareService extends DataExpl
                     }
                 });
                 const responseUsers = await this.services.userService.list({
-                    limit: dataExplorer.rowsPerPage,
-                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
                     filters: new FilterBuilder()
                         .addIn('uuid', userUuids)
-                        .getFilters()
+                        .getFilters(),
+                    count: "none"
                 });
                 const responseGroups = await this.services.groupsService.list({
-                    limit: dataExplorer.rowsPerPage,
-                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
                     filters: new FilterBuilder()
                         .addIn('uuid', groupUuids)
-                        .getFilters()
+                        .getFilters(),
+                    count: "none"
                 });
                 responseUsers.items.map(it => {
                     api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({ name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid }));
diff --git a/src/store/data-explorer/data-explorer-middleware-service.ts b/src/store/data-explorer/data-explorer-middleware-service.ts
index 219e7603..a7c32f42 100644
--- a/src/store/data-explorer/data-explorer-middleware-service.ts
+++ b/src/store/data-explorer/data-explorer-middleware-service.ts
@@ -36,6 +36,7 @@ export const getDataExplorerColumnFilters = <T>(columns: DataColumns<T>, columnN
 export const dataExplorerToListParams = (dataExplorer: DataExplorer) => ({
     limit: dataExplorer.rowsPerPage,
     offset: dataExplorer.page * dataExplorer.rowsPerPage,
+    count: "none"
 });
 
 export const listResultsToDataExplorerItemsMeta = <R>({ itemsAvailable, offset, limit }: ListResults<R>) => ({
diff --git a/src/store/group-details-panel/group-details-panel-middleware-service.ts b/src/store/group-details-panel/group-details-panel-middleware-service.ts
index 94f78a58..5aff4e7b 100644
--- a/src/store/group-details-panel/group-details-panel-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-middleware-service.ts
@@ -36,7 +36,8 @@ export class GroupDetailsPanelMiddlewareService extends DataExplorerMiddlewareSe
                 const users = await this.services.userService.list({
                     filters: new FilterBuilder()
                         .addIn('uuid', permissions.items.map(item => item.headUuid))
-                        .getFilters()
+                        .getFilters(),
+                    count: "none"
                 });
                 api.dispatch(GroupDetailsPanelActions.SET_ITEMS({
                     ...listResultsToDataExplorerItemsMeta(permissions),
diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
index 7b12fe75..8f696fa2 100644
--- a/src/store/users/users-actions.ts
+++ b/src/store/users/users-actions.ts
@@ -157,7 +157,7 @@ export const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID);
 
 export const loadUsersData = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        await services.userService.list();
+        await services.userService.list({ count: "none" });
     };
 
 export const loadUsersPanel = () =>
diff --git a/src/views-components/sharing-dialog/people-select.tsx b/src/views-components/sharing-dialog/people-select.tsx
index 90235cd5..181ff078 100644
--- a/src/views-components/sharing-dialog/people-select.tsx
+++ b/src/views-components/sharing-dialog/people-select.tsx
@@ -120,11 +120,11 @@ export const PeopleSelect = connect()(
                 .addNotIn('group_class', [GroupClass.PROJECT])
                 .addILike('name', value)
                 .getFilters();
-            const groupItems = await groupsService.list({ filters: filterGroups, limit: 5 });
+            const groupItems = await groupsService.list({ filters: filterGroups, limit: 5, count: "none" });
             const filterUsers = new FilterBuilder()
                 .addILike('email', value)
                 .getFilters();
-            const userItems: any = await userService.list({ filters: filterUsers, limit: 5 });
+            const userItems: any = await userService.list({ filters: filterUsers, limit: 5, count: "none" });
             const items = groupItems.items.concat(userItems.items);
             this.setState({ suggestions: this.props.onlyPeople ? userItems.items : items });
         }

commit 964e3cd81197d2131b5de5a77abc33dd81b2e7a4
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Aug 13 14:22:40 2020 -0400

    16683: Add count: "none" to sharing dialog
    
    To work with federated user listing.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts
index 671c10fc..58fc3198 100644
--- a/src/store/sharing-dialog/sharing-dialog-actions.ts
+++ b/src/store/sharing-dialog/sharing-dialog-actions.ts
@@ -79,8 +79,8 @@ const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
             .addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
             .getFilters();
 
-        const { items: users } = await userService.list({ filters });
-        const { items: groups} = await groupsService.list({ filters });
+        const { items: users } = await userService.list({ filters, count: "none" });
+        const { items: groups } = await groupsService.list({ filters, count: "none" });
 
         const getEmail = (tailUuid: string) => {
             const user = users.find(({ uuid }) => uuid === tailUuid);

commit 0fca724529ecc364ebbdb7bb1366ffff0d08bbd9
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Jun 2 16:05:22 2020 -0300

    16439: Disables +NEW button even on trashed subprojects.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/views-components/side-panel-button/side-panel-button.tsx b/src/views-components/side-panel-button/side-panel-button.tsx
index 4e2e90f1..5e547740 100644
--- a/src/views-components/side-panel-button/side-panel-button.tsx
+++ b/src/views-components/side-panel-button/side-panel-button.tsx
@@ -17,6 +17,7 @@ import { getUserUuid } from '~/common/getuser';
 import { matchProjectRoute } from '~/routes/routes';
 import { GroupResource } from '~/models/group';
 import { ResourcesState, getResource } from '~/store/resources/resources';
+import { extractUuidKind, ResourceKind } from '~/models/resource';
 
 type CssRules = 'button' | 'menuItem' | 'icon';
 
@@ -53,6 +54,13 @@ const transformOrigin: PopoverOrigin = {
     horizontal: 0
 };
 
+const isProjectTrashed = (proj: GroupResource, resources: ResourcesState): boolean => {
+    if (proj.isTrashed) { return true; }
+    if (extractUuidKind(proj.ownerUuid) === ResourceKind.USER) { return false; }
+    const parentProj = getResource<GroupResource>(proj.ownerUuid)(resources);
+    return isProjectTrashed(parentProj!, resources);
+};
+
 export const SidePanelButton = withStyles(styles)(
     connect((state: RootState) => ({
         currentItemId: state.router.location
@@ -78,7 +86,7 @@ export const SidePanelButton = withStyles(styles)(
                     const currentProject = getResource<GroupResource>(currentItemId)(resources);
                     if (currentProject &&
                         currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
-                        !currentProject.isTrashed) {
+                        !isProjectTrashed(currentProject, resources)) {
                         enabled = true;
                     }
                 }

commit 5c17f40de035a71cb05fc0b7f80bbf6550da2c31
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Jun 2 15:47:42 2020 -0300

    16439: Fixes colletion/project creation placement.
    
    When the user was placed on other sections than 'Projects' on the side
    panel, the newly created collections/projects were going to the user's
    home project.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/store/collections/collection-create-actions.ts b/src/store/collections/collection-create-actions.ts
index 140756bf..b6f0ddcc 100644
--- a/src/store/collections/collection-create-actions.ts
+++ b/src/store/collections/collection-create-actions.ts
@@ -12,7 +12,7 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from "~/ser
 import { uploadCollectionFiles } from './collection-upload-actions';
 import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
-import { isItemNotInProject, isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
+import { isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { CollectionResource } from "~/models/collection";
 
@@ -26,9 +26,8 @@ export const COLLECTION_CREATE_FORM_NAME = "collectionCreateFormName";
 
 export const openCollectionCreateDialog = (ownerUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const router = getState();
-        const properties = getState().properties;
-        if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
+        const { router } = getState();
+        if (!isProjectOrRunProcessRoute(router)) {
             const userUuid = getUserUuid(getState());
             if (!userUuid) { return; }
             dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid: userUuid }));
diff --git a/src/store/projects/project-create-actions.ts b/src/store/projects/project-create-actions.ts
index 6f45bc38..583a4bd6 100644
--- a/src/store/projects/project-create-actions.ts
+++ b/src/store/projects/project-create-actions.ts
@@ -12,6 +12,7 @@ import { ProjectResource } from '~/models/project';
 import { ServiceRepository } from '~/services/services';
 import { matchProjectRoute, matchRunProcessRoute } from '~/routes/routes';
 import { ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form';
+import { RouterState } from "react-router-redux";
 
 export interface ProjectCreateFormDialogData {
     ownerUuid: string;
@@ -28,26 +29,17 @@ export const PROJECT_CREATE_FORM_NAME = 'projectCreateFormName';
 export const PROJECT_CREATE_PROPERTIES_FORM_NAME = 'projectCreatePropertiesFormName';
 export const PROJECT_CREATE_FORM_SELECTOR = formValueSelector(PROJECT_CREATE_FORM_NAME);
 
-export const isProjectOrRunProcessRoute = ({ router }: RootState) => {
+export const isProjectOrRunProcessRoute = (router: RouterState) => {
     const pathname = router.location ? router.location.pathname : '';
     const matchProject = matchProjectRoute(pathname);
     const matchRunProcess = matchRunProcessRoute(pathname);
     return Boolean(matchProject || matchRunProcess);
 };
 
-export const isItemNotInProject = (properties: any) => {
-    if (properties.breadcrumbs) {
-        return Boolean(properties.breadcrumbs[0].label !== 'Projects');
-    } else {
-        return;
-    }
-};
-
 export const openProjectCreateDialog = (ownerUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const router = getState();
-        const properties = getState().properties;
-        if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
+        const { router } = getState();
+        if (!isProjectOrRunProcessRoute(router)) {
             const userUuid = getUserUuid(getState());
             if (!userUuid) { return; }
             dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid: userUuid }));

commit 43893508f11ffc3841e59f445e29c62c91b026d7
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Jun 1 18:30:19 2020 -0300

    16439: Enables the +NEW button only when the user's view is writable.
    
    Also, makes the SidePanelButton component to not depend on a state property
    set by a something rendered later, because that makes the button being
    re-renderered at least twice on every location change, instead try to get the
    current item id from the location url.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/views-components/side-panel-button/side-panel-button.tsx b/src/views-components/side-panel-button/side-panel-button.tsx
index 0f797590..4e2e90f1 100644
--- a/src/views-components/side-panel-button/side-panel-button.tsx
+++ b/src/views-components/side-panel-button/side-panel-button.tsx
@@ -5,8 +5,6 @@
 import * as React from 'react';
 import { connect, DispatchProp } from 'react-redux';
 import { RootState } from '~/store/store';
-import { getProperty } from '~/store/properties/properties';
-import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { PopoverOrigin } from '@material-ui/core/Popover';
 import { StyleRulesCallback, WithStyles, withStyles, Toolbar, Grid, Button, MenuItem, Menu } from '@material-ui/core';
@@ -15,6 +13,10 @@ import { openProjectCreateDialog } from '~/store/projects/project-create-actions
 import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
 import { navigateToRunProcess } from '~/store/navigation/navigation-action';
 import { runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions';
+import { getUserUuid } from '~/common/getuser';
+import { matchProjectRoute } from '~/routes/routes';
+import { GroupResource } from '~/models/group';
+import { ResourcesState, getResource } from '~/store/resources/resources';
 
 type CssRules = 'button' | 'menuItem' | 'icon';
 
@@ -36,6 +38,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 interface SidePanelDataProps {
     location: any;
     currentItemId: string;
+    resources: ResourcesState;
+    currentUserUUID: string | undefined;
 }
 
 interface SidePanelState {
@@ -51,8 +55,12 @@ const transformOrigin: PopoverOrigin = {
 
 export const SidePanelButton = withStyles(styles)(
     connect((state: RootState) => ({
-        currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
-        location: state.router.location
+        currentItemId: state.router.location
+            ? state.router.location.pathname.split('/').slice(-1)[0]
+            : null,
+        location: state.router.location,
+        resources: state.resources,
+        currentUserUUID: getUserUuid(state),
     }))(
         class extends React.Component<SidePanelProps> {
 
@@ -61,12 +69,24 @@ export const SidePanelButton = withStyles(styles)(
             };
 
             render() {
-                const { classes } = this.props;
+                const { classes, location, resources, currentUserUUID, currentItemId } = this.props;
                 const { anchorEl } = this.state;
+                let enabled = false;
+                if (currentItemId === currentUserUUID) {
+                    enabled = true;
+                } else if (matchProjectRoute(location ? location.pathname : '')) {
+                    const currentProject = getResource<GroupResource>(currentItemId)(resources);
+                    if (currentProject &&
+                        currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
+                        !currentProject.isTrashed) {
+                        enabled = true;
+                    }
+                }
                 return <Toolbar>
                     <Grid container>
                         <Grid container item xs alignItems="center" justify="flex-start">
-                            <Button variant="contained" color="primary" size="small" className={classes.button}
+                            <Button variant="contained" disabled={!enabled}
+                                color="primary" size="small" className={classes.button}
                                 aria-owns={anchorEl ? 'aside-menu-list' : undefined}
                                 aria-haspopup="true"
                                 onClick={this.handleOpen}>
@@ -104,7 +124,7 @@ export const SidePanelButton = withStyles(styles)(
                 this.props.dispatch(runProcessPanelActions.RESET_RUN_PROCESS_PANEL());
                 this.props.dispatch(runProcessPanelActions.SET_PROCESS_PATHNAME(location.pathname));
                 this.props.dispatch(runProcessPanelActions.SET_PROCESS_OWNER_UUID(this.props.currentItemId));
-                
+
                 this.props.dispatch<any>(navigateToRunProcess);
             }
 

commit e0357748cb73aa795970bb8cd2f495bd310e6b89
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Jun 1 10:49:29 2020 -0300

    16439: Reduces vertical spacing between form elements.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/components/form-dialog/form-dialog.tsx b/src/components/form-dialog/form-dialog.tsx
index e95693df..3df874b7 100644
--- a/src/components/form-dialog/form-dialog.tsx
+++ b/src/components/form-dialog/form-dialog.tsx
@@ -16,22 +16,24 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     },
     lastButton: {
         marginLeft: theme.spacing.unit,
-        marginRight: "20px",
+        marginRight: "0",
     },
     formContainer: {
         display: "flex",
         flexDirection: "column",
-        marginTop: "20px",
+        paddingBottom: "0",
     },
     dialogTitle: {
-        paddingBottom: "0"
+        paddingTop: theme.spacing.unit,
+        paddingBottom: theme.spacing.unit,
     },
     progressIndicator: {
         position: "absolute",
         minWidth: "20px",
     },
     dialogActions: {
-        marginBottom: theme.spacing.unit * 3
+        marginBottom: theme.spacing.unit,
+        marginRight: theme.spacing.unit * 3,
     }
 });
 
diff --git a/src/components/text-field/text-field.tsx b/src/components/text-field/text-field.tsx
index 82d640d8..1cf9a81d 100644
--- a/src/components/text-field/text-field.tsx
+++ b/src/components/text-field/text-field.tsx
@@ -19,7 +19,7 @@ type CssRules = 'textField' | 'rte';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     textField: {
-        marginBottom: theme.spacing.unit * 3
+        marginBottom: theme.spacing.unit
     },
     rte: {
         fontFamily: 'Arial',
diff --git a/src/views-components/dialog-create/dialog-collection-create.tsx b/src/views-components/dialog-create/dialog-collection-create.tsx
index 86d6a217..a70030c7 100644
--- a/src/views-components/dialog-create/dialog-collection-create.tsx
+++ b/src/views-components/dialog-create/dialog-collection-create.tsx
@@ -9,7 +9,7 @@ import { CollectionCreateFormDialogData } from '~/store/collections/collection-c
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { CollectionNameField, CollectionDescriptionField } from '~/views-components/form-fields/collection-form-fields';
 import { FileUploaderField } from '../file-uploader/file-uploader';
-import { ResourceLocationField } from '../form-fields/resource-form-fields';
+import { ResourceParentField } from '../form-fields/resource-form-fields';
 
 type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
 
@@ -22,7 +22,7 @@ export const DialogCollectionCreate = (props: DialogCollectionProps) =>
     />;
 
 const CollectionAddFields = () => <span>
-    <ResourceLocationField />
+    <ResourceParentField />
     <CollectionNameField />
     <CollectionDescriptionField />
     <Field
diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx
index 7359ba84..c835e04e 100644
--- a/src/views-components/dialog-create/dialog-project-create.tsx
+++ b/src/views-components/dialog-create/dialog-project-create.tsx
@@ -10,7 +10,7 @@ import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
 import { CreateProjectPropertiesForm } from '~/views-components/project-properties/create-project-properties-form';
 import { CreateProjectPropertiesList } from '~/views-components/project-properties/create-project-properties-list';
-import { ResourceLocationField } from '../form-fields/resource-form-fields';
+import { ResourceParentField } from '../form-fields/resource-form-fields';
 
 type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
 
@@ -23,7 +23,7 @@ export const DialogProjectCreate = (props: DialogProjectProps) =>
     />;
 
 const ProjectAddFields = () => <span>
-    <ResourceLocationField />
+    <ResourceParentField />
     <ProjectNameField />
     <ProjectDescriptionField />
     <CreateProjectPropertiesForm />
diff --git a/src/views-components/form-fields/resource-form-fields.tsx b/src/views-components/form-fields/resource-form-fields.tsx
index 60bcafc0..0c4ae64a 100644
--- a/src/views-components/form-fields/resource-form-fields.tsx
+++ b/src/views-components/form-fields/resource-form-fields.tsx
@@ -11,19 +11,19 @@ import { GroupResource } from "~/models/group";
 import { TextField } from "~/components/text-field/text-field";
 import { getUserUuid } from "~/common/getuser";
 
-interface ResourceLocationFieldProps {
+interface ResourceParentFieldProps {
     resources: ResourcesState;
     userUuid: string|undefined;
 }
 
-export const ResourceLocationField = connect(
+export const ResourceParentField = connect(
     (state: RootState) => {
         return {
             resources: state.resources,
             userUuid: getUserUuid(state),
         };
     })
-    ((props: ResourceLocationFieldProps) =>
+    ((props: ResourceParentFieldProps) =>
         <Field
             name='ownerUuid'
             disabled={true}

commit 36a400f4b2a04ccbcc7c43db984fc0062aa96042
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Fri May 29 15:59:42 2020 -0300

    16439: Updates field label.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/views-components/form-fields/resource-form-fields.tsx b/src/views-components/form-fields/resource-form-fields.tsx
index 0ba357c3..60bcafc0 100644
--- a/src/views-components/form-fields/resource-form-fields.tsx
+++ b/src/views-components/form-fields/resource-form-fields.tsx
@@ -27,7 +27,7 @@ export const ResourceLocationField = connect(
         <Field
             name='ownerUuid'
             disabled={true}
-            label='Location'
+            label='Parent project'
             format={
                 (value, name) => {
                     if (value === props.userUuid) {
@@ -35,7 +35,7 @@ export const ResourceLocationField = connect(
                     }
                     const rsc = getResource<GroupResource>(value)(props.resources);
                     if (rsc !== undefined) {
-                        return `Project '${rsc.name}' (${rsc.uuid})`;
+                        return `${rsc.name} (${rsc.uuid})`;
                     }
                     return value;
                 }

commit 99dd1757aa26b258aee49c631af39097db1a8bcf
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Fri May 29 11:05:24 2020 -0300

    16439: Adds read-only field 'Location' to project/collection creation dialogs.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/store/collections/collection-create-actions.ts b/src/store/collections/collection-create-actions.ts
index 39565f1d..140756bf 100644
--- a/src/store/collections/collection-create-actions.ts
+++ b/src/store/collections/collection-create-actions.ts
@@ -31,7 +31,7 @@ export const openCollectionCreateDialog = (ownerUuid: string) =>
         if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
             const userUuid = getUserUuid(getState());
             if (!userUuid) { return; }
-            dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { userUuid }));
+            dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid: userUuid }));
         } else {
             dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid }));
         }
diff --git a/src/store/projects/project-create-actions.ts b/src/store/projects/project-create-actions.ts
index a303b551..6f45bc38 100644
--- a/src/store/projects/project-create-actions.ts
+++ b/src/store/projects/project-create-actions.ts
@@ -50,7 +50,7 @@ export const openProjectCreateDialog = (ownerUuid: string) =>
         if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
             const userUuid = getUserUuid(getState());
             if (!userUuid) { return; }
-            dispatch(initialize(PROJECT_CREATE_FORM_NAME, { userUuid }));
+            dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid: userUuid }));
         } else {
             dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid }));
         }
diff --git a/src/views-components/dialog-create/dialog-collection-create.tsx b/src/views-components/dialog-create/dialog-collection-create.tsx
index fb492c43..86d6a217 100644
--- a/src/views-components/dialog-create/dialog-collection-create.tsx
+++ b/src/views-components/dialog-create/dialog-collection-create.tsx
@@ -9,7 +9,7 @@ import { CollectionCreateFormDialogData } from '~/store/collections/collection-c
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { CollectionNameField, CollectionDescriptionField } from '~/views-components/form-fields/collection-form-fields';
 import { FileUploaderField } from '../file-uploader/file-uploader';
-
+import { ResourceLocationField } from '../form-fields/resource-form-fields';
 
 type DialogCollectionProps = WithDialogProps<{}> & InjectedFormProps<CollectionCreateFormDialogData>;
 
@@ -22,6 +22,7 @@ export const DialogCollectionCreate = (props: DialogCollectionProps) =>
     />;
 
 const CollectionAddFields = () => <span>
+    <ResourceLocationField />
     <CollectionNameField />
     <CollectionDescriptionField />
     <Field
diff --git a/src/views-components/dialog-create/dialog-project-create.tsx b/src/views-components/dialog-create/dialog-project-create.tsx
index 02fb67e5..7359ba84 100644
--- a/src/views-components/dialog-create/dialog-project-create.tsx
+++ b/src/views-components/dialog-create/dialog-project-create.tsx
@@ -10,6 +10,7 @@ import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectNameField, ProjectDescriptionField } from '~/views-components/form-fields/project-form-fields';
 import { CreateProjectPropertiesForm } from '~/views-components/project-properties/create-project-properties-form';
 import { CreateProjectPropertiesList } from '~/views-components/project-properties/create-project-properties-list';
+import { ResourceLocationField } from '../form-fields/resource-form-fields';
 
 type DialogProjectProps = WithDialogProps<{}> & InjectedFormProps<ProjectCreateFormDialogData>;
 
@@ -22,6 +23,7 @@ export const DialogProjectCreate = (props: DialogProjectProps) =>
     />;
 
 const ProjectAddFields = () => <span>
+    <ResourceLocationField />
     <ProjectNameField />
     <ProjectDescriptionField />
     <CreateProjectPropertiesForm />
diff --git a/src/views-components/form-fields/resource-form-fields.tsx b/src/views-components/form-fields/resource-form-fields.tsx
new file mode 100644
index 00000000..0ba357c3
--- /dev/null
+++ b/src/views-components/form-fields/resource-form-fields.tsx
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { connect } from "react-redux";
+import { RootState } from "~/store/store";
+import { Field } from "redux-form";
+import { ResourcesState, getResource } from "~/store/resources/resources";
+import { GroupResource } from "~/models/group";
+import { TextField } from "~/components/text-field/text-field";
+import { getUserUuid } from "~/common/getuser";
+
+interface ResourceLocationFieldProps {
+    resources: ResourcesState;
+    userUuid: string|undefined;
+}
+
+export const ResourceLocationField = connect(
+    (state: RootState) => {
+        return {
+            resources: state.resources,
+            userUuid: getUserUuid(state),
+        };
+    })
+    ((props: ResourceLocationFieldProps) =>
+        <Field
+            name='ownerUuid'
+            disabled={true}
+            label='Location'
+            format={
+                (value, name) => {
+                    if (value === props.userUuid) {
+                        return 'Home project';
+                    }
+                    const rsc = getResource<GroupResource>(value)(props.resources);
+                    if (rsc !== undefined) {
+                        return `Project '${rsc.name}' (${rsc.uuid})`;
+                    }
+                    return value;
+                }
+            }
+            component={TextField} />
+    );

commit 1d244cd80af604f70d08985ee39d4184db2177a9
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Thu May 28 15:05:01 2020 -0300

    16439: Sends the user to the newly created project/collection.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index dbf795b6..7faad1e8 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -237,7 +237,7 @@ export const createProject = (data: projectCreateActions.ProjectCreateFormDialog
                 kind: SnackbarKind.SUCCESS
             }));
             await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
-            dispatch<any>(reloadProjectMatchingUuid([newProject.ownerUuid]));
+            dispatch<any>(navigateTo(newProject.uuid));
         }
     };
 
@@ -301,7 +301,6 @@ export const loadCollection = (uuid: string) =>
                         dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
                         dispatch(loadCollectionPanel(collection.uuid));
                     },
-
                 });
             }
         });
@@ -316,7 +315,7 @@ export const createCollection = (data: collectionCreateActions.CollectionCreateF
                 kind: SnackbarKind.SUCCESS
             }));
             dispatch<any>(updateResources([collection]));
-            dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
+            dispatch<any>(navigateTo(collection.uuid));
         }
     };
 

commit f9c8d194d7a27ee7624b01877abb65dd63f51dab
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue May 26 12:18:17 2020 -0300

    16439: Allows creation of empty (no files) collections.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/views-components/dialog-create/dialog-collection-create.tsx b/src/views-components/dialog-create/dialog-collection-create.tsx
index 690cf8e5..fb492c43 100644
--- a/src/views-components/dialog-create/dialog-collection-create.tsx
+++ b/src/views-components/dialog-create/dialog-collection-create.tsx
@@ -8,7 +8,6 @@ import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { CollectionCreateFormDialogData } from '~/store/collections/collection-create-actions';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { CollectionNameField, CollectionDescriptionField } from '~/views-components/form-fields/collection-form-fields';
-import { require } from '~/validators/require';
 import { FileUploaderField } from '../file-uploader/file-uploader';
 
 
@@ -27,7 +26,6 @@ const CollectionAddFields = () => <span>
     <CollectionDescriptionField />
     <Field
         name='files'
-        validate={[require]}
         label='Files'
         component={FileUploaderField} />
 </span>;

commit 3c722b14754ee83bf5cda8bceb8de03c28429f02
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Wed May 27 18:21:38 2020 -0300

    16439: Fixes WebDAV request URL.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/common/webdav.ts b/src/common/webdav.ts
index a09e8fdd..b2f43348 100644
--- a/src/common/webdav.ts
+++ b/src/common/webdav.ts
@@ -61,7 +61,11 @@ export class WebDAV {
     private request = (config: RequestConfig) => {
         return new Promise<XMLHttpRequest>((resolve, reject) => {
             const r = this.createRequest();
-            r.open(config.method, this.defaults.baseURL + config.url);
+            this.defaults.baseURL = this.defaults.baseURL.replace(/\/+$/, '');
+            r.open(config.method,
+                `${this.defaults.baseURL
+                    ? this.defaults.baseURL+'/'
+                    : ''}${config.url}`);
             const headers = { ...this.defaults.headers, ...config.headers };
             Object
                 .keys(headers)

commit 6f5c42c1239fe585b805088eb2b4baf783584cc7
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Apr 6 17:15:30 2020 -0300

    Upgrades vulnerable minimist module by upgrading handlebars (2 levels up).
    
    No issue #
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/yarn.lock b/yarn.lock
index 876d073e..8ed5fe10 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4549,13 +4549,14 @@ handle-thing@^2.0.0:
   integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==
 
 handlebars@^4.0.3:
-  version "4.7.2"
-  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.2.tgz#01127b3840156a0927058779482031afe0e730d7"
-  integrity sha512-4PwqDL2laXtTWZghzzCtunQUTLbo31pcCJrd/B/9JP8XbhVzpS5ZXuKqlOzsd1rtcaLo4KqAn8nl8mkknS4MHw==
+  version "4.7.6"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e"
+  integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==
   dependencies:
+    minimist "^1.2.5"
     neo-async "^2.6.0"
-    optimist "^0.6.1"
     source-map "^0.6.1"
+    wordwrap "^1.0.0"
   optionalDependencies:
     uglify-js "^3.1.4"
 
@@ -6780,16 +6781,11 @@ minimist at 0.0.8:
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
   integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
 
-minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
+minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
-minimist@~0.0.1:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
-  integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
-
 mississippi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -7335,14 +7331,6 @@ opn@^5.1.0, opn@^5.5.0:
   dependencies:
     is-wsl "^1.1.0"
 
-optimist@^0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
-  integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
-  dependencies:
-    minimist "~0.0.1"
-    wordwrap "~0.0.2"
-
 optionator@^0.8.1:
   version "0.8.3"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@@ -11008,10 +10996,10 @@ wordwrap at 0.0.2:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
   integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=
 
-wordwrap@~0.0.2:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
-  integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
+wordwrap@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+  integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
 
 worker-farm@^1.3.1, worker-farm@^1.5.2, worker-farm@^1.7.0:
   version "1.7.0"

commit 877b4f6e64dcd96b31e2ffc1610520d373c1a033
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Apr 6 16:59:19 2020 -0300

    Upgrades minimist package (indirect dependency) to address CVE-2020-7598.
    
    No issue #
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/yarn.lock b/yarn.lock
index 44b098f0..876d073e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6781,9 +6781,9 @@ minimist at 0.0.8:
   integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
 
 minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
-  integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
 minimist@~0.0.1:
   version "0.0.10"

commit 17ed55d1651843fc0ecf0ff5d62db80adfc842a1
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Apr 6 16:51:53 2020 -0300

    Upgrades acorn package (indirect dependency) to address a security issue.
    
    https://www.npmjs.com/advisories/1488
    
    No issue #
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/yarn.lock b/yarn.lock
index 41aaa359..44b098f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -584,19 +584,14 @@ acorn@^4.0.3, acorn@^4.0.4:
   integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=
 
 acorn@^5.0.0, acorn@^5.5.3:
-  version "5.7.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
-  integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
-
-acorn@^6.0.1:
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.0.tgz#b659d2ffbafa24baf5db1cdbb2c94a983ecd2784"
-  integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==
-
-acorn@^6.2.1:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e"
-  integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==
+  version "5.7.4"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
+  integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
+
+acorn@^6.0.1, acorn@^6.2.1:
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
+  integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
 
 address at 1.0.3:
   version "1.0.3"

commit 188c5c005b522683143d76d26529d37b88171ba9
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Mar 10 19:11:42 2020 -0300

    15951: Fixes 'select columns' button tooltip label & behavior.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/components/column-selector/column-selector.tsx b/src/components/column-selector/column-selector.tsx
index ccff6181..566bb9d6 100644
--- a/src/components/column-selector/column-selector.tsx
+++ b/src/components/column-selector/column-selector.tsx
@@ -55,8 +55,8 @@ export const ColumnSelector = withStyles(styles)(
 );
 
 export const ColumnSelectorTrigger = (props: IconButtonProps) =>
-    <Tooltip title="Filters">
+    <Tooltip disableFocusListener title="Select columns">
         <IconButton {...props}>
-            <MenuIcon aria-label="Filters" />
+            <MenuIcon aria-label="Select columns" />
         </IconButton>
     </Tooltip>;

commit 9593a6d5074b5aa817583941089207280dc74c64
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Mar 3 22:37:21 2020 -0300

    15951: Removes dead code, updates tests to work with component being used.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/components/data-table-filters/data-table-filters-popover.test.tsx b/src/components/data-table-filters/data-table-filters-popover.test.tsx
new file mode 100644
index 00000000..f7bd00f0
--- /dev/null
+++ b/src/components/data-table-filters/data-table-filters-popover.test.tsx
@@ -0,0 +1,24 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { mount, configure } from "enzyme";
+import { DataTableFiltersPopover } from "./data-table-filters-popover";
+import * as Adapter from 'enzyme-adapter-react-16';
+import { Checkbox, IconButton } from "@material-ui/core";
+import { getInitialProcessStatusFilters } from "~/store/resource-type-filters/resource-type-filters"
+
+configure({ adapter: new Adapter() });
+
+describe("<DataTableFiltersPopover />", () => {
+    it("renders filters according to their state", () => {
+        // 1st filter (All) is selected, the rest aren't.
+        const filters = getInitialProcessStatusFilters()
+
+        const dataTableFilter = mount(<DataTableFiltersPopover name="" filters={filters} />);
+        dataTableFilter.find(IconButton).simulate("click");
+        expect(dataTableFilter.find(Checkbox).at(0).prop("checked")).toBeTruthy();
+        expect(dataTableFilter.find(Checkbox).at(1).prop("checked")).toBeFalsy();
+    });
+});
diff --git a/src/components/data-table-filters/data-table-filters.test.tsx b/src/components/data-table-filters/data-table-filters.test.tsx
deleted file mode 100644
index dc1969f4..00000000
--- a/src/components/data-table-filters/data-table-filters.test.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from "react";
-import { mount, configure } from "enzyme";
-import { DataTableFilters } from "./data-table-filters";
-import * as Adapter from 'enzyme-adapter-react-16';
-import { Checkbox, ButtonBase } from "@material-ui/core";
-
-configure({ adapter: new Adapter() });
-
-describe("<DataTableFilter />", () => {
-    it("renders filters according to their state", () => {
-        const filters = [{
-            name: "Filter 1",
-            selected: true
-        }, {
-            name: "Filter 2",
-            selected: false
-        }];
-        const dataTableFilter = mount(<DataTableFilters name="" filters={filters} />);
-        dataTableFilter.find(ButtonBase).simulate("click");
-        expect(dataTableFilter.find(Checkbox).at(0).prop("checked")).toBeTruthy();
-        expect(dataTableFilter.find(Checkbox).at(1).prop("checked")).toBeFalsy();
-    });
-});
diff --git a/src/components/data-table-filters/data-table-filters.tsx b/src/components/data-table-filters/data-table-filters.tsx
index a57f29aa..ed7b30e7 100644
--- a/src/components/data-table-filters/data-table-filters.tsx
+++ b/src/components/data-table-filters/data-table-filters.tsx
@@ -2,207 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from "react";
-import {
-    WithStyles,
-    withStyles,
-    ButtonBase,
-    StyleRulesCallback,
-    Theme,
-    Popover,
-    List,
-    ListItem,
-    Checkbox,
-    ListItemText,
-    Button,
-    Card,
-    CardActions,
-    Typography,
-    CardContent,
-    Tooltip
-} from "@material-ui/core";
-import * as classnames from "classnames";
-import { DefaultTransformOrigin } from "../popover/helpers";
-import { createTree, initTreeNode, mapTree } from '~/models/tree';
-import { DataTableFilters as DataTableFiltersModel, DataTableFiltersTree } from "./data-table-filters-tree";
-import { pipe } from 'lodash/fp';
-import { setNode } from '~/models/tree';
-
-export type CssRules = "root" | "icon" | "active" | "checkbox";
-
-const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
-    root: {
-        cursor: "pointer",
-        display: "inline-flex",
-        justifyContent: "flex-start",
-        flexDirection: "inherit",
-        alignItems: "center",
-        "&:hover": {
-            color: theme.palette.text.primary,
-        },
-        "&:focus": {
-            color: theme.palette.text.primary,
-        },
-    },
-    active: {
-        color: theme.palette.text.primary,
-        '& $icon': {
-            opacity: 1,
-        },
-    },
-    icon: {
-        marginRight: 4,
-        marginLeft: 4,
-        opacity: 0.7,
-        userSelect: "none",
-        width: 16
-    },
-    checkbox: {
-        width: 24,
-        height: 24
-    }
-});
-
 export interface DataTableFilterItem {
     name: string;
     selected: boolean;
 }
-
-export interface DataTableFilterProps {
-    name: string;
-    filters: DataTableFilterItem[];
-    onChange?: (filters: DataTableFilterItem[]) => void;
-}
-
-interface DataTableFilterState {
-    anchorEl?: HTMLElement;
-    filters: DataTableFilterItem[];
-    prevFilters: DataTableFilterItem[];
-    filtersTree: DataTableFiltersModel;
-}
-
-const filters: DataTableFiltersModel = pipe(
-    createTree,
-    setNode(initTreeNode({ id: 'Project', value: { name: 'Project' } })),
-    setNode(initTreeNode({ id: 'Process', value: { name: 'Process' } })),
-    setNode(initTreeNode({ id: 'Data collection', value: { name: 'Data collection' } })),
-    setNode(initTreeNode({ id: 'General', parent: 'Data collection', value: { name: 'General' } })),
-    setNode(initTreeNode({ id: 'Output', parent: 'Data collection', value: { name: 'Output' } })),
-    setNode(initTreeNode({ id: 'Log', parent: 'Data collection', value: { name: 'Log' } })),
-    mapTree(node => ({...node, selected: true})),
-)();
-
-export const DataTableFilters = withStyles(styles)(
-    class extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
-        state: DataTableFilterState = {
-            anchorEl: undefined,
-            filters: [],
-            prevFilters: [],
-            filtersTree: filters,
-        };
-        icon = React.createRef<HTMLElement>();
-
-        render() {
-            const { name, classes, children } = this.props;
-            const isActive = this.state.filters.some(f => f.selected);
-            return <>
-                <Tooltip title='Filters'>
-                    <ButtonBase
-                        className={classnames([classes.root, { [classes.active]: isActive }])}
-                        component="span"
-                        onClick={this.open}
-                        disableRipple>
-                        {children}
-                        <i className={classnames(["fas fa-filter", classes.icon])}
-                            data-fa-transform="shrink-3"
-                            ref={this.icon} />
-                    </ButtonBase>
-                </Tooltip>
-                <Popover
-                    anchorEl={this.state.anchorEl}
-                    open={!!this.state.anchorEl}
-                    anchorOrigin={DefaultTransformOrigin}
-                    transformOrigin={DefaultTransformOrigin}
-                    onClose={this.cancel}>
-                    <Card>
-                        <CardContent>
-                            <Typography variant="caption">
-                                {name}
-                            </Typography>
-                        </CardContent>
-                        <List dense>
-                            {this.state.filters.map((filter, index) =>
-                                <ListItem
-                                    key={index}>
-                                    <Checkbox
-                                        onClick={this.toggleFilter(filter)}
-                                        color="primary"
-                                        checked={filter.selected}
-                                        className={classes.checkbox} />
-                                    <ListItemText>
-                                        {filter.name}
-                                    </ListItemText>
-                                </ListItem>
-                            )}
-                        </List>
-                        <DataTableFiltersTree
-                            filters={this.state.filtersTree}
-                            onChange={filtersTree => this.setState({ filtersTree })} />
-                        <CardActions>
-                            <Button
-                                color="primary"
-                                variant='contained'
-                                size="small"
-                                onClick={this.submit}>
-                                Ok
-                            </Button>
-                            <Button
-                                color="primary"
-                                variant="outlined"
-                                size="small"
-                                onClick={this.cancel}>
-                                Cancel
-                            </Button>
-                        </CardActions >
-                    </Card>
-                </Popover>
-            </>;
-        }
-
-        static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
-            return props.filters !== state.prevFilters
-                ? { ...state, filters: props.filters, prevFilters: props.filters }
-                : state;
-        }
-
-        open = () => {
-            this.setState({ anchorEl: this.icon.current || undefined });
-        }
-
-        submit = () => {
-            const { onChange } = this.props;
-            if (onChange) {
-                onChange(this.state.filters);
-            }
-            this.setState({ anchorEl: undefined });
-        }
-
-        cancel = () => {
-            this.setState(prev => ({
-                ...prev,
-                filters: prev.prevFilters,
-                anchorEl: undefined
-            }));
-        }
-
-        toggleFilter = (toggledFilter: DataTableFilterItem) => () => {
-            this.setState(prev => ({
-                ...prev,
-                filters: prev.filters.map(filter =>
-                    filter === toggledFilter
-                        ? { ...filter, selected: !filter.selected }
-                        : filter)
-            }));
-        }
-    }
-);
diff --git a/src/components/data-table/data-table.test.tsx b/src/components/data-table/data-table.test.tsx
index d0b83b96..3e4cc212 100644
--- a/src/components/data-table/data-table.test.tsx
+++ b/src/components/data-table/data-table.test.tsx
@@ -8,7 +8,6 @@ import { pipe } from 'lodash/fp';
 import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
 import * as Adapter from "enzyme-adapter-react-16";
 import { DataTable, DataColumns } from "./data-table";
-import { DataTableFilters } from "~/components/data-table-filters/data-table-filters";
 import { SortDirection, createDataColumn } from "./data-column";
 import { DataTableFiltersPopover } from '~/components/data-table-filters/data-table-filters-popover';
 import { createTree, setNode, initTreeNode } from '~/models/tree';
@@ -162,7 +161,7 @@ describe("<DataTable />", () => {
         expect(onSortToggle).toHaveBeenCalledWith(columns[0]);
     });
 
-    it("does not display <DataTableFilter /> if there is no filters provided", () => {
+    it("does not display <DataTableFiltersPopover /> if there is no filters provided", () => {
         const columns: DataColumns<string> = [{
             name: "Column 1",
             sortDirection: SortDirection.ASC,
@@ -180,7 +179,7 @@ describe("<DataTable />", () => {
             onRowDoubleClick={jest.fn()}
             onSortToggle={jest.fn()}
             onContextMenu={jest.fn()} />);
-        expect(dataTable.find(DataTableFilters)).toHaveLength(0);
+        expect(dataTable.find(DataTableFiltersPopover)).toHaveLength(0);
     });
 
     it("passes filter props to <DataTableFiltersPopover />", () => {
@@ -209,24 +208,4 @@ describe("<DataTable />", () => {
         dataTable.find(DataTableFiltersPopover).prop("onChange")([]);
         expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
     });
-
-    // it("shows default view if there is no items", () => {
-    //     const columns: DataColumns<string> = [
-    //         createDataColumn({
-    //             name: "Column 1",
-    //             render: () => <span />,
-    //             selected: true,
-    //             configurable: true
-    //         }),
-    //     ];
-    //     const dataTable = mount(<DataTable
-    //         columns={columns}
-    //         items={[]}
-    //         onFiltersChange={jest.fn()}
-    //         onRowClick={jest.fn()}
-    //         onRowDoubleClick={jest.fn()}
-    //         onContextMenu={jest.fn()}
-    //         onSortToggle={jest.fn()} />);
-    //     expect(dataTable.find(DataTableDefaultView)).toHaveLength(1);
-    // });
 });

commit f1e3bc95a43facf190d5f4250299fab0a267d10b
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Mar 3 21:42:12 2020 -0300

    15951: Hides tooltips on filter buttons after being clicked.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/components/data-table-filters/data-table-filters-popover.tsx b/src/components/data-table-filters/data-table-filters-popover.tsx
index 30b98d33..456d2375 100644
--- a/src/components/data-table-filters/data-table-filters-popover.tsx
+++ b/src/components/data-table-filters/data-table-filters-popover.tsx
@@ -108,7 +108,7 @@ export const DataTableFiltersPopover = withStyles(styles)(
                     : f.selected
                 );
             return <>
-                <Tooltip title='Filters'>
+                <Tooltip disableFocusListener title='Filters'>
                     <ButtonBase
                         className={classnames([classes.root, { [classes.active]: isActive }])}
                         component="span"

commit b76d55cc985f22cab2f59ec725f8b8a1b88d003e
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Mar 3 21:20:28 2020 -0300

    15951: Accepts filter toggle actions when clicking on the entire row.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/components/data-table-filters/data-table-filters-tree.tsx b/src/components/data-table-filters/data-table-filters-tree.tsx
index be08743e..5f0d5e39 100644
--- a/src/components/data-table-filters/data-table-filters-tree.tsx
+++ b/src/components/data-table-filters/data-table-filters-tree.tsx
@@ -39,9 +39,12 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
             useRadioButtons={this.props.mutuallyExclusive}
             disableRipple
             onContextMenu={noop}
-            toggleItemActive={this.props.mutuallyExclusive ? this.toggleRadioButtonFilter : noop}
+            toggleItemActive={
+                this.props.mutuallyExclusive
+                    ? this.toggleRadioButtonFilter
+                    : this.toggleFilter
+            }
             toggleItemOpen={this.toggleOpen}
-            toggleItemSelection={this.toggleFilter}
         />;
     }
 

commit 65f7ca2fd28e49891f942b639be43dc4f594a379
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Tue Mar 3 18:45:05 2020 -0300

    15951: Makes 'mutually exclusive' filter dialog auto-submit selection on click.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/components/data-table-filters/data-table-filters-popover.tsx b/src/components/data-table-filters/data-table-filters-popover.tsx
index 670afa95..30b98d33 100644
--- a/src/components/data-table-filters/data-table-filters-popover.tsx
+++ b/src/components/data-table-filters/data-table-filters-popover.tsx
@@ -139,7 +139,15 @@ export const DataTableFiltersPopover = withStyles(styles)(
                             mutuallyExclusive={this.props.mutuallyExclusive}
                             onChange={filters => {
                                 this.setState({ filters });
+                                if (this.props.mutuallyExclusive) {
+                                    const { onChange } = this.props;
+                                    if (onChange) {
+                                        onChange(filters);
+                                    }
+                                    this.setState({ anchorEl: undefined });
+                                }
                             }} />
+                        {this.props.mutuallyExclusive ||
                         <CardActions>
                             <Button
                                 color="primary"
@@ -156,6 +164,7 @@ export const DataTableFiltersPopover = withStyles(styles)(
                                 Cancel
                             </Button>
                         </CardActions >
+                        }
                     </Card>
                 </Popover>
             </>;
diff --git a/src/components/data-table-filters/data-table-filters-tree.tsx b/src/components/data-table-filters/data-table-filters-tree.tsx
index d964012d..be08743e 100644
--- a/src/components/data-table-filters/data-table-filters-tree.tsx
+++ b/src/components/data-table-filters/data-table-filters-tree.tsx
@@ -37,10 +37,9 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
             render={renderItem}
             showSelection
             useRadioButtons={this.props.mutuallyExclusive}
-            toggleItemRadioButton={this.toggleRadioButtonFilter}
             disableRipple
             onContextMenu={noop}
-            toggleItemActive={noop}
+            toggleItemActive={this.props.mutuallyExclusive ? this.toggleRadioButtonFilter : noop}
             toggleItemOpen={this.toggleOpen}
             toggleItemSelection={this.toggleFilter}
         />;
@@ -50,7 +49,7 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
      * Handler for when a tree item is toggled via a radio button.
      * Ensures mutual exclusivity among filter tree items.
      */
-    toggleRadioButtonFilter = (item: TreeItem<DataTableFilterItem>) => {
+    toggleRadioButtonFilter = (_: any, item: TreeItem<DataTableFilterItem>) => {
         const { onChange = noop } = this.props;
 
         // If the filter is already selected, do nothing.
diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx
index 67858e9b..76fbf011 100644
--- a/src/components/tree/tree.tsx
+++ b/src/components/tree/tree.tsx
@@ -98,15 +98,9 @@ export interface TreeProps<T> {
     /**
      * When set to true use radio buttons instead of checkboxes for item selection.
      * This does not guarantee radio group behavior (i.e item mutual exclusivity).
-     * Any item selection logic must be done in the toggleItemRadioButton callback prop.
+     * Any item selection logic must be done in the toggleItemActive callback prop.
      */
     useRadioButtons?: boolean;
-
-    /**
-     * Called when selection of an item in the tree is toggled via a radio button.
-     * Use this callback prop to implement any selection logic (i.e item mutual exclusivity).
-     */
-    toggleItemRadioButton?: (item: TreeItem<T>) => void;
 }
 
 export const Tree = withStyles(styles)(
@@ -151,8 +145,7 @@ export const Tree = withStyles(styles)(
                                 <Radio
                                     checked={it.selected}
                                     className={classes.checkbox}
-                                    color="primary"
-                                    onChange={this.handleRadioButtonChange(it)} />}
+                                    color="primary" />}
                             <div className={renderContainer}>
                                 {render(it, level)}
                             </div>
@@ -207,16 +200,6 @@ export const Tree = withStyles(styles)(
                 : undefined;
         }
 
-        handleRadioButtonChange = (item: TreeItem<T>) => {
-            const { toggleItemRadioButton } = this.props;
-            return toggleItemRadioButton
-                ? (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => {
-                    event.stopPropagation();
-                    toggleItemRadioButton(item);
-                }
-                : undefined;
-        }
-
         handleToggleItemOpen = (item: TreeItem<T>) => (event: React.MouseEvent<HTMLElement>) => {
             event.stopPropagation();
             this.props.toggleItemOpen(event, item);

commit 5924c2b69f48a6b91243d1593b4f16bab0eace56
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Feb 27 14:50:28 2020 -0500

    16181: Default to empty string if SSHHelpHostSuffix undefined
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx
index 5e131861..49d880e5 100644
--- a/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx
+++ b/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx
@@ -60,7 +60,7 @@ const mapStateToProps = (state: RootState) => {
         requestedDate: state.virtualMachines.date,
         userUuid: state.auth.user!.uuid,
         helpText: state.auth.config.clusterConfig.Workbench.SSHHelpPageHTML,
-        hostSuffix: state.auth.config.clusterConfig.Workbench.SSHHelpHostSuffix,
+        hostSuffix: state.auth.config.clusterConfig.Workbench.SSHHelpHostSuffix || "",
         webShell: state.auth.config.clusterConfig.Services.WebShell.ExternalURL,
         ...state.virtualMachines
     };

commit 63fe27f7e2715f9e6d5ea90cd1be791ea96410da
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Feb 27 10:44:42 2020 -0500

    16181: Rework the user attributes dialog a bit
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/views-components/user-dialog/attributes-dialog.tsx b/src/views-components/user-dialog/attributes-dialog.tsx
index 67b267ae..df44d62c 100644
--- a/src/views-components/user-dialog/attributes-dialog.tsx
+++ b/src/views-components/user-dialog/attributes-dialog.tsx
@@ -61,35 +61,38 @@ export const UserAttributesDialog = compose(
     );
 
 const attributes = (user: UserResource, classes: any) => {
-    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, firstName, lastName, href, username } = user;
+    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid,
+        firstName, lastName, username, email, isActive, isAdmin } = user;
     return (
         <span>
             <Grid container direction="row">
                 <Grid item xs={5} className={classes.rightContainer}>
+                    {uuid && <Grid item>Uuid</Grid>}
                     {firstName && <Grid item>First name</Grid>}
                     {lastName && <Grid item>Last name</Grid>}
-                    {ownerUuid && <Grid item>Owner uuid</Grid>}
+                    {email && <Grid item>Email</Grid>}
+                    {username && <Grid item>Username</Grid>}
+                    {isActive && <Grid item>Is active</Grid>}
+                    {isAdmin && <Grid item>Is admin</Grid>}
                     {createdAt && <Grid item>Created at</Grid>}
                     {modifiedAt && <Grid item>Modified at</Grid>}
+                    {ownerUuid && <Grid item>Owner uuid</Grid>}
                     {modifiedByUserUuid && <Grid item>Modified by user uuid</Grid>}
                     {modifiedByClientUuid && <Grid item>Modified by client uuid</Grid>}
-                    {uuid && <Grid item>uuid</Grid>}
-                    {href && <Grid item>Href</Grid>}
-                    {username && <Grid item>Username</Grid>}
-                    {username && <Grid item>Username</Grid>}
                 </Grid>
                 <Grid item xs={7} className={classes.leftContainer}>
+                    <Grid item>{uuid}</Grid>
                     <Grid item>{firstName}</Grid>
                     <Grid item>{lastName}</Grid>
-                    <Grid item>{ownerUuid}</Grid>
+                    <Grid item>{email}</Grid>
+                    <Grid item>{username}</Grid>
+                    <Grid item>{isActive}</Grid>
+                    <Grid item>{isAdmin}</Grid>
                     <Grid item>{createdAt}</Grid>
                     <Grid item>{modifiedAt}</Grid>
+                    <Grid item>{ownerUuid}</Grid>
                     <Grid item>{modifiedByUserUuid}</Grid>
                     <Grid item>{modifiedByClientUuid}</Grid>
-                    <Grid item>{uuid}</Grid>
-                    <Grid item>{href}</Grid>
-                    <Grid item>{username}</Grid>
-                    <Grid item>{username}</Grid>
                 </Grid>
             </Grid>
         </span>

commit 987db424780cd47b96305bb16ec744b284131828
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Feb 27 10:02:32 2020 -0500

    16181: Support SSHHelpHostSuffix, fix user VM page render issues
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/common/config.ts b/src/common/config.ts
index 23faaf91..356ad391 100644
--- a/src/common/config.ts
+++ b/src/common/config.ts
@@ -53,6 +53,7 @@ export interface ClusterConfigJSON {
         WelcomePageHTML: string;
         InactivePageHTML: string;
         SSHHelpPageHTML: string;
+        SSHHelpHostSuffix: string;
         SiteName: string;
     };
     Login: {
@@ -168,6 +169,7 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
         WelcomePageHTML: "",
         InactivePageHTML: "",
         SSHHelpPageHTML: "",
+        SSHHelpHostSuffix: "",
         SiteName: "",
     },
     Login: {
diff --git a/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx b/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx
index a641ec63..5e131861 100644
--- a/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx
+++ b/src/views/virtual-machine-panel/virtual-machine-user-panel.tsx
@@ -12,6 +12,7 @@ import { saveRequestedDate, loadVirtualMachinesUserData } from '~/store/virtual-
 import { RootState } from '~/store/store';
 import { ListResults } from '~/services/common-service/common-service';
 import { HelpIcon } from '~/components/icon/icon';
+// import * as CopyToClipboard from 'react-copy-to-clipboard';
 
 type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon';
 
@@ -59,6 +60,7 @@ const mapStateToProps = (state: RootState) => {
         requestedDate: state.virtualMachines.date,
         userUuid: state.auth.user!.uuid,
         helpText: state.auth.config.clusterConfig.Workbench.SSHHelpPageHTML,
+        hostSuffix: state.auth.config.clusterConfig.Workbench.SSHHelpHostSuffix,
         webShell: state.auth.config.clusterConfig.Services.WebShell.ExternalURL,
         ...state.virtualMachines
     };
@@ -75,6 +77,7 @@ interface VirtualMachinesPanelDataProps {
     userUuid: string;
     links: ListResults<any>;
     helpText: string;
+    hostSuffix: string;
     webShell: string;
 }
 
@@ -166,25 +169,30 @@ const virtualMachinesTable = (props: VirtualMachineProps) =>
             </TableRow>
         </TableHead>
         <TableBody>
-            {props.virtualMachines.items.map((it, index) =>
-                <TableRow key={index}>
-                    <TableCell>{it.hostname}</TableCell>
-                    <TableCell>{getUsername(props.links, props.userUuid)}</TableCell>
-                    <TableCell>ssh {getUsername(props.links, props.userUuid)}@{it.hostname}</TableCell>
-                    {props.webShell !== "" && <TableCell>
-                        <a href={`${props.webShell}${it.href}/webshell/${getUsername(props.links, props.userUuid)}`} target="_blank" className={props.classes.link}>
-                            Log in as {getUsername(props.links, props.userUuid)}
-                        </a>
-                    </TableCell>}
-                </TableRow>
-            )}
+            {props.virtualMachines.items.map(it =>
+                props.links.items.map(lk => {
+                    if (lk.tailUuid === props.userUuid) {
+                        const username = lk.properties.username;
+                        const command = `ssh ${username}@${it.hostname}${props.hostSuffix}`;
+                        return <TableRow key={lk.uuid}>
+                            <TableCell>{it.hostname}</TableCell>
+                            <TableCell>{username}</TableCell>
+                            <TableCell>
+                                {command}
+                            </TableCell>
+                            {props.webShell !== "" && <TableCell>
+                                <a href={`${props.webShell}${it.href}/webshell/${username}`} target="_blank" className={props.classes.link}>
+                                    Log in as {username}
+                                </a>
+                            </TableCell>}
+                        </TableRow>;
+                    }
+                    return;
+                }
+                ))}
         </TableBody>
     </Table>;
 
-const getUsername = (links: ListResults<any>, userUuid: string) => {
-    return links.items.map(it => it.tailUuid === userUuid ? it.properties.username : '');
-};
-
 const CardSSHSection = (props: VirtualMachineProps) =>
     <Grid item xs={12}>
         <Card>

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list