return "Group";
case ResourceKind.VIRTUAL_MACHINE:
return "Virtual Machine";
+ case ResourceKind.WORKFLOW:
+ return "Workflow";
default:
return "Unknown";
}
import { loadFileViewersConfig } from 'store/file-viewers/file-viewers-actions';
import { filterGroupAdminActionSet, projectAdminActionSet } from 'views-components/context-menu/action-sets/project-admin-action-set';
import { permissionEditActionSet } from 'views-components/context-menu/action-sets/permission-edit-action-set';
+import { workflowActionSet } from 'views-components/context-menu/action-sets/workflow-action-set';
import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action';
import { storeRedirects } from './common/redirect-to';
addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
+addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
storeRedirects();
import { ProcessResource } from "./process";
import { EmptyResource } from "./empty";
import { CollectionFile, CollectionDirectory } from 'models/collection-file';
+import { WorkflowResource } from 'models/workflow';
-export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory;
+export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory | WorkflowResource;
return ContextMenuKind.ROOT_PROJECT;
case ResourceKind.LINK:
return ContextMenuKind.LINK;
+ case ResourceKind.WORKFLOW:
+ return ContextMenuKind.WORKFLOW;
default:
return;
}
import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
import { Routes, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
import { RootState } from 'store/store';
+import { openDetailsPanel } from 'store/details-panel/details-panel-action';
import { ServiceRepository } from 'services/services';
import { pluginConfig } from 'plugins';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
case ResourceKind.VIRTUAL_MACHINE:
dispatch<any>(navigateToAdminVirtualMachines);
return;
+ case ResourceKind.WORKFLOW:
+ dispatch<any>(openDetailsPanel(uuid));
+ return;
}
switch (uuid) {
import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from "views/run-process-panel/run-process-advanced-form";
import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from 'models/process';
import { getWorkflow, getWorkflowInputs } from "models/workflow";
+import { ProjectResource } from "models/project";
+import { UserResource } from "models/user";
export const loadProcess = (containerRequestUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
const stringifiedDefinition = JSON.stringify(process.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
const newWorkflow = { ...workflow, definition: stringifiedDefinition };
- const basicInitialData: RunProcessBasicFormData = { name: `Copy of: ${process.name}`, description: process.description };
+ const owner = getResource<ProjectResource | UserResource>(workflow.ownerUuid)(getState().resources);
+ const basicInitialData: RunProcessBasicFormData = { name: `Copy of: ${process.name}`, description: process.description, owner };
dispatch<any>(initialize(RUN_PROCESS_BASIC_FORM, basicInitialData));
const advancedInitialData: RunProcessAdvancedFormData = {
PROJECT = 'Project',
PROCESS = 'Process',
COLLECTION = 'Data collection',
+ WORKFLOW = 'Workflow',
}
export enum GroupTypeFilter {
initFilter(ObjectTypeFilter.PROJECT),
initFilter(ObjectTypeFilter.PROCESS),
initFilter(ObjectTypeFilter.COLLECTION),
+ initFilter(ObjectTypeFilter.WORKFLOW),
);
// Using pipe() with more than 7 arguments makes the return type be 'any',
initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION),
initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
),
+ initFilter(ObjectTypeFilter.WORKFLOW)
+
);
export const getInitialProcessTypeFilters = pipe(
return ResourceKind.PROCESS;
case ObjectTypeFilter.COLLECTION:
return ResourceKind.COLLECTION;
+ case ObjectTypeFilter.WORKFLOW:
+ return ResourceKind.WORKFLOW;
}
};
} from 'views/run-process-panel/run-process-advanced-form';
import { dialogActions } from 'store/dialog/dialog-actions';
import { setBreadcrumbs } from 'store/breadcrumbs/breadcrumbs-actions';
+import { getResource } from 'store/resources/resources';
+import { ProjectResource } from "models/project";
+import { UserResource } from "models/user";
export const runProcessPanelActions = unionize({
SET_PROCESS_PATHNAME: ofType<string>(),
export const loadRunProcessPanel = () =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
try {
- dispatch(setBreadcrumbs([{ label: 'Run workflow' }]));
+ dispatch(setBreadcrumbs([{ label: 'Run Process' }]));
const response = await services.workflowService.list();
dispatch(runProcessPanelActions.SET_WORKFLOWS(response.items));
} catch (e) {
const advancedFormValues = getWorkflowRunnerSettings(workflow);
+ let owner = getResource<ProjectResource | UserResource>(getState().runProcessPanel.processOwnerUuid)(getState().resources);
+ const userUuid = getUserUuid(getState());
+ if (!owner || !userUuid || owner.writableBy.indexOf(userUuid) === -1) {
+ owner = undefined;
+ }
+
if (isStepChanged && isWorkflowChanged) {
dispatch(runProcessPanelActions.SET_STEP_CHANGED(false));
dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
dispatch<any>(loadPresets(workflow.uuid));
- dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name: workflow.name }));
+ dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name: workflow.name, owner }));
dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, advancedFormValues));
}
if (!isWorkflowChanged) {
dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
dispatch<any>(loadPresets(workflow.uuid));
- dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name: workflow.name }));
+ dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name: workflow.name, owner }));
dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, advancedFormValues));
}
};
const userUuid = getUserUuid(getState());
if (!userUuid) { return; }
const { processOwnerUuid, selectedWorkflow } = state.runProcessPanel;
- const ownerUUid = processOwnerUuid ? processOwnerUuid : userUuid;
+ const ownerUUid = basicForm.owner ? basicForm.owner.uuid : (processOwnerUuid ? processOwnerUuid : userUuid);
if (selectedWorkflow) {
const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData || getWorkflowRunnerSettings(selectedWorkflow);
const newProcessData = {
import { RUN_PROCESS_BASIC_FORM } from 'views/run-process-panel/run-process-basic-form';
import { RUN_PROCESS_INPUTS_FORM } from 'views/run-process-panel/run-process-inputs-form';
import { RUN_PROCESS_ADVANCED_FORM } from 'views/run-process-panel/run-process-advanced-form';
+import { getResource, ResourcesState } from 'store/resources/resources';
+import { ProjectResource } from 'models/project';
+import { UserResource } from 'models/user';
+import { getUserUuid } from "common/getuser";
export const WORKFLOW_PANEL_ID = "workflowPanel";
const UUID_PREFIX_PROPERTY_NAME = 'uuidPrefix';
dispatch<any>(loadPresets(workflow.uuid));
dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, getWorkflowRunnerSettings(workflow)));
+ let owner;
if (ownerUuid) {
- dispatch(runProcessPanelActions.SET_PROCESS_OWNER_UUID(ownerUuid));
+ // Must be writable.
+ const userUuid = getUserUuid(getState());
+ owner = getResource<ProjectResource | UserResource>(ownerUuid)(getState().resources);
+ if (!owner || !userUuid || owner.writableBy.indexOf(userUuid) === -1) {
+ owner = undefined;
+ }
}
- if (name) {
- dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name }));
+ if (owner) {
+ dispatch(runProcessPanelActions.SET_PROCESS_OWNER_UUID(owner.uuid));
}
+
+ dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name, owner }));
+
if (inputObj) {
dispatch(initialize(RUN_PROCESS_INPUTS_FORM, inputObj));
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { openRunProcess } from "store/workflow-panel/workflow-panel-actions";
+
+export const workflowActionSet: ContextMenuActionSet = [[
+ {
+ name: "Run",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openRunProcess(resource.uuid, resource.ownerUuid, resource.name));
+ }
+ },
+]];
const emptyActionSet: ContextMenuActionSet = [];
const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => (
- resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet
+ resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet
);
export enum ContextMenuKind {
GROUP_MEMBER = "GroupMember",
PERMISSION_EDIT = "PermissionEdit",
LINK = "Link",
+ WORKFLOW = "Workflow",
}
import { CollectionDetails } from "./collection-details";
import { ProcessDetails } from "./process-details";
import { EmptyDetails } from "./empty-details";
+import { WorkflowDetails } from "./workflow-details";
import { DetailsData } from "./details-data";
import { DetailsResource } from "models/details";
import { Config } from 'common/config';
return new CollectionDetails(res);
case ResourceKind.PROCESS:
return new ProcessDetails(res);
+ case ResourceKind.WORKFLOW:
+ return new WorkflowDetails(res);
default:
return new EmptyDetails(res);
}
let shouldShowInlinePreview = false;
if (!('kind' in res)) {
shouldShowInlinePreview = isInlineFileUrlSafe(
- res ? res.url : "",
- authConfig.keepWebServiceUrl,
- authConfig.keepWebInlineServiceUrl
+ res ? res.url : "",
+ authConfig.keepWebServiceUrl,
+ authConfig.keepWebInlineServiceUrl
) || authConfig.clusterConfig.Collections.TrustAllContent;
}
</Grid>
<Grid item>
<Tabs onChange={this.handleChange}
- value={(item.getTabLabels().length >= tabNr+1) ? tabNr : 0}>
- { item.getTabLabels().map((tabLabel, idx) =>
+ value={(item.getTabLabels().length >= tabNr + 1) ? tabNr : 0}>
+ {item.getTabLabels().map((tabLabel, idx) =>
<Tab key={`tab-label-${idx}`} disableRipple label={tabLabel} />)
}
</Tabs>
</Grid>
<Grid item xs className={this.props.classes.tabContainer} >
- {item.getDetails({tabNr, showPreview: shouldShowInlinePreview})}
+ {item.getDetails({ tabNr, showPreview: shouldShowInlinePreview })}
</Grid>
</Grid >;
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { DefaultIcon, WorkflowIcon } from 'components/icon/icon';
+import { WorkflowResource } from 'models/workflow';
+import { DetailsData } from "./details-data";
+import { DefaultView } from 'components/default-view/default-view';
+import { DetailsAttribute } from 'components/details-attribute/details-attribute';
+import { ResourceOwnerWithName } from 'views-components/data-explorer/renderers';
+import { formatDate } from "common/formatters";
+import { Grid } from '@material-ui/core';
+import { withStyles, StyleRulesCallback, WithStyles, Button } from '@material-ui/core';
+import { openRunProcess } from "store/workflow-panel/workflow-panel-actions";
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { ArvadosTheme } from 'common/custom-theme';
+
+export interface WorkflowDetailsCardDataProps {
+ workflow?: WorkflowResource;
+}
+
+export interface WorkflowDetailsCardActionProps {
+ onClick: (wf: WorkflowResource) => () => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ onClick: (wf: WorkflowResource) =>
+ () => wf && dispatch<any>(openRunProcess(wf.uuid, wf.ownerUuid, wf.name)),
+});
+
+type CssRules = 'runButton';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ runButton: {
+ boxShadow: 'none',
+ padding: '2px 10px 2px 5px',
+ fontSize: '0.75rem'
+ },
+});
+
+export const WorkflowDetailsAttributes = connect(null, mapDispatchToProps)(
+ withStyles(styles)(
+ ({ workflow, onClick, classes }: WorkflowDetailsCardDataProps & WorkflowDetailsCardActionProps & WithStyles<CssRules>) => {
+ return <Grid container>
+ <Button onClick={workflow && onClick(workflow)} className={classes.runButton} variant='contained'
+ data-cy='details-panel-run-btn' color='primary' size='small'>
+ Run
+ </Button>
+ {workflow && workflow.description !== "" && <Grid item xs={12} >
+ <DetailsAttribute
+ label={"Description"}
+ value={workflow?.description} />
+ </Grid>}
+ <Grid item xs={12} >
+ <DetailsAttribute
+ label={"Workflow UUID"}
+ linkToUuid={workflow?.uuid} />
+ </Grid>
+ <Grid item xs={12} >
+ <DetailsAttribute
+ label='Owner' linkToUuid={workflow?.ownerUuid}
+ uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+ </Grid>
+ <Grid item xs={12}>
+ <DetailsAttribute label='Created at' value={formatDate(workflow?.createdAt)} />
+ </Grid>
+ <Grid item xs={12}>
+ <DetailsAttribute label='Last modified' value={formatDate(workflow?.modifiedAt)} />
+ </Grid>
+ <Grid item xs={12} >
+ <DetailsAttribute
+ label='Last modified by user' linkToUuid={workflow?.modifiedByUserUuid}
+ uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+ </Grid>
+ </Grid >;
+ }));
+
+export class WorkflowDetails extends DetailsData<WorkflowResource> {
+ getIcon(className?: string) {
+ return <WorkflowIcon className={className} />;
+ }
+
+ getDetails() {
+ return <WorkflowDetailsAttributes workflow={this.item} />;
+ }
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { Field } from 'redux-form';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
+import {
+ GenericCommandInputParameter
+} from 'models/workflow';
+import { GenericInput, GenericInputProps } from './generic-input';
+import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
+import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { TreeItem } from 'components/tree/tree';
+import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectResource } from 'models/project';
+import { ResourceKind } from 'models/resource';
+import { RootState } from 'store/store';
+import { getUserUuid } from 'common/getuser';
+
+export type ProjectCommandInputParameter = GenericCommandInputParameter<ProjectResource, ProjectResource>;
+
+const require: any = (value?: ProjectResource) => (value === undefined);
+
+export interface ProjectInputProps {
+ input: ProjectCommandInputParameter;
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+}
+export const ProjectInput = ({ input, options }: ProjectInputProps) =>
+ <Field
+ name={input.id}
+ commandInput={input}
+ component={ProjectInputComponent as any}
+ format={format}
+ validate={require}
+ {...{
+ options
+ }} />;
+
+const format = (value?: ProjectResource) => value ? value.name : '';
+
+interface ProjectInputComponentState {
+ open: boolean;
+ project?: ProjectResource;
+}
+
+interface HasUserUuid {
+ userUuid: string;
+};
+
+const mapStateToProps = (state: RootState) => ({ userUuid: getUserUuid(state) });
+
+export const ProjectInputComponent = connect(mapStateToProps)(
+ class ProjectInputComponent extends React.Component<GenericInputProps & DispatchProp & HasUserUuid & {
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+ }, ProjectInputComponentState> {
+ state: ProjectInputComponentState = {
+ open: false,
+ };
+
+ componentDidMount() {
+ this.props.dispatch<any>(
+ initProjectsTreePicker(this.props.commandInput.id));
+ }
+
+ render() {
+ return <>
+ {this.renderInput()}
+ {this.renderDialog()}
+ </>;
+ }
+
+ openDialog = () => {
+ this.setState({ open: true });
+ }
+
+ closeDialog = () => {
+ this.setState({ open: false });
+ }
+
+ submit = () => {
+ this.closeDialog();
+ this.props.input.onChange(this.state.project);
+ }
+
+ setProject = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
+ if ('kind' in data && data.kind === ResourceKind.PROJECT) {
+ this.setState({ project: data });
+ } else {
+ this.setState({ project: undefined });
+ }
+ }
+
+ invalid = () => (!this.state.project || this.state.project.writableBy.indexOf(this.props.userUuid) === -1);
+
+ renderInput() {
+ return <GenericInput
+ component={props =>
+ <Input
+ readOnly
+ fullWidth
+ value={props.input.value}
+ error={props.meta.touched && !!props.meta.error}
+ disabled={props.commandInput.disabled}
+ onClick={!this.props.commandInput.disabled ? this.openDialog : undefined}
+ onKeyPress={!this.props.commandInput.disabled ? this.openDialog : undefined} />}
+ {...this.props} />;
+ }
+
+ renderDialog() {
+ return <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ data-cy="choose-a-project-dialog"
+ maxWidth='md'>
+ <DialogTitle>Choose a project</DialogTitle>
+ <DialogContent>
+ <ProjectsTreePicker
+ pickerId={this.props.commandInput.id}
+ options={this.props.options}
+ toggleItemActive={this.setProject} />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ disabled={this.invalid()}
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog>;
+ }
+
+ });
import { reduxForm, Field } from 'redux-form';
import { Grid } from '@material-ui/core';
import { TextField } from 'components/text-field/text-field';
+import { ProjectInput, ProjectCommandInputParameter } from 'views/run-process-panel/inputs/project-input';
import { PROCESS_NAME_VALIDATION } from 'validators/validators';
+import { ProjectResource } from 'models/project';
+import { UserResource } from 'models/user';
export const RUN_PROCESS_BASIC_FORM = 'runProcessBasicForm';
export interface RunProcessBasicFormData {
name: string;
description: string;
+ owner?: ProjectResource | UserResource;
}
+
export const RunProcessBasicForm =
reduxForm<RunProcessBasicFormData>({
form: RUN_PROCESS_BASIC_FORM
<Field
name='name'
component={TextField as any}
- label="Enter a new name for run process"
+ label="Name for this workflow run"
required
validate={PROCESS_NAME_VALIDATION} />
</Grid>
<Field
name='description'
component={TextField as any}
- label="Enter a description for run process" />
+ label="Optional description of this workflow run" />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <ProjectInput input={{
+ id: "owner",
+ label: "Project where the workflow will run"
+ } as ProjectCommandInputParameter}
+ options={{ showOnlyOwned: false, showOnlyWritable: true }} />
</Grid>
</Grid>
</form>);
TableCell,
TableBody,
TableRow,
- Grid,
} from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
import { WorkflowIcon } from 'components/icon/icon';
import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
-import { WorkflowResource, parseWorkflowDefinition, getWorkflowInputs, getInputLabel, stringifyInputType } from 'models/workflow';
-import { DetailsAttribute } from 'components/details-attribute/details-attribute';
-import { ResourceOwnerWithName } from 'views-components/data-explorer/renderers';
-import { formatDate } from "common/formatters";
+import { parseWorkflowDefinition, getWorkflowInputs, getInputLabel, stringifyInputType } from 'models/workflow';
+import { WorkflowDetailsCardDataProps, WorkflowDetailsAttributes } from 'views-components/details-panel/workflow-details';
export type CssRules = 'root' | 'tab' | 'inputTab' | 'graphTab' | 'graphTabWithChosenWorkflow' | 'descriptionTab' | 'inputsTable';
},
});
-interface WorkflowDetailsCardDataProps {
- workflow?: WorkflowResource;
-}
-
type WorkflowDetailsCardProps = WorkflowDetailsCardDataProps & WithStyles<CssRules>;
export const WorkflowDetailsCard = withStyles(styles)(
</Table>;
}
});
-
-export const WorkflowDetailsAttributes = ({ workflow }: WorkflowDetailsCardDataProps) => {
- return <Grid container>
- <Grid item xs={12} >
- <DetailsAttribute
- label={"Workflow UUID"}
- linkToUuid={workflow?.uuid} />
- </Grid>
- <Grid item xs={12} >
- <DetailsAttribute
- label='Owner' linkToUuid={workflow?.ownerUuid}
- uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
- </Grid>
- <Grid item xs={12}>
- <DetailsAttribute label='Created at' value={formatDate(workflow?.createdAt)} />
- </Grid>
- <Grid item xs={12}>
- <DetailsAttribute label='Last modified' value={formatDate(workflow?.modifiedAt)} />
- </Grid>
- <Grid item xs={12} >
- <DetailsAttribute
- label='Last modified by user' linkToUuid={workflow?.modifiedByUserUuid}
- uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
- </Grid>
- </Grid >;
-};