const rocheBlue = '#06C';
export const themeOptions: ArvadosThemeOptions = {
+ typography: {
+ useNextVariants: true,
+ },
customs: {
colors: {
green700: green["700"],
dark: teal.A400,
contrastText: '#fff'
}
- }
+ },
};
export const CustomTheme = createMuiTheme(themeOptions);
\ No newline at end of file
onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>) => void;
onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+ onFileClick: (id: string) => void;
}
type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader' | 'uploadIcon' | 'button';
classes={{ action: classes.button }}
action={
<Button onClick={onUploadDataClick}
- variant='raised'
+ variant='contained'
color='primary'
size='small'>
<DownloadIcon className={classes.uploadIcon} />
</DialogContent>
<DialogActions style={{ margin: '0px 24px 24px' }}>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
{props.data.cancelButtonLabel || 'Cancel'}
<CardActions>
<Button
color="primary"
- variant="raised"
+ variant='contained'
size="small"
onClick={this.submit}>
Ok
<CardActions>
<Button
color="primary"
- variant="raised"
+ variant='contained'
size="small"
onClick={this.submit}>
Ok
}
},
typography: {
- fontFamily: 'monospace'
+ fontFamily: 'monospace',
+ useNextVariants: true,
}
});
<Typography className={classnames([classes.root, classRoot])} component="div">
<Icon className={classnames([classes.icon, classIcon])} />
{messages.map((msg: string, index: number) => {
- return <Typography key={index} variant="body1"
+ return <Typography key={index}
className={classnames([classes.message, classMessage])}>{msg}</Typography>;
})}
</Typography>
return <>
<div className={classes.root}>
<ListItemTextIcon
- icon={getIcon(item)}
+ icon={getIcon(item.data.type)}
name={item.data.name} />
<div className={classes.spacer} />
<Typography
</IconButton>
</Tooltip>
</div >
- <FileThumbnail file={item.data} />
</>;
}
}
});
-const getIcon = (item: TreeItem<FileTreeData>) => {
- switch (item.data.type) {
+export const getIcon = (type: string) => {
+ switch (type) {
case 'directory':
return ProjectIcon;
case 'file':
onMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+ onFileClick: (id: string) => void;
}
export class FileTree extends React.Component<FileTreeProps> {
this.props.onCollapseToggle(id, status);
}
- handleToggleActive = () => { return; };
+ handleToggleActive = (_: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
+ this.props.onFileClick(item.id);
+ }
handleSelectionChange = (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
event.stopPropagation();
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
disabled={props.uploading}
onClick={props.closeDialog}>
{files.length === 0 &&
<Grid container justify="center" alignItems="center" className={classes.container}>
<Grid item component={"span"}>
- <Typography variant={"subheading"}>
+ <Typography variant='subtitle1'>
<CloudUploadIcon className={classes.uploadIcon} /> Drag and drop data or click to browse
</Typography>
</Grid>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WrappedFieldProps, WrappedFieldInputProps } from 'redux-form';
+import { FormGroup, FormLabel, FormHelperText } from '@material-ui/core';
+
+interface FormFieldCustomProps {
+ children: <P>(props: WrappedFieldInputProps) => React.ReactElement<P>;
+ label?: string;
+ helperText?: string;
+ required?: boolean;
+}
+
+export type FormFieldProps = FormFieldCustomProps & WrappedFieldProps;
+
+export const FormField = ({ children, ...props }: FormFieldProps & WrappedFieldProps) => {
+ return (
+ <FormGroup>
+
+ <FormLabel
+ focused={props.meta.active}
+ required={props.required}
+ error={props.meta.touched && !!props.meta.error}>
+ {props.label}
+ </FormLabel>
+
+ { children(props.input) }
+
+ <FormHelperText error={props.meta.touched && !!props.meta.error}>
+ {
+ props.meta.touched && props.meta.error
+ ? props.meta.error
+ : props.helperText
+ }
+ </FormHelperText>
+
+ </FormGroup>
+ );
+};
<Icon style={{ fontSize: `${iconSize}rem` }} />
</ListItemIcon>
<ListItemText primary={
- <Typography variant='body1' className={classnames(classes.listItemText, {
+ <Typography className={classnames(classes.listItemText, {
[classes.active]: isActive
})}>
{name}
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
disabled={props.submitting}
onClick={props.closeDialog}>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { FormFieldProps, FormField } from '~/components/form-field/form-field';
+import { Switch } from '@material-ui/core';
+import { SwitchProps } from '@material-ui/core/Switch';
+
+export const SwitchField = ({ switchProps, ...props }: FormFieldProps & { switchProps: SwitchProps }) =>
+ <FormField {...props}>
+ {input => <Switch {...switchProps} checked={input.value} onChange={input.onChange} />}
+ </FormField>;
+
type TextFieldProps = WrappedFieldProps & WithStyles<CssRules>;
export const TextField = withStyles(styles)((props: TextFieldProps & {
- label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, disabled?: boolean, children: React.ReactNode, margin?: Margin, placeholder?: string
+ label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, disabled?: boolean, children: React.ReactNode, margin?: Margin, placeholder?: string,
+ helperText?: string, type?: string,
}) =>
<MaterialTextField
- helperText={props.meta.touched && props.meta.error}
+ helperText={(props.meta.touched && props.meta.error) || props.helperText}
className={props.classes.textField}
label={props.label}
disabled={props.disabled || props.meta.submitting}
children={props.children}
margin={props.margin}
placeholder={props.placeholder}
+ type={props.type}
{...props.input}
/>);
import { CollectionResource } from "./collection";
import { ProcessResource } from "./process";
import { EmptyResource } from "./empty";
+import { CollectionFile, CollectionDirectory } from '~/models/collection-file';
-export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource;
+export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory;
// SPDX-License-Identifier: AGPL-3.0
export interface SchedulingParameters {
- partitions: string[];
- preemptible: boolean;
- maxRunTime: number;
+ partitions?: string[];
+ preemptible?: boolean;
+ maxRunTime?: number;
}
// SPDX-License-Identifier: AGPL-3.0
import { History, Location } from 'history';
+import { match } from 'react-router-dom';
+import { Dispatch } from 'redux';
+import { ThunkAction } from 'redux-thunk';
import { RootStore } from '~/store/store';
-import * as Routes from '~/routes/routes';
-import * as WorkbenchActions from '~/store/workbench/workbench-actions';
+import * as R from '~/routes/routes';
+import * as WA from '~/store/workbench/workbench-actions';
import { navigateToRootProject } from '~/store/navigation/navigation-action';
import { dialogActions } from '~/store/dialog/dialog-actions';
import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
};
const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
- const rootMatch = Routes.matchRootRoute(pathname);
- const projectMatch = Routes.matchProjectRoute(pathname);
- const collectionMatch = Routes.matchCollectionRoute(pathname);
- const favoriteMatch = Routes.matchFavoritesRoute(pathname);
- const trashMatch = Routes.matchTrashRoute(pathname);
- const processMatch = Routes.matchProcessRoute(pathname);
- const processLogMatch = Routes.matchProcessLogRoute(pathname);
- const repositoryMatch = Routes.matchRepositoriesRoute(pathname);
- const searchResultsMatch = Routes.matchSearchResultsRoute(pathname);
- const sharedWithMeMatch = Routes.matchSharedWithMeRoute(pathname);
- const runProcessMatch = Routes.matchRunProcessRoute(pathname);
- const virtualMachineUserMatch = Routes.matchUserVirtualMachineRoute(pathname);
- const virtualMachineAdminMatch = Routes.matchAdminVirtualMachineRoute(pathname);
- const workflowMatch = Routes.matchWorkflowRoute(pathname);
- const sshKeysUserMatch = Routes.matchSshKeysUserRoute(pathname);
- const sshKeysAdminMatch = Routes.matchSshKeysAdminRoute(pathname);
- const siteManagerMatch = Routes.matchSiteManagerRoute(pathname);
- const keepServicesMatch = Routes.matchKeepServicesRoute(pathname);
- const computeNodesMatch = Routes.matchComputeNodesRoute(pathname);
- const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname);
- const myAccountMatch = Routes.matchMyAccountRoute(pathname);
- const userMatch = Routes.matchUsersRoute(pathname);
- const groupsMatch = Routes.matchGroupsRoute(pathname);
- const groupDetailsMatch = Routes.matchGroupDetailsRoute(pathname);
- const linksMatch = Routes.matchLinksRoute(pathname);
store.dispatch(dialogActions.CLOSE_ALL_DIALOGS());
store.dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
store.dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
- if (projectMatch) {
- store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id));
- } else if (collectionMatch) {
- store.dispatch(WorkbenchActions.loadCollection(collectionMatch.params.id));
- } else if (favoriteMatch) {
- store.dispatch(WorkbenchActions.loadFavorites());
- } else if (trashMatch) {
- store.dispatch(WorkbenchActions.loadTrash());
- } else if (processMatch) {
- store.dispatch(WorkbenchActions.loadProcess(processMatch.params.id));
- } else if (processLogMatch) {
- store.dispatch(WorkbenchActions.loadProcessLog(processLogMatch.params.id));
- } else if (rootMatch) {
- store.dispatch(navigateToRootProject);
- } else if (sharedWithMeMatch) {
- store.dispatch(WorkbenchActions.loadSharedWithMe);
- } else if (runProcessMatch) {
- store.dispatch(WorkbenchActions.loadRunProcess);
- } else if (workflowMatch) {
- store.dispatch(WorkbenchActions.loadWorkflow);
- } else if (searchResultsMatch) {
- store.dispatch(WorkbenchActions.loadSearchResults);
- } else if (virtualMachineUserMatch) {
- store.dispatch(WorkbenchActions.loadVirtualMachines);
- } else if (virtualMachineAdminMatch) {
- store.dispatch(WorkbenchActions.loadVirtualMachines);
- } else if (repositoryMatch) {
- store.dispatch(WorkbenchActions.loadRepositories);
- } else if (sshKeysUserMatch) {
- store.dispatch(WorkbenchActions.loadSshKeys);
- } else if (sshKeysAdminMatch) {
- store.dispatch(WorkbenchActions.loadSshKeys);
- } else if (siteManagerMatch) {
- store.dispatch(WorkbenchActions.loadSiteManager);
- } else if (keepServicesMatch) {
- store.dispatch(WorkbenchActions.loadKeepServices);
- } else if (computeNodesMatch) {
- store.dispatch(WorkbenchActions.loadComputeNodes);
- } else if (apiClientAuthorizationsMatch) {
- store.dispatch(WorkbenchActions.loadApiClientAuthorizations);
- } else if (myAccountMatch) {
- store.dispatch(WorkbenchActions.loadMyAccount);
- } else if (userMatch) {
- store.dispatch(WorkbenchActions.loadUsers);
- } else if (groupsMatch) {
- store.dispatch(WorkbenchActions.loadGroupsPanel);
- } else if (groupDetailsMatch) {
- store.dispatch(WorkbenchActions.loadGroupDetailsPanel(groupDetailsMatch.params.id));
- } else if (linksMatch) {
- store.dispatch(WorkbenchActions.loadLinks);
- }
+ locationChangeHandlers.find(handler => handler(store.dispatch, pathname));
+
};
+
+type MatchRoute<Params> = (route: string) => match<Params> | null;
+type ActionCreator<Params> = (params: Params) => ThunkAction<any, any, any, any>;
+
+const handle = <Params>(matchRoute: MatchRoute<Params>, actionCreator: ActionCreator<Params>) =>
+ (dispatch: Dispatch, route: string) => {
+ const match = matchRoute(route);
+ return match
+ ? (
+ dispatch<any>(actionCreator(match.params)),
+ true
+ )
+ : false;
+ };
+
+const locationChangeHandlers = [
+
+ handle(
+ R.matchApiClientAuthorizationsRoute,
+ () => WA.loadApiClientAuthorizations
+ ),
+
+ handle(
+ R.matchCollectionRoute,
+ ({ id }) => WA.loadCollection(id)
+ ),
+
+ handle(
+ R.matchComputeNodesRoute,
+ () => WA.loadComputeNodes
+ ),
+
+ handle(
+ R.matchFavoritesRoute,
+ () => WA.loadFavorites
+ ),
+
+ handle(
+ R.matchGroupDetailsRoute,
+ ({ id }) => WA.loadGroupDetailsPanel(id)
+ ),
+
+ handle(
+ R.matchGroupsRoute,
+ () => WA.loadGroupsPanel
+ ),
+
+ handle(
+ R.matchKeepServicesRoute,
+ () => WA.loadKeepServices
+ ),
+
+ handle(
+ R.matchLinksRoute,
+ () => WA.loadLinks
+ ),
+
+ handle(
+ R.matchMyAccountRoute,
+ () => WA.loadMyAccount
+ ),
+
+ handle(
+ R.matchProcessLogRoute,
+ ({ id }) => WA.loadProcessLog(id)
+ ),
+
+ handle(
+ R.matchProcessRoute,
+ ({ id }) => WA.loadProcess(id)
+ ),
+
+ handle(
+ R.matchProjectRoute,
+ ({ id }) => WA.loadProject(id)
+ ),
+
+ handle(
+ R.matchRepositoriesRoute,
+ () => WA.loadRepositories
+ ),
+
+ handle(
+ R.matchRootRoute,
+ () => navigateToRootProject
+ ),
+
+ handle(
+ R.matchRunProcessRoute,
+ () => WA.loadRunProcess
+ ),
+
+ handle(
+ R.matchSearchResultsRoute,
+ () => WA.loadSearchResults
+ ),
+
+ handle(
+ R.matchSharedWithMeRoute,
+ () => WA.loadSharedWithMe
+ ),
+
+ handle(
+ R.matchSiteManagerRoute,
+ () => WA.loadSiteManager
+ ),
+
+ handle(
+ R.matchSshKeysAdminRoute,
+ () => WA.loadSshKeys
+ ),
+
+ handle(
+ R.matchSshKeysUserRoute,
+ () => WA.loadSshKeys
+ ),
+
+ handle(
+ R.matchTrashRoute,
+ () => WA.loadTrash
+ ),
+
+ handle(
+ R.matchUsersRoute,
+ () => WA.loadUsers
+ ),
+
+ handle(
+ R.matchAdminVirtualMachineRoute,
+ () => WA.loadVirtualMachines
+ ),
+
+ handle(
+ R.matchUserVirtualMachineRoute,
+ () => WA.loadVirtualMachines
+ ),
+
+ handle(
+ R.matchWorkflowRoute,
+ () => WA.loadWorkflow
+ ),
+
+];
+
export const detailsPanelActions = unionize({
TOGGLE_DETAILS_PANEL: ofType<{}>(),
+ OPEN_DETAILS_PANEL: ofType<string>(),
LOAD_DETAILS_PANEL: ofType<string>()
});
export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+export const openDetailsPanel = (uuid: string) => detailsPanelActions.OPEN_DETAILS_PANEL(uuid);
+
export const openProjectPropertiesDialog = () =>
(dispatch: Dispatch) => {
dispatch<any>(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } }));
detailsPanelActions.match(action, {
default: () => state,
LOAD_DETAILS_PANEL: resourceUuid => ({ ...state, resourceUuid }),
- TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
+ OPEN_DETAILS_PANEL: resourceUuid => ({ resourceUuid, isOpened: true }),
+ TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened }),
});
import { createWorkflowMounts } from '~/models/process';
import { ContainerRequestState } from '~/models/container-request';
import { navigateToProcess } from '../navigation/navigation-action';
-import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from '~/views/run-process-panel/run-process-advanced-form';
+import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM, VCPUS_FIELD, RAM_FIELD, RUNTIME_FIELD, OUTPUT_FIELD, API_FIELD } from '~/views/run-process-panel/run-process-advanced-form';
import { isItemNotInProject, isProjectOrRunProcessRoute } from '~/store/projects/project-create-actions';
import { dialogActions } from '~/store/dialog/dialog-actions';
import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
dispatch(runProcessPanelActions.SET_STEP_CHANGED(false));
dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
dispatch<any>(loadPresets(workflow.uuid));
+ dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, DEFAULT_ADVANCED_FORM_VALUES));
}
if (!isWorkflowChanged) {
dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
dispatch<any>(loadPresets(workflow.uuid));
+ dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, DEFAULT_ADVANCED_FORM_VALUES));
}
};
const state = getState();
const basicForm = getFormValues(RUN_PROCESS_BASIC_FORM)(state) as RunProcessBasicFormData;
const inputsForm = getFormValues(RUN_PROCESS_INPUTS_FORM)(state) as WorkflowInputsData;
- const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData;
+ const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData || DEFAULT_ADVANCED_FORM_VALUES;
const userUuid = getState().auth.user!.uuid;
const router = getState();
const properties = getState().properties;
mounts: createWorkflowMounts(selectedWorkflow, normalizeInputKeys(inputsForm)),
runtimeConstraints: {
API: true,
- vcpus: 1,
- ram: 1073741824,
+ vcpus: advancedForm[VCPUS_FIELD],
+ ram: advancedForm[RAM_FIELD],
+ api: advancedForm[API_FIELD],
+ },
+ schedulingParameters: {
+ maxRunTime: advancedForm[RUNTIME_FIELD]
},
containerImage: 'arvados/jobs',
cwd: '/var/spool/cwl',
command: [
'arvados-cwl-runner',
- '--local',
'--api=containers',
- `--project-uuid=${processOwnerUuid}`,
'/var/lib/cwl/workflow.json#main',
'/var/lib/cwl/cwl.input.json'
],
outputPath: '/var/spool/cwl',
priority: 1,
- outputName: advancedForm && advancedForm.output ? advancedForm.output : undefined,
+ outputName: advancedForm[OUTPUT_FIELD] ? advancedForm[OUTPUT_FIELD] : undefined,
};
const newProcess = await services.containerRequestService.create(newProcessData);
dispatch(navigateToProcess(newProcess.uuid));
}
};
+export const DEFAULT_ADVANCED_FORM_VALUES: Partial<RunProcessAdvancedFormData> = {
+ [VCPUS_FIELD]: 1,
+ [RAM_FIELD]: 1073741824,
+ [API_FIELD]: true,
+};
+
const normalizeInputKeys = (inputs: WorkflowInputsData): WorkflowInputsData =>
Object.keys(inputs).reduce((normalizedInputs, key) => ({
...normalizedInputs,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { isNumber } from 'lodash';
+
+export const ERROR_MESSAGE = (minValue: number) => `Minimum value is ${minValue}`;
+
+export const min =
+ (minValue: number, errorMessage = ERROR_MESSAGE) =>
+ (value: any) =>
+ isNumber(value) && value >= minValue ? undefined : errorMessage(minValue);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const optional = (validator: (value: any) => string | undefined) =>
+ (value: any) =>
+ value === undefined || value === null || value === '' ? undefined : validator(value);
\ No newline at end of file
{value === 4 && dialogContent(curlHeader, curlExample, classes)}
</DialogContent>
<DialogActions>
- <Button variant='flat' color='primary' onClick={closeDialog}>
+ <Button variant='text' color='primary' onClick={closeDialog}>
Close
</Button>
</DialogActions>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
import { openContextMenu, openCollectionFilesContextMenu } from '~/store/context-menu/context-menu-actions';
import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions';
import { ResourceKind } from "~/models/resource";
+import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
const memoizedMapStateToProps = () => {
let prevState: CollectionPanelFilesState;
};
};
-const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
+const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onFileClick' | 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
onUploadDataClick: () => {
dispatch<any>(openUploadCollectionFilesDialog());
},
onOptionsMenuOpen: (event) => {
dispatch<any>(openCollectionFilesContextMenu(event));
},
+ onFileClick: (id) => {
+ dispatch(openDetailsPanel(id));
+ },
});
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
maxWidth='md'>
<DialogTitle>Current Token</DialogTitle>
<DialogContent>
- <Typography variant='body1' paragraph={true}>
+ <Typography paragraph={true}>
The Arvados API token is a secret key that enables the Arvados SDKs to access Arvados with the proper permissions.
<Typography component='p'>
For more information see
</a>
</Typography>
</Typography>
- <Typography variant='body1' paragraph={true}>
+ <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.
</Typography>
<DefaultCodeSnippet lines={[getSnippet(data)]} />
- <Typography variant='body1'>
+ <Typography >
Arvados
<a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>
do this for you automatically. This setup is needed only when you use the API remotely (e.g., from your own workstation).
import { ResourceData } from "~/store/resources-data/resources-data-reducer";
import { getResourceData } from "~/store/resources-data/resources-data";
import { toggleDetailsPanel, SLIDE_TIMEOUT } from '~/store/details-panel/details-panel-action';
+import { FileDetails } from '~/views-components/details-panel/file-details';
+import { getNode } from '~/models/tree';
type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
},
});
-const getItem = (resource: DetailsResource, resourceData?: ResourceData): DetailsData => {
- const res = resource || { kind: undefined, name: 'Projects' };
- switch (res.kind) {
- case ResourceKind.PROJECT:
- return new ProjectDetails(res);
- case ResourceKind.COLLECTION:
- return new CollectionDetails(res, resourceData);
- case ResourceKind.PROCESS:
- return new ProcessDetails(res);
- default:
- return new EmptyDetails(res as EmptyResource);
+const EMPTY_RESOURCE: EmptyResource = { kind: undefined, name: 'Projects' };
+
+const getItem = (res: DetailsResource, resourceData?: ResourceData): DetailsData => {
+ if ('kind' in res) {
+ switch (res.kind) {
+ case ResourceKind.PROJECT:
+ return new ProjectDetails(res);
+ case ResourceKind.COLLECTION:
+ return new CollectionDetails(res, resourceData);
+ case ResourceKind.PROCESS:
+ return new ProcessDetails(res);
+ default:
+ return new EmptyDetails(res);
+ }
+ } else {
+ return new FileDetails(res);
}
};
-const mapStateToProps = ({ detailsPanel, resources, resourcesData }: RootState) => {
- const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource;
+const mapStateToProps = ({ detailsPanel, resources, resourcesData, collectionPanelFiles }: RootState) => {
+ const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource | undefined;
+ const file = getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
const resourceData = getResourceData(detailsPanel.resourceUuid)(resourcesData);
return {
isOpened: detailsPanel.isOpened,
- item: getItem(resource, resourceData)
+ item: getItem(resource || (file && file.value) || EMPTY_RESOURCE, resourceData)
};
};
</Grid>
<Grid item xs={8}>
<Tooltip title={item.getTitle()}>
- <Typography variant="title" noWrap>
+ <Typography variant='h6' noWrap>
{item.getTitle()}
</Typography>
</Tooltip>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { DetailsData } from "./details-data";
+import { CollectionFile, CollectionDirectory, CollectionFileType } from '~/models/collection-file';
+import { getIcon } from '~/components/file-tree/file-tree-item';
+import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
+import { formatFileSize } from '~/common/formatters';
+import { FileThumbnail } from '~/components/file-tree/file-thumbnail';
+import isImage from 'is-image';
+
+export class FileDetails extends DetailsData<CollectionFile | CollectionDirectory> {
+
+ getIcon(className?: string) {
+ const Icon = getIcon(this.item.type);
+ return <Icon className={className} />;
+ }
+
+ getDetails() {
+ const { item } = this;
+ return item.type === CollectionFileType.FILE
+ ? <>
+ <DetailsAttribute label='Size' value={formatFileSize(item.size)} />
+ {
+ isImage(item.url) && <>
+ <DetailsAttribute label='Preview' />
+ <FileThumbnail file={item} />
+ </>
+ }
+ </>
+ : <div />;
+ }
+}
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data && attributes(props.data, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data && attributes(props.data, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
<MenuItem key={link.title}>
<a href={link.link} target="_blank" className={classes.link}>
<ImportContactsIcon className={classes.icon} />
- <Typography variant="body1" className={classes.linkTitle}>{link.title}</Typography>
+ <Typography className={classes.linkTitle}>{link.title}</Typography>
</a>
</MenuItem>
)
<Toolbar className={props.classes.toolbar}>
<Grid container justify="space-between">
<Grid container item xs={3} direction="column" justify="center">
- <Typography variant="title" color="inherit" noWrap>
+ <Typography variant='h6' color="inherit" noWrap>
<Link to={Routes.ROOT} className={props.classes.link}>
arvados workbench
</Link>
lines={[props.data.command]} />
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Cancel
</Button>
- <Button variant='raised' color='primary'>
+ <Button variant='contained' color='primary'>
Remove
</Button>
</DialogActions>
<DefaultCodeSnippet
className={props.classes.codeSnippet}
lines={[snippetText(props.data.uuidPrefix)]} />
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
See also:
<div><a href="https://doc.arvados.org/user/getting_started/ssh-access-unix.html" className={props.classes.link} target="_blank">SSH access</a></div>
<div><a href="https://doc.arvados.org/user/tutorials/tutorial-firstscript.html" className={props.classes.link} target="_blank">Writing a Crunch Script</a></div>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data.repositoryData && attributes(props.data.repositoryData, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
{props.advancedViewOpen &&
<>
<Grid item>
- <Typography variant='subheading'>
+ <Typography variant='subtitle1'>
Who can access
</Typography>
<SharingPublicAccessForm />
<Divider />
<Grid container alignItems='center' spacing={8} wrap='nowrap' className={classes.root}>
<Grid item xs={8}>
- <Typography noWrap variant='subheading'>{fields.get(index).email}</Typography>
+ <Typography noWrap variant='subtitle1'>{fields.get(index).email}</Typography>
</Grid>
<Grid item xs={4} container wrap='nowrap'>
<Field
<Divider />
<Grid container alignItems='center' spacing={8} className={classes.root}>
<Grid item xs={8}>
- <Typography variant='subheading'>
+ <Typography variant='subtitle1'>
{renderVisibilityInfo(visibility)}
</Typography>
</Grid>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={closeDialog}>
Close
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data && attributes(props.data, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
<span>
<DialogTitle>{`Manage - ${props.data.user.firstName} ${props.data.user.lastName}`}</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
</Typography>
<Button variant="contained" color="primary" onClick={() => props.loginAs(props.data.client.uuid)}>
{`LOG IN AS ${props.data.user.firstName} ${props.data.user.lastName}`}
</Button>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
As an admin, you can setup a shell account for this user. The login name is automatically generated from the user's e-mail address.
</Typography>
<Button variant="contained" color="primary" onClick={() => props.openSetupShellAccount(props.data.uuid)}>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
maxWidth="sm">
<DialogTitle>Attributes</DialogTitle>
<DialogContent>
- <Typography variant="body2" className={props.classes.spacing}>
+ <Typography variant='body1' className={props.classes.spacing}>
{props.data.virtualMachineData && attributes(props.data.virtualMachineData, props.classes)}
</Typography>
</DialogContent>
<DialogActions>
<Button
- variant='flat'
+ variant='text'
color='primary'
onClick={props.closeDialog}>
Close
import { formatFileSize } from "~/common/formatters";
import { getResourceData } from "~/store/resources-data/resources-data";
import { ResourceData } from "~/store/resources-data/resources-data-reducer";
+import { openDetailsPanel } from '~/store/details-panel/details-panel-action';
type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value' | 'link';
return { item, data };
})(
class extends React.Component<CollectionPanelProps> {
+
render() {
const { classes, item, data, dispatch } = this.props;
return item
? <>
<Card className={classes.card}>
<CardHeader
- avatar={<CollectionIcon className={classes.iconHeader} />}
+ avatar={
+ <IconButton onClick={this.openCollectionDetails}>
+ <CollectionIcon className={classes.iconHeader} />
+ </IconButton>
+ }
action={
<Tooltip title="More options" disableFocusListener>
<IconButton
</Tooltip>
}
title={item && item.name}
- subheader={item && item.description} />
+ titleTypographyProps={this.titleProps}
+ subheader={item && item.description}
+ subheaderTypographyProps={this.titleProps} />
<CardContent>
<Grid container direction="column">
<Grid item xs={6}>
label='Content size' value={data && formatFileSize(data.fileSize)} />
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
label='Owner' value={item && item.ownerUuid} />
- <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
+ <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
<DetailsAttribute classLabel={classes.link} label='Link to process' />
</span>
</Grid>
hideDuration: 2000
}));
}
+
+ openCollectionDetails = () => {
+ const { item } = this.props;
+ if (item) {
+ this.props.dispatch(openDetailsPanel(item.uuid));
+ }
+ }
+
+ titleProps = {
+ onClick: this.openCollectionDetails
+ };
+
}
)
);
({ classes, dispatch }: LoginPanelProps) =>
<Grid container direction="column" item xs alignItems="center" justify="center" className={classes.root}>
<Grid item className={classes.container}>
- <Typography variant="title" align="center" className={classes.title}>
+ <Typography variant='h6' align="center" className={classes.title}>
Welcome to the Arvados Workbench
</Typography>
- <Typography variant="body1" className={classes.content}>
+ <Typography className={classes.content}>
The "Log in" button below will show you a Google sign-in page.
After you assure Google that you want to log in here with your Google account, you will be redirected back here to Arvados Workbench.
</Typography>
- <Typography variant="body1" className={classes.content}>
+ <Typography className={classes.content}>
If you have never used Arvados Workbench before, logging in for the first time will automatically create a new account.
</Typography>
- <Typography variant="body2" className={classNames(classes.content, classes.content__bolder)}>
+ <Typography variant='body1' className={classNames(classes.content, classes.content__bolder)}>
IMPORTANT: Please keep in mind to store exploratory data only but not any information used for clinical decision making.
</Typography>
- <Typography variant="body1" className={classes.content}>
+ <Typography className={classes.content}>
Arvados Workbench uses your name and email address only for identification, and does not retrieve any other personal information from Google.
</Typography>
<Typography component="div" align="right">
({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting }: MyAccountPanelRootProps) => {
return <Card className={classes.root}>
<CardContent>
- <Typography variant="title" className={classes.title}>User profile</Typography>
+ <Typography variant='h6' className={classes.title}>User profile</Typography>
<form onSubmit={handleSubmit}>
<Grid container direction="row" spacing={24}>
<Grid item xs={6}>
}
},
typography: {
- fontFamily: 'monospace'
+ fontFamily: 'monospace',
+ useNextVariants: true,
}
});
</Tooltip>}
title={
<Tooltip title={process.containerRequest.name} placement="bottom-start">
- <Typography noWrap variant="title" className={classes.title}>
+ <Typography noWrap variant='h6' className={classes.title}>
{process.containerRequest.name}
</Typography>
</Tooltip>}
}
title={
<Tooltip title={process.containerRequest.name} placement="bottom-start">
- <Typography noWrap variant="title" color='inherit'>
+ <Typography noWrap variant='h6' color='inherit'>
{process.containerRequest.name}
</Typography>
</Tooltip>
}
subheader={
<Tooltip title={getDescription(process)} placement="bottom-start">
- <Typography noWrap variant="body2" color='inherit'>
+ <Typography noWrap variant='body1' color='inherit'>
{getDescription(process)}
</Typography>
</Tooltip>}/>
classes={{ content: classes.title, action: classes.action }}
action={
<div className={classes.rightSideHeader}>
- <Typography noWrap variant="body2" className={classes.status}>
+ <Typography noWrap variant='body1' className={classes.status}>
{getProcessStatus(subprocess)}
</Typography>
<Tooltip title="More options" disableFocusListener>
className={classes.options}
aria-label="More options"
onClick={onContextMenu}>
- <MoreOptionsIcon className={classes.moreOptions}/>
+ <MoreOptionsIcon className={classes.moreOptions} />
</IconButton>
</Tooltip>
</div>
}
title={
<Tooltip title={subprocess.containerRequest.name}>
- <Typography noWrap variant="body2" className={classes.titleHeader}>
+ <Typography noWrap variant='body1' className={classes.titleHeader}>
{subprocess.containerRequest.name}
</Typography>
</Tooltip>
} />
<CardContent className={classes.content}>
<DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label="Runtime" value={formatTime(getProcessRuntime(subprocess))} />
+ label="Runtime" value={subprocess.container && subprocess.container.startedAt && subprocess.container.finishedAt
+ ? formatTime(getProcessRuntime(subprocess)) :
+ '(none)'} />
</CardContent>
</Card>;
});
<CardHeader
className={classes.title}
title={
- <Typography noWrap variant="title" color='inherit'>
+ <Typography noWrap variant='h6' color='inherit'>
Subprocess and filters
</Typography>} />
<CardContent>
<CardContent>
<Grid container direction="row">
<Grid item xs={8}>
- <Typography variant="body2">
+ <Typography variant='body1'>
When you are using an Arvados virtual machine, you should clone the https:// URLs. This will authenticate automatically using your API token. <br />
In order to clone git repositories using SSH, <Link to={Routes.SSH_KEYS_USER} className={classes.link}>add an SSH key to your account</Link> and clone the git@ URLs.
</Typography>
</div>
<Divider />
<div className={classes.chips}>
- <Typography variant='subheading'>Selected collections ({this.state.directories.length}):</Typography>
+ <Typography variant='subtitle1'>Selected collections ({this.state.directories.length}):</Typography>
<Chips
orderable
deletable
</div>
<Divider />
<div className={classes.chips}>
- <Typography variant='subheading'>Selected files ({this.state.files.length}):</Typography>
+ <Typography variant='subtitle1'>Selected files ({this.state.files.length}):</Typography>
<Chips
orderable
deletable
format={format}
validate={getValidation(input)} />;
-const parse = (value: any) => parseInt(value, 10);
+export const parse = (value: any) => value === '' ? '' : parseInt(value, 10);
-const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value);
+export const format = (value: any) => isNaN(value) ? '' : JSON.stringify(value);
const getValidation = memoize(
(input: IntCommandInputParameter) => ([
import { Grid } from '@material-ui/core';
import { TextField } from '~/components/text-field/text-field';
import { ExpandIcon } from '~/components/icon/icon';
+import * as IntInput from './inputs/int-input';
+import { require } from '~/validators/require';
+import { min } from '~/validators/min';
+import { optional } from '~/validators/optional';
+import { SwitchField } from '~/components/switch-field/switch-field';
export const RUN_PROCESS_ADVANCED_FORM = 'runProcessAdvancedForm';
+export const OUTPUT_FIELD = 'output';
+export const RUNTIME_FIELD = 'runtime';
+export const RAM_FIELD = 'ram';
+export const VCPUS_FIELD = 'vcpus';
+export const KEEP_CACHE_RAM_FIELD = 'keepCacheRam';
+export const API_FIELD = 'api';
+
export interface RunProcessAdvancedFormData {
- output: string;
- runtime: string;
+ [OUTPUT_FIELD]?: string;
+ [RUNTIME_FIELD]?: number;
+ [RAM_FIELD]: number;
+ [VCPUS_FIELD]: number;
+ [KEEP_CACHE_RAM_FIELD]?: number;
+ [API_FIELD]?: boolean;
}
export const RunProcessAdvancedForm =
reduxForm<RunProcessAdvancedFormData>({
- form: RUN_PROCESS_ADVANCED_FORM
+ form: RUN_PROCESS_ADVANCED_FORM,
})(() =>
<form>
<ExpansionPanel elevation={0}>
<Grid container spacing={32}>
<Grid item xs={12} md={6}>
<Field
- name='output'
+ name={OUTPUT_FIELD}
component={TextField}
label="Output name" />
</Grid>
<Grid item xs={12} md={6}>
<Field
- name='runtime'
+ name={RUNTIME_FIELD}
+ component={TextField}
+ helperText="Maximum running time (in seconds) that this container will be allowed to run before being cancelled."
+ label="Runtime limit"
+ parse={IntInput.parse}
+ format={IntInput.format}
+ type='number'
+ validate={runtimeValidation} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <Field
+ name={RAM_FIELD}
+ component={TextField}
+ label="RAM"
+ helperText="Number of ram bytes to be used to run this process."
+ parse={IntInput.parse}
+ format={IntInput.format}
+ type='number'
+ required
+ validate={ramValidation} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <Field
+ name={VCPUS_FIELD}
component={TextField}
- label="Runtime limit (hh)" />
+ label="VCPUs"
+ helperText="Number of cores to be used to run this process."
+ parse={IntInput.parse}
+ format={IntInput.format}
+ type='number'
+ required
+ validate={vcpusValidation} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <Field
+ name={KEEP_CACHE_RAM_FIELD}
+ component={TextField}
+ label="Keep cache RAM"
+ helperText="Number of keep cache bytes to be used to run this process."
+ parse={IntInput.parse}
+ format={IntInput.format}
+ type='number'
+ validate={keepCacheRamValdation} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <Field
+ name={API_FIELD}
+ component={SwitchField}
+ switchProps={{
+ color: 'primary'
+ }}
+ label='API'
+ helperText='When set, ARVADOS_API_HOST and ARVADOS_API_TOKEN will be set, and process will have networking enabled to access the Arvados API server.' />
</Grid>
</Grid>
</ExpansionPanelDetails>
</ExpansionPanel>
</form >);
+
+const ramValidation = [min(0)];
+const vcpusValidation = [min(1)];
+const keepCacheRamValdation = [optional(min(0))];
+const runtimeValidation = [optional(min(1))];
import { RootState } from '~/store/store';
import { isValid } from 'redux-form';
import { RUN_PROCESS_INPUTS_FORM } from './run-process-inputs-form';
-import { RunProcessAdvancedForm } from './run-process-advanced-form';
+import { RunProcessAdvancedForm, RUN_PROCESS_ADVANCED_FORM } from './run-process-advanced-form';
import { createSelector, createStructuredSelector } from 'reselect';
import { WorkflowPresetSelect } from '~/views/run-process-panel/workflow-preset-select';
import { selectPreset } from '~/store/run-process-panel/run-process-panel-actions';
state.runProcessPanel.inputs;
const validSelector = (state: RootState) =>
- isValid(RUN_PROCESS_BASIC_FORM)(state) && isValid(RUN_PROCESS_INPUTS_FORM)(state);
+ isValid(RUN_PROCESS_BASIC_FORM)(state) && isValid(RUN_PROCESS_INPUTS_FORM)(state) && isValid(RUN_PROCESS_ADVANCED_FORM)(state);
const mapStateToProps = createStructuredSelector({
inputs: inputsSelector,
<CardContent>
<Grid container direction="row">
<Grid item xs={12}>
- <Typography variant='body1' paragraph={true} >
+ <Typography paragraph={true} >
You can log in to multiple Arvados sites here, then use the multi-site search page to search collections and projects on all sites at once.
</Typography>
</Grid>
<form onSubmit={handleSubmit}>
<Grid container direction="row">
<Grid item xs={12}>
- <Typography variant='body1' paragraph={true} className={classes.remoteSiteInfo}>
+ <Typography paragraph={true} className={classes.remoteSiteInfo}>
To add a remote Arvados site, paste the remote site's host here (see "ARVADOS_API_HOST" on the "current token" page).
</Typography>
</Grid>
<CardContent>
<Grid container direction="row">
<Grid item xs={8}>
- { !hasKeys && <Typography variant='body1' paragraph={true} >
+ { !hasKeys && <Typography paragraph={true} >
You have not yet set up an SSH public key for use with Arvados.
<a href='https://doc.arvados.org/user/getting_started/ssh-access-unix.html'
target='blank' className={classes.link}>
Learn more.
</a>
</Typography>}
- { !hasKeys && <Typography variant='body1' paragraph={true}>
+ { !hasKeys && <Typography paragraph={true}>
When you have an SSH key you would like to use, add it using button below.
</Typography> }
</Grid>
<Card>
<CardContent className={props.classes.cardWithoutMachines}>
<Grid item xs={6}>
- <Typography variant="body2">
+ <Typography variant='body1'>
You do not have access to any virtual machines. Some Arvados features require using the command line. You may request access to a hosted virtual machine with the command line shell.
</Typography>
</Grid>
SEND REQUEST FOR SHELL ACCESS
</Button>
{props.requestedDate &&
- <Typography variant="body1">
+ <Typography >
A request for shell access was sent on {props.requestedDate}
</Typography>}
</span>;
<Grid item xs={12}>
<Card>
<CardContent>
- <Typography variant="body2">
+ <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>
<DefaultCodeSnippet
render() {
const { classes, workflow } = this.props;
- if (workflow) {
- console.log(workflow.definition);
- }
const { value } = this.state;
return <div className={classes.root}>
<Tabs value={value} onChange={this.handleChange} centered={true}>