15781: Merge branch 'master' into 15781-multi-value-property-edit
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 11 Feb 2020 13:54:41 +0000 (10:54 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 11 Feb 2020 13:54:41 +0000 (10:54 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

20 files changed:
src/common/config.ts
src/components/warning/warning.tsx
src/services/collection-service/collection-service.ts
src/services/common-service/common-resource-service.test.ts
src/services/common-service/common-resource-service.ts
src/services/common-service/common-service.ts
src/services/common-service/trashable-resource-service.ts
src/services/favorite-service/favorite-service.test.ts
src/services/favorite-service/favorite-service.ts
src/store/collections/collection-copy-actions.ts
src/store/collections/collection-update-actions.ts
src/store/projects/project-update-actions.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/tree-picker/tree-picker-actions.ts
src/store/users/users-actions.ts
src/validators/validators.tsx
src/views-components/context-menu/action-sets/user-action-set.ts
src/views-components/dialog-forms/setup-shell-account-dialog.tsx
src/views-components/main-app-bar/help-menu.tsx
src/views/virtual-machine-panel/virtual-machine-user-panel.tsx

index f44dc1680935596b14d3923b04b13b199e3abe84..23faaf91adbe7c75a16b566edc3538add8d6d5f6 100644 (file)
@@ -41,6 +41,9 @@ export interface ClusterConfigJSON {
         },
         WebDAVDownload: {
             ExternalURL: string
+        },
+        WebShell: {
+            ExternalURL: string
         }
     };
     Workbench: {
@@ -49,6 +52,7 @@ export interface ClusterConfigJSON {
         FileViewersConfigURL: string;
         WelcomePageHTML: string;
         InactivePageHTML: string;
+        SSHHelpPageHTML: string;
         SiteName: string;
     };
     Login: {
@@ -155,6 +159,7 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
         Websocket: { ExternalURL: "" },
         WebDAV: { ExternalURL: "" },
         WebDAVDownload: { ExternalURL: "" },
+        WebShell: { ExternalURL: "" },
     },
     Workbench: {
         ArvadosDocsite: "",
@@ -162,6 +167,7 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
         FileViewersConfigURL: "",
         WelcomePageHTML: "",
         InactivePageHTML: "",
+        SSHHelpPageHTML: "",
         SiteName: "",
     },
     Login: {
index bd3303616d7e41a9b7f618ec6742df558c8cc159..95e495d72a038db1b48a66d9f06738028803899f 100644 (file)
@@ -16,11 +16,12 @@ interface WarningComponentProps {
 }
 
 export const WarningComponent = ({ text, rules, message }: WarningComponentProps) =>
-    rules.find(aRule => text.match(aRule) !== null)
-        ? message
-            ? <Tooltip title={message}><ErrorIcon /></Tooltip>
-            : <ErrorIcon />
-        : null;
+    !text ? <Tooltip title={"No name"}><ErrorIcon /></Tooltip>
+        : (rules.find(aRule => text.match(aRule) !== null)
+            ? message
+                ? <Tooltip title={message}><ErrorIcon /></Tooltip>
+                : <ErrorIcon />
+            : null);
 
 interface IllegalNamingWarningProps {
     name: string;
index 6eb9b5ba664f15892d570206051a93803050987b..77f5bf3bc605b76a6a37053e9ff1b540925319a4 100644 (file)
@@ -15,7 +15,16 @@ export type UploadProgress = (fileId: number, loaded: number, total: number, cur
 
 export class CollectionService extends TrashableResourceService<CollectionResource> {
     constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
-        super(serverApi, "collections", actions);
+        super(serverApi, "collections", actions, [
+            'fileCount',
+            'fileSizeTotal',
+            'replicationConfirmed',
+            'replicationConfirmedAt',
+            'storageClassesConfirmed',
+            'storageClassesConfirmedAt',
+            'unsignedManifestText',
+            'version',
+        ]);
     }
 
     async files(uuid: string) {
index 2a18ce2369b7f7bd849646f7458b1d716f69bd93..038c6943fbf9df52b6fb491205c85de777f3dd8d 100644 (file)
@@ -48,6 +48,22 @@ describe("CommonResourceService", () => {
         expect(axiosInstance.post).toHaveBeenCalledWith("/resource", {owner_uuid: "ownerUuidValue"});
     });
 
+    it("#create ignores fields listed as readonly", async () => {
+        axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        // UUID fields are read-only on all resources.
+        await commonResourceService.create({ uuid: "this should be ignored", ownerUuid: "ownerUuidValue" });
+        expect(axiosInstance.post).toHaveBeenCalledWith("/resource", {owner_uuid: "ownerUuidValue"});
+    });
+
+    it("#update ignores fields listed as readonly", async () => {
+        axiosInstance.put = jest.fn(() => Promise.resolve({data: {}}));
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
+        // UUID fields are read-only on all resources.
+        await commonResourceService.update('resource-uuid', { uuid: "this should be ignored", ownerUuid: "ownerUuidValue" });
+        expect(axiosInstance.put).toHaveBeenCalledWith("/resource/resource-uuid", {owner_uuid: "ownerUuidValue"});
+    });
+
     it("#delete", async () => {
         axiosMock
             .onDelete("/resource/uuid")
index d29ea15642f47dd51153c4d9ceb6e1986617c4db..bc24f22796b21001bce09b358e1efe6c657d6248 100644 (file)
@@ -17,8 +17,26 @@ export enum CommonResourceServiceError {
 }
 
 export class CommonResourceService<T extends Resource> extends CommonService<T> {
-   constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
-        super(serverApi, resourceType, actions);
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
+        super(serverApi, resourceType, actions, readOnlyFields.concat([
+            'uuid',
+            'etag',
+            'kind'
+        ]));
+    }
+
+    create(data?: Partial<T>) {
+        if (data !== undefined) {
+            this.readOnlyFields.forEach( field => delete data[field] );
+        }
+        return super.create(data);
+    }
+
+    update(uuid: string, data: Partial<T>) {
+        if (data !== undefined) {
+            this.readOnlyFields.forEach( field => delete data[field] );
+        }
+        return super.update(uuid, data);
     }
 }
 
index d81178212711a7e7634e5493b7e516793d158aa2..44233eb17bd2f683ecf4fcb48459160dd583119e 100644 (file)
@@ -36,11 +36,13 @@ export class CommonService<T> {
     protected serverApi: AxiosInstance;
     protected resourceType: string;
     protected actions: ApiActions;
+    protected readOnlyFields: string[];
 
-    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
         this.serverApi = serverApi;
         this.resourceType = '/' + resourceType;
         this.actions = actions;
+        this.readOnlyFields = readOnlyFields;
     }
 
     static mapResponseKeys = (response: { data: any }) =>
index 5746bffb83136a6a80723d7da58b28ae0fe69301..63be3ab866673ee98084579d48bd160734ffa5f4 100644 (file)
@@ -10,8 +10,8 @@ import { ApiActions } from "~/services/api/api-actions";
 
 export class TrashableResourceService<T extends TrashableResource> extends CommonResourceService<T> {
 
-    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
-        super(serverApi, resourceType, actions);
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
+        super(serverApi, resourceType, actions, readOnlyFields);
     }
 
     trash(uuid: string): Promise<T> {
index 566b4cbcf82a857c6cbee714a102dd482358c586..e4cf23ef8eebaf45f557ec6f21d14eff7be65ce8 100644 (file)
@@ -39,7 +39,7 @@ describe("FavoriteService", () => {
     it("unmarks resource as favorite", async () => {
         const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "linkUuid" }] }));
         const filters = new FilterBuilder()
-            .addEqual('tail_uuid', "userUuid")
+            .addEqual('owner_uuid', "userUuid")
             .addEqual('head_uuid', "resourceUuid")
             .addEqual('link_class', LinkClass.STAR);
         linkService.list = list;
@@ -56,7 +56,7 @@ describe("FavoriteService", () => {
     it("lists favorite resources", async () => {
         const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "headUuid" }] }));
         const listFilters = new FilterBuilder()
-            .addEqual('tail_uuid', "userUuid")
+            .addEqual('owner_uuid', "userUuid")
             .addEqual('link_class', LinkClass.STAR);
         const contents = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "resourceUuid" }] }));
         const contentFilters = new FilterBuilder().addIn('uuid', ["headUuid"]);
@@ -76,7 +76,7 @@ describe("FavoriteService", () => {
         const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "foo" }] }));
         const listFilters = new FilterBuilder()
             .addIn("head_uuid", ["foo", "oof"])
-            .addEqual("tail_uuid", "userUuid")
+            .addEqual("owner_uuid", "userUuid")
             .addEqual("link_class", LinkClass.STAR);
         linkService.list = list;
         const favoriteService = new FavoriteService(linkService, groupService);
index 90849c8b95b1a79b632827ab75bf404b1c046744..fbb2a52f448920e8b34cccaac6219709193534c7 100644 (file)
@@ -20,7 +20,7 @@ export class FavoriteService {
     constructor(
         private linkService: LinkService,
         private groupsService: GroupsService,
-    ) {}
+    ) { }
 
     create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
         return this.linkService.create({
@@ -36,7 +36,7 @@ export class FavoriteService {
         return this.linkService
             .list({
                 filters: new FilterBuilder()
-                    .addEqual('tail_uuid', data.userUuid)
+                    .addEqual('owner_uuid', data.userUuid)
                     .addEqual('head_uuid', data.resourceUuid)
                     .addEqual('link_class', LinkClass.STAR)
                     .getFilters()
@@ -47,7 +47,7 @@ export class FavoriteService {
 
     list(userUuid: string, { filters, limit, offset, linkOrder, contentOrder }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
         const listFilters = new FilterBuilder()
-            .addEqual('tail_uuid', userUuid)
+            .addEqual('owner_uuid', userUuid)
             .addEqual('link_class', LinkClass.STAR)
             .getFilters();
 
@@ -75,7 +75,7 @@ export class FavoriteService {
             .list({
                 filters: new FilterBuilder()
                     .addIn("head_uuid", resourceUuids)
-                    .addEqual("tail_uuid", userUuid)
+                    .addEqual("owner_uuid", userUuid)
                     .addEqual("link_class", LinkClass.STAR)
                     .getFilters()
             })
index b13d08aaad236eb8eae6962a969fc1aaae79e326..cf87f3f104801ffe18fd916b18e8072b4958ff28 100644 (file)
@@ -28,13 +28,9 @@ export const copyCollection = (resource: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
         try {
-            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_COPY_FORM_NAME));
             const collection = await services.collectionService.get(resource.uuid);
-            const uuidKey = 'uuid';
-            delete collection[uuidKey];
             const newCollection = await services.collectionService.create({ ...collection, ownerUuid: resource.ownerUuid, name: resource.name });
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
-            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
             return newCollection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
@@ -47,7 +43,8 @@ export const copyCollection = (resource: CopyFormDialogData) =>
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
                 throw new Error('Could not copy the collection.');
             }
-            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
             return;
+        } finally {
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
         }
     };
index 5b176beac5bd6d2c67bcbca2204e70b307e1c369..f8fac398b7ad3458241213e4f4772be401f43074 100644 (file)
@@ -10,30 +10,29 @@ import { dialogActions } from "~/store/dialog/dialog-actions";
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { ServiceRepository } from "~/services/services";
 import { CollectionResource } from '~/models/collection';
-import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export interface CollectionUpdateFormDialogData {
     uuid: string;
     name: string;
-    description: string;
+    description?: string;
 }
 
 export const COLLECTION_UPDATE_FORM_NAME = 'collectionUpdateFormName';
 
-export const openCollectionUpdateDialog = (resource: ContextMenuResource) =>
+export const openCollectionUpdateDialog = (resource: CollectionUpdateFormDialogData) =>
     (dispatch: Dispatch) => {
         dispatch(initialize(COLLECTION_UPDATE_FORM_NAME, resource));
         dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME, data: {} }));
     };
 
-export const updateCollection = (collection: Partial<CollectionResource>) =>
+export const updateCollection = (collection: CollectionUpdateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const uuid = collection.uuid || '';
         dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
         dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
         try {
-            const updatedCollection = await services.collectionService.update(uuid, collection);
+            const updatedCollection = await services.collectionService.update(uuid, { name: collection.name, description: collection.description });
             dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
             dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
index 2449b9ce9bf1a049eeb7790cfe1fc84ee2ef6b3b..2cb0278872b20cb6150166c4e9cd28302277bf00 100644 (file)
@@ -8,32 +8,28 @@ import { RootState } from "~/store/store";
 import { dialogActions } from "~/store/dialog/dialog-actions";
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { ServiceRepository } from "~/services/services";
-import { ProjectResource } from '~/models/project';
-import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
-import { getResource } from '~/store/resources/resources';
 import { projectPanelActions } from '~/store/project-panel/project-panel-action';
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
     name: string;
-    description: string;
+    description?: string;
 }
 
 export const PROJECT_UPDATE_FORM_NAME = 'projectUpdateFormName';
 
-export const openProjectUpdateDialog = (resource: ContextMenuResource) =>
+export const openProjectUpdateDialog = (resource: ProjectUpdateFormDialogData) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const project = getResource(resource.uuid)(getState().resources);
-        dispatch(initialize(PROJECT_UPDATE_FORM_NAME, project));
+        dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource));
         dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {} }));
     };
 
-export const updateProject = (project: Partial<ProjectResource>) =>
+export const updateProject = (project: ProjectUpdateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const uuid = project.uuid || '';
         dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
         try {
-            const updatedProject = await services.projectService.update(uuid, project);
+            const updatedProject = await services.projectService.update(uuid, { name: project.name, description: project.description });
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
             return updatedProject;
index 1f18b85c521a4d12ceb3a684f66ec98b54fefa9a..a09ab6ba6e41ad84d094251f1675172dc078a005 100644 (file)
@@ -22,7 +22,7 @@ export enum SidePanelTreeCategory {
     SHARED_WITH_ME = 'Shared with me',
     PUBLIC_FAVORITES = 'Public Favorites',
     WORKFLOWS = 'Workflows',
-    FAVORITES = 'Favorites',
+    FAVORITES = 'My Favorites',
     TRASH = 'Trash',
     ALL_PROCESSES = 'All Processes'
 }
@@ -45,10 +45,10 @@ export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker)
 };
 
 const SIDE_PANEL_CATEGORIES = [
-    SidePanelTreeCategory.ALL_PROCESSES,
     SidePanelTreeCategory.PUBLIC_FAVORITES,
-    SidePanelTreeCategory.WORKFLOWS,
     SidePanelTreeCategory.FAVORITES,
+    SidePanelTreeCategory.WORKFLOWS,
+    SidePanelTreeCategory.ALL_PROCESSES,
     SidePanelTreeCategory.TRASH,
 ];
 
index 6b9be5396396a1421fbe603f9d7cb6bd8ef851b4..5e880aad9587583ffaeebde865b675612b752785 100644 (file)
@@ -281,7 +281,6 @@ export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =
                 fb => fb
                     .addEqual('link_class', LinkClass.STAR)
                     .addEqual('owner_uuid', uuid)
-                    .addLike('name', '')
                     .getFilters(),
             )(new FilterBuilder());
 
index f6287260d67e816603ea9ef47ad139932ca0118d..7b12fe75633ec403a8978bb15c9ece5bc9012cce 100644 (file)
@@ -27,6 +27,12 @@ export interface UserCreateFormDialogData {
     groupVirtualMachine: string;
 }
 
+export interface SetupShellAccountFormDialogData {
+    email: string;
+    virtualMachineName: string;
+    groupVirtualMachine: string;
+}
+
 export const openUserAttributes = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { resources } = getState();
@@ -94,6 +100,23 @@ export const createUser = (user: UserCreateFormDialogData) =>
         }
     };
 
+
+export const setupUserVM = (setupData: SetupShellAccountFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(startSubmit(USER_CREATE_FORM_NAME));
+        try {
+            // TODO: make correct API call
+            // const setupResult = await services.userService.setup({ ...setupData });
+            dispatch(dialogActions.CLOSE_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG }));
+            dispatch(reset(SETUP_SHELL_ACCOUNT_DIALOG));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been added to VM.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+            dispatch<any>(loadUsersPanel());
+            dispatch(userBindedActions.REQUEST_ITEMS());
+        } catch (e) {
+            return;
+        }
+    };
+
 export const openUserPanel = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const user = getState().auth.user;
index 605d051c789388f34a21c0b1c3df89a6cb890f69..d9eca97f4c2a85b54a424497f3bce836e1380112 100644 (file)
@@ -38,3 +38,5 @@ export const SSH_KEY_NAME_VALIDATION = [require, maxLength(255)];
 export const SITE_MANAGER_REMOTE_HOST_VALIDATION = [require, isRemoteHost, maxLength(255)];
 
 export const MY_ACCOUNT_VALIDATION = [require];
+
+export const CHOOSE_VM_VALIDATION = [require];
index d2b97d15525755f6d66260ff6b78bdf7f9d3400e..2582800e5b7ed73dbef42577b7655e1f44d2c0e8 100644 (file)
@@ -3,9 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, ProjectIcon, AttributesIcon, UserPanelIcon } from "~/components/icon/icon";
+import { AdvancedIcon, ProjectIcon, AttributesIcon } from "~/components/icon/icon";
 import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
-import { openUserAttributes, openUserProjects, openUserManagement } from "~/store/users/users-actions";
+import { openUserAttributes, openUserProjects } from "~/store/users/users-actions";
 
 export const userActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
@@ -25,10 +25,13 @@ export const userActionSet: ContextMenuActionSet = [[{
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
     }
-}, {
+}, /*
+    // Neither of the buttons on this dialog work correctly (bugs #16114 and #16124) so hide it for now.
+    {
     name: "Manage",
     icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openUserManagement(uuid));
     }
-}]];
+} */
+]];
index c53f53c2ad90346572ff53cdf7ccd7a975238218..7e8a657ac032e70e7c9884e1578667d2d5e889b4 100644 (file)
@@ -8,24 +8,18 @@ import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { TextField } from '~/components/text-field/text-field';
 import { VirtualMachinesResource } from '~/models/virtual-machines';
-import { USER_LENGTH_VALIDATION } from '~/validators/validators';
+import { USER_LENGTH_VALIDATION, CHOOSE_VM_VALIDATION } from '~/validators/validators';
 import { InputLabel } from '@material-ui/core';
 import { NativeSelectField } from '~/components/select-field/select-field';
-import { SETUP_SHELL_ACCOUNT_DIALOG, createUser } from '~/store/users/users-actions';
+import { SetupShellAccountFormDialogData, SETUP_SHELL_ACCOUNT_DIALOG, setupUserVM } from '~/store/users/users-actions';
 import { UserResource } from '~/models/user';
 
-interface SetupShellAccountFormDialogData {
-    email: string;
-    virtualMachineName: string;
-    groupVirtualMachine: string;
-}
-
 export const SetupShellAccountDialog = compose(
     withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
     reduxForm<SetupShellAccountFormDialogData>({
         form: SETUP_SHELL_ACCOUNT_DIALOG,
         onSubmit: (data, dispatch) => {
-            dispatch(createUser(data));
+            dispatch(setupUserVM(data));
         }
     })
 )(
@@ -68,7 +62,7 @@ const UserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
         <Field
             name='virtualMachine'
             component={NativeSelectField}
-            validate={USER_LENGTH_VALIDATION}
+            validate={CHOOSE_VM_VALIDATION}
             items={getVirtualMachinesList(data.items)} />
     </div>;
 
@@ -80,7 +74,7 @@ const UserGroupsVirtualMachineField = () =>
         label="Groups for virtual machine (comma separated list)" />;
 
 const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
-    virtualMachines.map(it => ({ key: it.hostname, value: it.hostname }));
+    [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.hostname, value: it.hostname })));
 
 type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<SetupShellAccountFormDialogData>;
 
@@ -90,6 +84,3 @@ const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProp
         <UserVirtualMachineField data={props.data as DataProps} />
         <UserGroupsVirtualMachineField />
     </>;
-
-
-
index 859c907a6fd88553118038df3c7b763ea0b70873..350f9aa6322980c9c876305f338004e9d01089bf 100644 (file)
@@ -37,10 +37,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 const links = [
-    {
-        title: "Public Pipelines and Data sets",
-        link: "https://dev.arvados.org/projects/arvados/wiki/Public_Pipelines_and_Datasets",
-    },
     {
         title: "Tutorials and User guide",
         link: "http://doc.arvados.org/user/",
@@ -78,7 +74,7 @@ export const HelpMenu = compose(
                         <MenuItem key={link.title}>
                             <a href={link.link} target="_blank" className={classes.link}>
                                 <ImportContactsIcon className={classes.icon} />
-                                <Typography  className={classes.linkTitle}>{link.title}</Typography>
+                                <Typography className={classes.linkTitle}>{link.title}</Typography>
                             </a>
                         </MenuItem>
                     )
index 291041b3b984cbf4ddf3951b99c1931043f5335c..a641ec63cd8b8c2a687e6fb68755ad7f2ecb17e2 100644 (file)
@@ -7,14 +7,11 @@ import { connect } from 'react-redux';
 import { Grid, Typography, Button, Card, CardContent, TableBody, TableCell, TableHead, TableRow, Table, Tooltip } from '@material-ui/core';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
-import { Link } from 'react-router-dom';
 import { compose, Dispatch } from 'redux';
 import { saveRequestedDate, loadVirtualMachinesUserData } from '~/store/virtual-machines/virtual-machines-actions';
 import { RootState } from '~/store/store';
 import { ListResults } from '~/services/common-service/common-service';
 import { HelpIcon } from '~/components/icon/icon';
-import { Routes } from '~/routes/routes';
 
 type CssRules = 'button' | 'codeSnippet' | 'link' | 'linkIcon' | 'rightAlign' | 'cardWithoutMachines' | 'icon';
 
@@ -61,6 +58,8 @@ const mapStateToProps = (state: RootState) => {
     return {
         requestedDate: state.virtualMachines.date,
         userUuid: state.auth.user!.uuid,
+        helpText: state.auth.config.clusterConfig.Workbench.SSHHelpPageHTML,
+        webShell: state.auth.config.clusterConfig.Services.WebShell.ExternalURL,
         ...state.virtualMachines
     };
 };
@@ -75,6 +74,8 @@ interface VirtualMachinesPanelDataProps {
     virtualMachines: ListResults<any>;
     userUuid: string;
     links: ListResults<any>;
+    helpText: string;
+    webShell: string;
 }
 
 interface VirtualMachinesPanelActionProps {
@@ -161,7 +162,7 @@ const virtualMachinesTable = (props: VirtualMachineProps) =>
                 <TableCell>Host name</TableCell>
                 <TableCell>Login name</TableCell>
                 <TableCell>Command line</TableCell>
-                <TableCell>Web shell</TableCell>
+                {props.webShell !== "" && <TableCell>Web shell</TableCell>}
             </TableRow>
         </TableHead>
         <TableBody>
@@ -169,12 +170,12 @@ const virtualMachinesTable = (props: VirtualMachineProps) =>
                 <TableRow key={index}>
                     <TableCell>{it.hostname}</TableCell>
                     <TableCell>{getUsername(props.links, props.userUuid)}</TableCell>
-                    <TableCell>ssh {getUsername(props.links, props.userUuid)}@{it.hostname}.arvados</TableCell>
-                    <TableCell>
-                        <a href={`https://workbench.c97qk.arvadosapi.com${it.href}/webshell/${getUsername(props.links, props.userUuid)}`} target="_blank" className={props.classes.link}>
+                    <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>
+                    </TableCell>}
                 </TableRow>
             )}
         </TableBody>
@@ -188,17 +189,9 @@ const CardSSHSection = (props: VirtualMachineProps) =>
     <Grid item xs={12}>
         <Card>
             <CardContent>
-                <Typography variant='body1'>
-                    In order to access virtual machines using SSH, <Link to={Routes.SSH_KEYS_USER} className={props.classes.link}>add an SSH key to your account</Link> and add a section like this to your SSH configuration file ( ~/.ssh/config):
+                <Typography>
+                    <div dangerouslySetInnerHTML={{ __html: props.helpText }} style={{ margin: "1em" }} />
                 </Typography>
-                <DefaultCodeSnippet
-                    className={props.classes.codeSnippet}
-                    lines={[textSSH]} />
             </CardContent>
         </Card>
     </Grid>;
-
-const textSSH = `Host *.arvados
-    TCPKeepAlive yes
-    ServerAliveInterval 60
-    ProxyCommand ssh -p2222 turnout@switchyard.api.ardev.roche.com -x -a $SSH_PROXY_FLAGS %h`;
\ No newline at end of file