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
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 }));
+ }
+ };
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 { 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: CommandIcon,
name: "Command",
execute: (dispatch, resource) => {
- // add code
+ dispatch<any>(openProcessCommandDialog(resource.uuid));
}
},
{
--- /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
// 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';
};
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>;
selected: true,
type: ResourceKind.COLLECTION
},
- {
- name: resourceLabel(ResourceKind.PROCESS),
- selected: true,
- type: ResourceKind.PROCESS
- },
{
name: resourceLabel(ResourceKind.PROJECT),
selected: true,
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 { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
type CssRules = 'root' | 'asidePanel' | 'contentWrapper' | 'content' | 'appBar';
</Grid>}
</Grid>
<ContextMenu />
- <Snackbar />
- <CreateProjectDialog />
- <CreateCollectionDialog />
- <RenameFileDialog />
- <PartialCopyCollectionDialog />
- <FileRemoveDialog />
<CopyCollectionDialog />
<CopyProcessDialog />
+ <CreateCollectionDialog />
+ <CreateProjectDialog />
+ <CurrentTokenDialog />
+ <FileRemoveDialog />
<FileRemoveDialog />
- <MultipleFilesRemoveDialog />
- <UpdateCollectionDialog />
- <UpdateProcessDialog />
<FilesUploadCollectionDialog />
- <UpdateProjectDialog />
<MoveCollectionDialog />
<MoveProcessDialog />
<MoveProjectDialog />
- <CurrentTokenDialog />
+ <MultipleFilesRemoveDialog />
+ <PartialCopyCollectionDialog />
+ <ProcessCommandDialog />
+ <RenameFileDialog />
+ <Snackbar />
+ <UpdateCollectionDialog />
+ <UpdateProcessDialog />
+ <UpdateProjectDialog />
</>;
}