},
WebDAVDownload: {
ExternalURL: string
+ },
+ WebShell: {
+ ExternalURL: string
}
};
Workbench: {
FileViewersConfigURL: string;
WelcomePageHTML: string;
InactivePageHTML: string;
+ SSHHelpPageHTML: string;
SiteName: string;
};
Login: {
Websocket: { ExternalURL: "" },
WebDAV: { ExternalURL: "" },
WebDAVDownload: { ExternalURL: "" },
+ WebShell: { ExternalURL: "" },
},
Workbench: {
ArvadosDocsite: "",
FileViewersConfigURL: "",
WelcomePageHTML: "",
InactivePageHTML: "",
+ SSHHelpPageHTML: "",
SiteName: "",
},
Login: {
}
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;
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) {
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")
}
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);
}
}
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 }) =>
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> {
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;
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"]);
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);
constructor(
private linkService: LinkService,
private groupsService: GroupsService,
- ) {}
+ ) { }
create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
return this.linkService.create({
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()
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();
.list({
filters: new FilterBuilder()
.addIn("head_uuid", resourceUuids)
- .addEqual("tail_uuid", userUuid)
+ .addEqual("owner_uuid", userUuid)
.addEqual("link_class", LinkClass.STAR)
.getFilters()
})
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);
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));
}
};
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));
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;
SHARED_WITH_ME = 'Shared with me',
PUBLIC_FAVORITES = 'Public Favorites',
WORKFLOWS = 'Workflows',
- FAVORITES = 'Favorites',
+ FAVORITES = 'My Favorites',
TRASH = 'Trash',
ALL_PROCESSES = 'All Processes'
}
};
const SIDE_PANEL_CATEGORIES = [
- SidePanelTreeCategory.ALL_PROCESSES,
SidePanelTreeCategory.PUBLIC_FAVORITES,
- SidePanelTreeCategory.WORKFLOWS,
SidePanelTreeCategory.FAVORITES,
+ SidePanelTreeCategory.WORKFLOWS,
+ SidePanelTreeCategory.ALL_PROCESSES,
SidePanelTreeCategory.TRASH,
];
fb => fb
.addEqual('link_class', LinkClass.STAR)
.addEqual('owner_uuid', uuid)
- .addLike('name', '')
.getFilters(),
)(new FilterBuilder());
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();
}
};
+
+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;
export const SITE_MANAGER_REMOTE_HOST_VALIDATION = [require, isRemoteHost, maxLength(255)];
export const MY_ACCOUNT_VALIDATION = [require];
+
+export const CHOOSE_VM_VALIDATION = [require];
// 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",
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));
}
-}]];
+} */
+]];
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));
}
})
)(
<Field
name='virtualMachine'
component={NativeSelectField}
- validate={USER_LENGTH_VALIDATION}
+ validate={CHOOSE_VM_VALIDATION}
items={getVirtualMachinesList(data.items)} />
</div>;
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>;
<UserVirtualMachineField data={props.data as DataProps} />
<UserGroupsVirtualMachineField />
</>;
-
-
-
});
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/",
<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>
)
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';
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
};
};
virtualMachines: ListResults<any>;
userUuid: string;
links: ListResults<any>;
+ helpText: string;
+ webShell: string;
}
interface VirtualMachinesPanelActionProps {
<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>
<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>
<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