import * as React from 'react';
import { StyleRulesCallback, WithStyles, Typography, withStyles, Theme } from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
+import * as classNames from 'classnames';
type CssRules = 'root';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
boxSizing: 'border-box',
- width: '100%',
- height: 'auto',
- maxHeight: '550px',
overflow: 'auto',
padding: theme.spacing.unit
}
export interface CodeSnippetDataProps {
lines: string[];
+ className?: string;
}
type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
export const CodeSnippet = withStyles(styles)(
- ({ classes, lines }: CodeSnippetProps) =>
- <Typography component="div" className={classes.root}>
+ ({ classes, lines, className }: CodeSnippetProps) =>
+ <Typography
+ component="div"
+ className={classNames(classes.root, className)}>
{
lines.map((line: string, index: number) => {
return <Typography key={index} component="pre">{line}</Typography>;
}
});
-type DefaultCodeSnippet = CodeSnippetDataProps;
-
-export const DefaultCodeSnippet = (props: DefaultCodeSnippet) =>
+export const DefaultCodeSnippet = (props: CodeSnippetDataProps) =>
<MuiThemeProvider theme={theme}>
- <CodeSnippet lines={props.lines} />
+ <CodeSnippet {...props} />
</MuiThemeProvider>;
\ No newline at end of file
import * as React from 'react';
import AccessTime from '@material-ui/icons/AccessTime';
+import Add from '@material-ui/icons/Add';
import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import BubbleChart from '@material-ui/icons/BubbleChart';
export type IconType = React.SFC<{ className?: string }>;
+export const AddIcon: IconType = (props) => <Add {...props} />;
export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
export const AdvancedIcon: IconType = (props) => <SettingsApplications {...props} />;
export const BackIcon: IconType = (props) => <ArrowBack {...props} />;
filters = new FilterBuilder();
});
- it("should add 'equal' rule", () => {
+ it("should add 'equal' rule (string)", () => {
expect(
filters.addEqual("etag", "etagValue").getFilters()
).toEqual(`["etag","=","etagValue"]`);
});
+ it("should add 'equal' rule (boolean)", () => {
+ expect(
+ filters.addEqual("is_trashed", true).getFilters()
+ ).toEqual(`["is_trashed","=",true]`);
+ });
+
it("should add 'like' rule", () => {
expect(
filters.addLike("etag", "etagValue").getFilters()
export class FilterBuilder {
constructor(private filters = "") { }
- public addEqual(field: string, value?: string, resourcePrefix?: string) {
+ public addEqual(field: string, value?: string | boolean, resourcePrefix?: string) {
return this.addCondition(field, "=", value, "", "", resourcePrefix );
}
return this.filters;
}
- private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "", resourcePrefix?: string) {
+ private addCondition(field: string, cond: string, value?: string | string[] | boolean, prefix: string = "", postfix: string = "", resourcePrefix?: string) {
if (value) {
- value = typeof value === "string"
- ? `"${prefix}${value}${postfix}"`
- : `["${value.join(`","`)}"]`;
+ if (typeof value === "string") {
+ value = `"${prefix}${value}${postfix}"`;
+ } else if (Array.isArray(value)) {
+ value = `["${value.join(`","`)}"]`;
+ } else {
+ value = value ? "true" : "false";
+ }
const resPrefix = resourcePrefix
? _.snakeCase(resourcePrefix) + "."
import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
import { extractUuidKind, ResourceKind } from '~/models/resource';
import { matchProcessRoute } from '~/routes/routes';
+import { Process } from '~/store/processes/process';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
}
};
-export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
+export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) =>
(dispatch: Dispatch, getState: () => RootState) => {
- const { location } = getState().router;
- const pathname = location ? location.pathname : '';
- const match = matchProcessRoute(pathname);
- const uuid = match ? match.params.id : '';
const resource = {
- uuid,
+ uuid: process.containerRequest.uuid,
ownerUuid: '',
kind: ResourceKind.PROCESS,
name: '',
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { RootState } from '../store';
+import { Dispatch } from 'redux';
+import { getProcess } from '~/store/processes/process';
+
+export const PROCESS_COMMAND_DIALOG_NAME = 'processCommandDialog';
+
+export interface ProcessCommandDialogData {
+ command: string;
+ processName: string;
+}
+
+export const openProcessCommandDialog = (processUuid: string) =>
+ (dispatch: Dispatch<any>, getState: () => RootState) => {
+ const process = getProcess(processUuid)(getState().resources);
+ if (process) {
+ const data: ProcessCommandDialogData = {
+ command: process.containerRequest.command.join(' '),
+ processName: process.containerRequest.name,
+ };
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
+ }
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { initialize, startSubmit, stopSubmit } from 'redux-form';
+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 { getProcess } from '~/store/processes/process';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export interface ProcessUpdateFormDialogData {
+ uuid: string;
+ name: string;
+}
+
+export const PROCESS_UPDATE_FORM_NAME = 'processUpdateFormName';
+
+export const openProcessUpdateDialog = (resource: ProcessUpdateFormDialogData) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const process = getProcess(resource.uuid)(getState().resources);
+ if(process) {
+ dispatch(initialize(PROCESS_UPDATE_FORM_NAME, { ...resource, name: process.containerRequest.name }));
+ dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_UPDATE_FORM_NAME, data: {} }));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000 }));
+ }
+ };
+
+export const updateProcess = (resource: ProcessUpdateFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(PROCESS_UPDATE_FORM_NAME));
+ try {
+ const process = await services.containerRequestService.get(resource.uuid);
+ const updatedProcess = await services.containerRequestService.update(resource.uuid, { ...process, name: resource.name });
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
+ return updatedProcess;
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
+ dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' }));
+ } else if (error === CommonResourceServiceError.MODIFYING_CONTAINER_REQUEST_FINAL_STATE) {
+ dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'You cannot modified in "Final" state.' }));
+ } else {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not update the process.', hideDuration: 2000 }));
+ }
+ return;
+ }
+ };
\ No newline at end of file
import { ProjectResource } from "~/models/project";
import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel";
import { updateFavorites } from "~/store/favorites/favorites-actions";
-import { TrashableResource } from "~/models/resource";
import { snackbarActions } from "~/store/snackbar/snackbar-actions";
import { updateResources } from "~/store/resources/resources-actions";
.addIsA("uuid", typeFilters.map(f => f.type))
.addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
.addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+ .addEqual("is_trashed", true)
.getFilters(),
recursive: true,
includeTrash: true
});
- const items = listResults.items
- .filter(it => (it as TrashableResource).isTrashed)
- .map(it => it.uuid);
+ const items = listResults.items.map(it => it.uuid);
api.dispatch(trashPanelActions.SET_ITEMS({
...listResultsToDataExplorerItemsMeta(listResults),
import * as collectionMoveActions from '~/store/collections/collection-move-actions';
import * as processesActions from '../processes/processes-actions';
import * as processMoveActions from '~/store/processes/process-move-actions';
+import * as processUpdateActions from '~/store/processes/process-update-actions';
import * as processCopyActions from '~/store/processes/process-copy-actions';
import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
-
export const loadWorkbench = () =>
async (dispatch: Dispatch, getState: () => RootState) => {
const { auth, router } = getState();
};
+export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) =>
+ async (dispatch: Dispatch) => {
+ try {
+ const process = await dispatch<any>(processUpdateActions.updateProcess(data));
+ if (process) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Process has been successfully updated.",
+ hideDuration: 2000
+ }));
+ dispatch<any>(updateResources([process]));
+ dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ }
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
+ }
+ };
+
export const moveProcess = (data: MoveToFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
try {
export const COPY_FILE_VALIDATION = [require];
export const MOVE_TO_VALIDATION = [require];
+
+export const PROCESS_NAME_VALIDATION = [require, maxLength(255)];
\ No newline at end of file
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { navigateToProcessLogs } from '~/store/navigation/navigation-action';
import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
+import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
+import { openProcessCommandDialog } from '../../../store/processes/process-command-actions';
export const processActionSet: ContextMenuActionSet = [[
{
icon: RenameIcon,
name: "Edit process",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openProcessUpdateDialog(resource))
},
{
icon: ShareIcon,
icon: CommandIcon,
name: "Command",
execute: (dispatch, resource) => {
- // add code
+ dispatch<any>(openProcessCommandDialog(resource.uuid));
}
},
{
import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
+import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
export const processResourceActionSet: ContextMenuActionSet = [[
{
icon: RenameIcon,
name: "Edit process",
- execute: (dispatch, resource) => {
- // add code
- }
+ execute: (dispatch, resource) => dispatch<any>(openProcessUpdateDialog(resource))
},
{
icon: ShareIcon,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { reduxForm } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { DialogProcessUpdate } from '~/views-components/dialog-update/dialog-process-update';
+import { PROCESS_UPDATE_FORM_NAME, ProcessUpdateFormDialogData } from '~/store/processes/process-update-actions';
+import { updateProcess } from "~/store/workbench/workbench-actions";
+
+export const UpdateProcessDialog = compose(
+ withDialog(PROCESS_UPDATE_FORM_NAME),
+ reduxForm<ProcessUpdateFormDialogData>({
+ form: PROCESS_UPDATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(updateProcess(data));
+ }
+ })
+)(DialogProcessUpdate);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProcessUpdateFormDialogData } from '~/store/processes/process-update-actions';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { ProcessNameField } from '~/views-components/form-fields/process-form-fields';
+
+type DialogProcessProps = WithDialogProps<{}> & InjectedFormProps<ProcessUpdateFormDialogData>;
+
+export const DialogProcessUpdate = (props: DialogProcessProps) =>
+ <FormDialog
+ dialogTitle='Edit Process'
+ formFields={ProcessEditFields}
+ submitLabel='Save'
+ {...props}
+ />;
+
+const ProcessEditFields = () => <span>
+ <ProcessNameField />
+</span>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Field, WrappedFieldProps } from "redux-form";
+import { TextField } from "~/components/text-field/text-field";
+import { PROCESS_NAME_VALIDATION } from "~/validators/validators";
+
+export const ProcessNameField = () =>
+ <Field
+ name='name'
+ component={TextField}
+ validate={PROCESS_NAME_VALIDATION}
+ label="Process Name" />;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { PROCESS_COMMAND_DIALOG_NAME } from '~/store/processes/process-command-actions';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ProcessCommandDialogData } from '~/store/processes/process-command-actions';
+import { DefaultCodeSnippet } from "~/components/default-code-snippet/default-code-snippet";
+import { compose } from 'redux';
+
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ codeSnippet: {
+ marginLeft: theme.spacing.unit * 3,
+ marginRight: theme.spacing.unit * 3,
+ }
+});
+
+export const ProcessCommandDialog = compose(
+ withDialog(PROCESS_COMMAND_DIALOG_NAME),
+ withStyles(styles),
+)(
+ (props: WithDialogProps<ProcessCommandDialogData> & WithStyles<CssRules>) =>
+ <Dialog
+ open={props.open}
+ maxWidth="md"
+ onClose={props.closeDialog}
+ style={{ alignSelf: 'stretch' }}>
+ <DialogTitle>{`Command - ${props.data.processName}`}</DialogTitle>
+ <DefaultCodeSnippet
+ className={props.classes.codeSnippet}
+ lines={[props.data.command]} />
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={props.closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+);
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from '~/store/store';
+import { getProperty } from '~/store/properties/properties';
+import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { PopoverOrigin } from '@material-ui/core/Popover';
+import { StyleRulesCallback, WithStyles, withStyles, Toolbar, Grid, Button, MenuItem, Menu } from '@material-ui/core';
+import { AddIcon, CollectionIcon, ProcessIcon, ProjectIcon } from '~/components/icon/icon';
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
+import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
+import { matchProjectRoute } from '~/routes/routes';
+
+type CssRules = 'button' | 'menuItem' | 'icon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ button: {
+ boxShadow: 'none',
+ padding: '2px 10px 2px 5px',
+ fontSize: '0.75rem'
+ },
+ menuItem: {
+ fontSize: '0.875rem',
+ color: theme.palette.grey["700"]
+ },
+ icon: {
+ marginRight: theme.spacing.unit
+ }
+});
+
+interface SidePanelDataProps {
+ currentItemId: string;
+ buttonVisible: boolean;
+}
+
+interface SidePanelState {
+ anchorEl: any;
+}
+
+type SidePanelProps = SidePanelDataProps & DispatchProp & WithStyles<CssRules>;
+
+const transformOrigin: PopoverOrigin = {
+ vertical: -50,
+ horizontal: 45
+};
+
+const isButtonVisible = ({ router }: RootState) => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchProjectRoute(pathname);
+ return !!match;
+};
+
+export const SidePanelButton = withStyles(styles)(
+ connect((state: RootState) => ({
+ currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+ buttonVisible: isButtonVisible(state)
+ }))(
+ class extends React.Component<SidePanelProps> {
+
+ state: SidePanelState = {
+ anchorEl: undefined
+ };
+
+ render() {
+ const { classes, buttonVisible } = this.props;
+ const { anchorEl } = this.state;
+ return <Toolbar>
+ {buttonVisible && <Grid container>
+ <Grid container item xs alignItems="center" justify="center">
+ <Button variant="contained" color="primary" size="small" className={classes.button}
+ aria-owns={anchorEl ? 'aside-menu-list' : undefined}
+ aria-haspopup="true"
+ onClick={this.handleOpen}>
+ <AddIcon />
+ New
+ </Button>
+ <Menu
+ id='aside-menu-list'
+ anchorEl={anchorEl}
+ open={Boolean(anchorEl)}
+ onClose={this.handleClose}
+ onClick={this.handleClose}
+ transformOrigin={transformOrigin}>
+ <MenuItem className={classes.menuItem} onClick={this.handleNewCollectionClick}>
+ <CollectionIcon className={classes.icon} /> New collection
+ </MenuItem>
+ <MenuItem className={classes.menuItem}>
+ <ProcessIcon className={classes.icon} /> Run a process
+ </MenuItem>
+ <MenuItem className={classes.menuItem} onClick={this.handleNewProjectClick}>
+ <ProjectIcon className={classes.icon} /> New project
+ </MenuItem>
+ </Menu>
+ </Grid>
+ </Grid> }
+ </Toolbar>;
+ }
+
+ handleNewProjectClick = () => {
+ this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
+ }
+
+ handleNewCollectionClick = () => {
+ this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
+ }
+
+ handleClose = () => {
+ this.setState({ anchorEl: undefined });
+ }
+
+ handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
+ this.setState({ anchorEl: event.currentTarget });
+ }
+ }
+ )
+);
\ No newline at end of file
import * as React from 'react';
import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
import { ArvadosTheme } from '~/common/custom-theme';
import { SidePanelTree, SidePanelTreeProps } from '~/views-components/side-panel-tree/side-panel-tree';
import { compose, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action';
+import { Grid } from '@material-ui/core';
+import { SidePanelButton } from '~/views-components/side-panel-button/side-panel-button';
const DRAWER_WITDH = 240;
withStyles(styles),
connect(undefined, mapDispatchToProps)
)(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
- <div className={classes.root}>
+ <Grid item xs>
+ <SidePanelButton />
<SidePanelTree {...props} />
- </div>);
+ </Grid>);
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
-import { CodeSnippet, CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { MuiThemeProvider, createMuiTheme, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
+import { CodeSnippet } from '~/components/code-snippet/code-snippet';
import grey from '@material-ui/core/colors/grey';
+type CssRules = 'codeSnippet';
+
+const styles: StyleRulesCallback<CssRules> = () => ({
+ codeSnippet: {
+ maxHeight: '550px',
+ }
+});
+
const theme = createMuiTheme({
overrides: {
MuiTypography: {
}
});
-type ProcessLogCodeSnippet = CodeSnippetDataProps;
+interface ProcessLogCodeSnippetProps {
+ lines: string[];
+}
-export const ProcessLogCodeSnippet = (props: ProcessLogCodeSnippet) =>
- <MuiThemeProvider theme={theme}>
- <CodeSnippet lines={props.lines} />
- </MuiThemeProvider>;
\ No newline at end of file
+export const ProcessLogCodeSnippet = withStyles(styles)(
+ (props: ProcessLogCodeSnippetProps & WithStyles<CssRules>) =>
+ <MuiThemeProvider theme={theme}>
+ <CodeSnippet lines={props.lines} className={props.classes.codeSnippet} />
+ </MuiThemeProvider>);
\ No newline at end of file
process: Process;
}
-export type ProcessLogMainCardProps = ProcessLogMainCardDataProps & CodeSnippetDataProps & ProcessLogFormDataProps & ProcessLogFormActionProps;
+export interface ProcessLogMainCardActionProps {
+ onContextMenu: (event: React.MouseEvent<any>, process: Process) => void;
+}
+
+export type ProcessLogMainCardProps = ProcessLogMainCardDataProps
+ & ProcessLogMainCardActionProps
+ & CodeSnippetDataProps
+ & ProcessLogFormDataProps
+ & ProcessLogFormActionProps;
export const ProcessLogMainCard = withStyles(styles)(
- ({ classes, process, selectedFilter, filters, onChange, lines }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
+ ({ classes, process, selectedFilter, filters, onChange, lines, onContextMenu }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
<Grid item xs={12}>
<Link to={`/processes/${process.containerRequest.uuid}`} className={classes.backLink}>
<BackIcon className={classes.backIcon} /> Back
<CardHeader
avatar={<ProcessIcon className={classes.iconHeader} />}
action={
- <div>
- <IconButton aria-label="More options">
- <Tooltip title="More options">
- <MoreOptionsIcon />
- </Tooltip>
- </IconButton>
- </div>
- }
+ <IconButton onClick={event => onContextMenu(event, process)} aria-label="More options">
+ <Tooltip title="More options">
+ <MoreOptionsIcon />
+ </Tooltip>
+ </IconButton>}
title={
<Tooltip title={process.containerRequest.name} placement="bottom-start">
<Typography noWrap variant="title" className={classes.title}>
{process.containerRequest.name}
</Typography>
- </Tooltip>
- }
+ </Tooltip>}
subheader={process.containerRequest.description} />
<CardContent>
{lines.length > 0
- ? < Grid container spacing={24} alignItems='center'>
- <Grid item xs={6}>
- <ProcessLogForm selectedFilter={selectedFilter} filters={filters} onChange={onChange} />
- </Grid>
- <Grid item xs={6} className={classes.link}>
- <Typography component='div'>
- Go to Log collection
+ ? < Grid
+ container
+ spacing={24}
+ direction='column'>
+ <Grid container item>
+ <Grid item xs={6}>
+ <ProcessLogForm selectedFilter={selectedFilter} filters={filters} onChange={onChange} />
+ </Grid>
+ <Grid item xs={6} className={classes.link}>
+ <Typography component='div'>
+ Go to Log collection
</Typography>
+ </Grid>
</Grid>
- <Grid item xs={12}>
+ <Grid item xs>
<ProcessLogCodeSnippet lines={lines} />
</Grid>
</Grid>
import { DefaultView } from '~/components/default-view/default-view';
import { ProcessIcon } from '~/components/icon/icon';
import { CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { ProcessLogMainCardActionProps } from './process-log-main-card';
export type ProcessLogPanelRootDataProps = {
process?: Process;
} & ProcessLogFormDataProps & CodeSnippetDataProps;
-export type ProcessLogPanelRootActionProps = {
- onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
-} & ProcessLogFormActionProps;
+export type ProcessLogPanelRootActionProps = ProcessLogMainCardActionProps & ProcessLogFormActionProps;
export type ProcessLogPanelRootProps = ProcessLogPanelRootDataProps & ProcessLogPanelRootActionProps;
//
// SPDX-License-Identifier: AGPL-3.0
-import * as React from 'react';
import { RootState } from '~/store/store';
import { connect } from 'react-redux';
import { getProcess } from '~/store/processes/process';
import { Dispatch } from 'redux';
import { openProcessContextMenu } from '~/store/context-menu/context-menu-actions';
-import { matchProcessLogRoute } from '~/routes/routes';
import { ProcessLogPanelRootDataProps, ProcessLogPanelRootActionProps, ProcessLogPanelRoot } from './process-log-panel-root';
import { getProcessPanelLogs } from '~/store/process-logs-panel/process-logs-panel';
import { setProcessLogsPanelFilter } from '~/store/process-logs-panel/process-logs-panel-actions';
};
const mapDispatchToProps = (dispatch: Dispatch): ProcessLogPanelRootActionProps => ({
- onContextMenu: (event: React.MouseEvent<HTMLElement>) => {
- dispatch<any>(openProcessContextMenu(event));
+ onContextMenu: (event, process) => {
+ dispatch<any>(openProcessContextMenu(event, process));
},
- onChange: (filter: FilterOption) => {
+ onChange: filter => {
dispatch(setProcessLogsPanelFilter(filter.value));
}
});
}
export interface ProcessPanelRootActionProps {
- onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
onToggle: (status: string) => void;
}
export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps;
-export const ProcessPanelRoot = (props: ProcessPanelRootProps) =>
- props.process
+export const ProcessPanelRoot = ({process, ...props}: ProcessPanelRootProps) =>
+ process
? <Grid container spacing={16} alignItems="stretch">
<Grid item sm={12} md={7}>
<ProcessInformationCard
- process={props.process}
- onContextMenu={props.onContextMenu} />
+ process={process}
+ onContextMenu={event => props.onContextMenu(event, process)} />
</Grid>
<Grid item sm={12} md={5}>
<SubprocessesCard
};
const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
- onContextMenu: event => {
- dispatch<any>(openProcessContextMenu(event));
+ onContextMenu: (event, process) => {
+ dispatch<any>(openProcessContextMenu(event, process));
},
onToggle: status => {
dispatch<any>(toggleProcessPanelFilter(status));
export interface ProcessSubprocessesDataProps {
subprocesses: Array<Process>;
- onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
}
export const ProcessSubprocesses = ({ onContextMenu, subprocesses }: ProcessSubprocessesDataProps) => {
return <Grid container spacing={16}>
{subprocesses.map(subprocess =>
<Grid item xs={12} sm={6} md={4} lg={2} key={subprocess.containerRequest.uuid}>
- <ProcessSubprocessesCard onContextMenu={onContextMenu} subprocess={subprocess} />
+ <ProcessSubprocessesCard
+ onContextMenu={event => onContextMenu(event, subprocess)}
+ subprocess={subprocess} />
</Grid>
)}
</Grid>;
import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
-type CssRules = 'root' | "toolbar" | "button";
+type CssRules = 'root' | "button";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
width: '100%',
height: '100%'
},
- toolbar: {
- paddingBottom: theme.spacing.unit * 3,
- textAlign: "right"
- },
button: {
marginLeft: theme.spacing.unit
},
render() {
const { classes } = this.props;
return <div className={classes.root}>
- <div className={classes.toolbar}>
- <Button color="primary" onClick={this.handleNewCollectionClick} variant="raised" className={classes.button}>
- New collection
- </Button>
- <Button color="primary" variant="raised" className={classes.button}>
- Run a process
- </Button>
- <Button color="primary" onClick={this.handleNewProjectClick} variant="raised" className={classes.button}>
- New project
- </Button>
- </div>
{this.hasAnyItems()
? <DataExplorer
id={PROJECT_PANEL_ID}
return resource.ownerUuid === this.props.currentItemId;
}
- handleNewProjectClick = () => {
- this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
- }
-
- handleNewCollectionClick = () => {
- this.props.dispatch<any>(openCollectionCreateDialog(this.props.currentItemId));
- }
-
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
const menuKind = resourceKindToContextMenuKind(resourceUuid);
const resource = getResource<ProjectResource>(resourceUuid)(this.props.resources);
selected: true,
type: ResourceKind.COLLECTION
},
- {
- name: resourceLabel(ResourceKind.PROCESS),
- selected: true,
- type: ResourceKind.PROCESS
- },
{
name: resourceLabel(ResourceKind.PROJECT),
selected: true,
import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
import { CopyProcessDialog } from '~/views-components/dialog-forms/copy-process-dialog';
import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
+import { UpdateProcessDialog } from '~/views-components/dialog-forms/update-process-dialog';
import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
import { MoveProcessDialog } from '~/views-components/dialog-forms/move-process-dialog';
import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
import { TrashPanel } from "~/views/trash-panel/trash-panel";
-import { MainContentBar } from '../../views-components/main-content-bar/main-content-bar';
+import { MainContentBar } from '~/views-components/main-content-bar/main-content-bar';
import { Grid } from '@material-ui/core';
import { WorkbenchProgress } from '~/views-components/progress/workbench-progress';
+import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
-type CssRules = 'root' | 'contentWrapper' | 'content' | 'appBar';
+type CssRules = 'root' | 'asidePanel' | 'contentWrapper' | 'content' | 'appBar';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
width: '100vw',
height: '100vh'
},
+ asidePanel: {
+ maxWidth: '240px',
+ background: theme.palette.background.default
+ },
contentWrapper: {
background: theme.palette.background.default,
minWidth: 0,
state = {
searchText: "",
};
-
render() {
+ const { classes } = this.props;
return <>
- <Grid
- container
- direction="column"
- className={this.props.classes.root}>
- <Grid className={this.props.classes.appBar}>
+ <Grid container direction="column" className={classes.root}>
+ <Grid className={classes.appBar}>
<MainAppBar
searchText={this.state.searchText}
user={this.props.user}
buildInfo={this.props.buildInfo} />
</Grid>
{this.props.user &&
- <Grid
- container
- item
- xs
- alignItems="stretch"
- wrap="nowrap">
- <Grid item>
+ <Grid container item xs alignItems="stretch" wrap="nowrap">
+ <Grid container item xs component='aside' direction='column' className={classes.asidePanel}>
<SidePanel />
</Grid>
- <Grid
- container
- item
- xs
- component="main"
- direction="column"
- className={this.props.classes.contentWrapper}>
+ <Grid container item xs component="main" direction="column" className={classes.contentWrapper}>
<Grid item>
<WorkbenchProgress />
<MainContentBar />
</Grid>
- <Grid item xs className={this.props.classes.content}>
+ <Grid item xs className={classes.content}>
<Switch>
<Route path={Routes.PROJECTS} component={ProjectPanel} />
<Route path={Routes.COLLECTIONS} component={CollectionPanel} />
</Grid>}
</Grid>
<ContextMenu />
- <Snackbar />
- <CreateProjectDialog />
- <CreateCollectionDialog />
- <RenameFileDialog />
- <PartialCopyCollectionDialog />
- <FileRemoveDialog />
<CopyCollectionDialog />
<CopyProcessDialog />
+ <CreateCollectionDialog />
+ <CreateProjectDialog />
+ <CurrentTokenDialog />
+ <FileRemoveDialog />
<FileRemoveDialog />
- <MultipleFilesRemoveDialog />
- <UpdateCollectionDialog />
<FilesUploadCollectionDialog />
- <UpdateProjectDialog />
<MoveCollectionDialog />
<MoveProcessDialog />
<MoveProjectDialog />
- <CurrentTokenDialog />
+ <MultipleFilesRemoveDialog />
+ <PartialCopyCollectionDialog />
+ <ProcessCommandDialog />
+ <RenameFileDialog />
+ <Snackbar />
+ <UpdateCollectionDialog />
+ <UpdateProcessDialog />
+ <UpdateProjectDialog />
</>;
}