--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, Typography, withStyles, Theme } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+
+type CssRules = 'root';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ boxSizing: 'border-box',
+ width: '100%',
+ height: 'auto',
+ maxHeight: '550px',
+ overflow: 'scroll',
+ padding: theme.spacing.unit
+ }
+});
+
+export interface CodeSnippetDataProps {
+ lines: string[];
+}
+
+type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
+
+export const CodeSnippet = withStyles(styles)(
+ ({ classes, lines }: CodeSnippetProps) =>
+ <Typography component="div" className={classes.root}>
+ {
+ lines.map((line: string, index: number) => {
+ return <Typography key={index} component="div">{line}</Typography>;
+ })
+ }
+ </Typography>
+ );
\ 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 { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
+import { CodeSnippet, CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import grey from '@material-ui/core/colors/grey';
+
+const theme = createMuiTheme({
+ overrides: {
+ MuiTypography: {
+ body1: {
+ color: grey["900"]
+ },
+ root: {
+ backgroundColor: grey["200"]
+ }
+ }
+ },
+ typography: {
+ fontFamily: 'VT323'
+ }
+});
+
+type DefaultCodeSnippet = CodeSnippetDataProps;
+
+export const DefaultCodeSnippet = (props: DefaultCodeSnippet) =>
+ <MuiThemeProvider theme={theme}>
+ <CodeSnippet lines={props.lines} />
+ </MuiThemeProvider>;
\ No newline at end of file
import * as React from 'react';
import AccessTime from '@material-ui/icons/AccessTime';
+import ArrowBack from '@material-ui/icons/ArrowBack';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import BubbleChart from '@material-ui/icons/BubbleChart';
import Cached from '@material-ui/icons/Cached';
export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
export const AdvancedIcon: IconType = (props) => <SettingsApplications {...props} />;
+export const BackIcon: IconType = (props) => <ArrowBack {...props} />;
export const CustomizeTableIcon: IconType = (props) => <Menu {...props} />;
export const CopyIcon: IconType = (props) => <ContentCopy {...props} />;
export const CollectionIcon: IconType = (props) => <LibraryBooks {...props} />;
body {
margin: 0;
padding: 0;
- font-family: sans-serif;
+ font-family: 'Roboto', "Helvetica", "Arial", sans-serif;
width: 100vw;
height: 100vh;
}
import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
import { getProjectUrl } from '../models/project';
import { getCollectionUrl } from '~/models/collection';
-import { loadProject, loadFavorites, loadCollection } from '~/store/workbench/workbench-actions';
+import { loadProject, loadFavorites, loadCollection, loadProcessLog } from '~/store/workbench/workbench-actions';
import { loadProcess } from '~/store/processes/processes-actions';
export const Routes = {
COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
FAVORITES: '/favorites',
+ PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`
};
export const getResourceUrl = (uuid: string) => {
export const getProcessUrl = (uuid: string) => `/processes/${uuid}`;
+export const getProcessLogUrl = (uuid: string) => `/process-logs/${uuid}`;
+
export const addRouteChangeHandlers = (history: History, store: RootStore) => {
const handler = handleLocationChange(store);
handler(history.location);
export const matchProcessRoute = (route: string) =>
matchPath<ResourceRouteParams>(route, { path: Routes.PROCESSES });
+export const matchProcessLogRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.PROCESS_LOGS });
const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
const projectMatch = matchProjectRoute(pathname);
const collectionMatch = matchCollectionRoute(pathname);
const favoriteMatch = matchFavoritesRoute(pathname);
const processMatch = matchProcessRoute(pathname);
+ const processLogMatch = matchProcessLogRoute(pathname);
+
if (projectMatch) {
store.dispatch(loadProject(projectMatch.params.id));
} else if (collectionMatch) {
store.dispatch(loadFavorites());
} else if (processMatch) {
store.dispatch(loadProcess(processMatch.params.id));
+ } else if (processLogMatch) {
+ store.dispatch(loadProcessLog(processLogMatch.params.id));
}
};
import { UserResource } from '../../models/user';
import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
import { extractUuidKind, ResourceKind } from '~/models/resource';
+import { matchProcessRoute } from '~/routes/routes';
export const contextMenuActions = unionize({
OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
(dispatch: Dispatch, getState: () => RootState) => {
+ const { location } = getState().router;
+ const pathname = location ? location.pathname : '';
+ // ToDo: We get error from matchProcessRoute
+ // const match = matchProcessRoute(pathname);
+ // console.log('match: ', match);
+ // const uuid = match ? match.params.id : '';
+ const uuid = pathname.split('/').slice(-1)[0];
const resource = {
- uuid: '',
+ uuid,
name: '',
description: '',
kind: ContextMenuKind.PROCESS
import { getProjectUrl } from "~/models/project";
import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
-import { Routes, getProcessUrl } from '~/routes/routes';
+import { Routes, getProcessUrl, getProcessLogUrl } from '~/routes/routes';
export const navigateTo = (uuid: string) =>
async (dispatch: Dispatch) => {
dispatch<any>(navigateToCollection(uuid));
} else if (kind === ResourceKind.CONTAINER_REQUEST) {
dispatch<any>(navigateToProcess(uuid));
- }
+ }
if (uuid === SidePanelTreeCategory.FAVORITES) {
dispatch<any>(navigateToFavorites);
}
export const navigateToCollection = compose(push, getCollectionUrl);
export const navigateToProcess = compose(push, getProcessUrl);
+
+export const navigateToProcessLogs = compose(push, getProcessLogUrl);
\ No newline at end of file
}
};
+export const loadProcessLog = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch<any>(loadProcess(uuid));
+ // ToDo: loadLog();
+ };
+
export const resourceIsNotLoaded = (uuid: string) =>
snackbarActions.OPEN_SNACKBAR({
message: `Resource identified by ${uuid} is not loaded.`
AdvancedIcon, RemoveIcon, ReRunProcessIcon, LogIcon
} from "~/components/icon/icon";
import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { navigateToProcessLogs } from '~/store/navigation/navigation-action';
export const processActionSet: ContextMenuActionSet = [[
{
icon: LogIcon,
name: "Log",
execute: (dispatch, resource) => {
- // add code
+ dispatch<any>(navigateToProcessLogs(resource.uuid));
}
},
{
COLLECTION_FILES_ITEM = "CollectionFilesItem",
COLLECTION = 'Collection',
COLLECTION_RESOURCE = 'CollectionResource',
- PROCESS = "Process"
+ PROCESS = "Process",
+ PROCESS_LOGS = "ProcessLogs"
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 grey from '@material-ui/core/colors/grey';
+
+const theme = createMuiTheme({
+ overrides: {
+ MuiTypography: {
+ body1: {
+ color: grey["200"]
+ },
+ root: {
+ backgroundColor: '#000'
+ }
+ }
+ },
+ typography: {
+ fontFamily: 'VT323'
+ }
+});
+
+type ProcessLogCodeSnippet = CodeSnippetDataProps;
+
+export const ProcessLogCodeSnippet = (props: ProcessLogCodeSnippet) =>
+ <MuiThemeProvider theme={theme}>
+ <CodeSnippet lines={props.lines} />
+ </MuiThemeProvider>;
\ 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 { withStyles, WithStyles, StyleRulesCallback, FormControl, InputLabel, Select, MenuItem, Input } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { FilterOption } from './process-log-panel';
+
+type CssRules = 'formControl';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ formControl: {
+ minWidth: 200
+ }
+});
+
+export interface ProcessLogFormDataProps {
+ selectedFilter: FilterOption;
+ filters: FilterOption[];
+}
+
+export interface ProcessLogFormActionProps {
+ onChange: (filter: FilterOption) => void;
+}
+
+type ProcessLogFormProps = ProcessLogFormDataProps & ProcessLogFormActionProps & WithStyles<CssRules>;
+
+export const ProcessLogForm = withStyles(styles)(
+ ({ classes, selectedFilter, onChange, filters }: ProcessLogFormProps) =>
+ <form autoComplete="off">
+ <FormControl className={classes.formControl}>
+ <InputLabel shrink htmlFor="log-label-placeholder">
+ Event Type
+ </InputLabel>
+ <Select
+ value={selectedFilter.value}
+ onChange={event => onChange}
+ input={<Input name="eventType" id="log-label-placeholder" />}
+ name="eventType">
+ {
+ filters.map(option =>
+ <MenuItem key={option.value} value={option.value}>{option.label}</MenuItem>
+ )
+ }
+ </Select>
+ </FormControl>
+ </form>
+);
\ 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 { Link } from 'react-router-dom';
+import {
+ StyleRulesCallback, WithStyles, withStyles, Card,
+ CardHeader, IconButton, CardContent, Grid, Typography, Tooltip
+} from '@material-ui/core';
+import { Process } from '~/store/processes/process';
+import { ProcessLogCodeSnippet } from '~/views/process-log-panel/process-log-code-snippet';
+import { ProcessLogForm, ProcessLogFormDataProps, ProcessLogFormActionProps } from '~/views/process-log-panel/process-log-form';
+import { MoreOptionsIcon, ProcessIcon } from '~/components/icon/icon';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+import { BackIcon } from '~/components/icon/icon';
+
+type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ backLink: {
+ fontSize: '1rem',
+ fontWeight: 600,
+ display: 'flex',
+ alignItems: 'center',
+ textDecoration: 'none',
+ padding: theme.spacing.unit,
+ color: theme.palette.grey["700"],
+ },
+ backIcon: {
+ marginRight: theme.spacing.unit
+ },
+ card: {
+ width: '100%'
+ },
+ title: {
+ color: theme.palette.grey["700"]
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.green700
+ },
+ link: {
+ alignSelf: 'flex-end',
+ textAlign: 'right'
+ }
+});
+
+
+interface ProcessLogMainCardDataProps {
+ process: Process;
+}
+
+export type ProcessLogMainCardProps = ProcessLogMainCardDataProps & CodeSnippetDataProps & ProcessLogFormDataProps & ProcessLogFormActionProps;
+
+export const ProcessLogMainCard = withStyles(styles)(
+ ({ classes, process, selectedFilter, filters, onChange, lines }: ProcessLogMainCardProps & WithStyles<CssRules>) =>
+ <Grid item xs={12}>
+ <Link to={`/processes/${process.containerRequest.uuid}`} className={classes.backLink}>
+ <BackIcon className={classes.backIcon}/> Back
+ </Link>
+ <Card className={classes.card}>
+ <CardHeader
+ avatar={<ProcessIcon className={classes.iconHeader} />}
+ action={
+ <div>
+ <IconButton aria-label="More options">
+ <MoreOptionsIcon />
+ </IconButton>
+ </div>
+ }
+ title={
+ <Tooltip title={process.containerRequest.name} placement="bottom-start">
+ <Typography noWrap variant="title" className={classes.title}>
+ {process.containerRequest.name}
+ </Typography>
+ </Tooltip>
+ }
+ subheader={process.containerRequest.description} />
+ <CardContent>
+ <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
+ </Typography>
+ </Grid>
+ <Grid item xs={12}>
+ <ProcessLogCodeSnippet lines={lines}/>
+ </Grid>
+ </Grid>
+ </CardContent>
+ </Card>
+ </Grid>
+);
\ 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 { Grid } from '@material-ui/core';
+import { Process } from '~/store/processes/process';
+import { ProcessLogMainCard } from '~/views/process-log-panel/process-log-main-card';
+import { ProcessLogFormDataProps, ProcessLogFormActionProps } from '~/views/process-log-panel/process-log-form';
+import { DefaultView } from '~/components/default-view/default-view';
+import { ProcessIcon } from '~/components/icon/icon';
+import { CodeSnippetDataProps } from '~/components/code-snippet/code-snippet';
+
+export type ProcessLogPanelRootDataProps = {
+ process?: Process;
+} & ProcessLogFormDataProps & CodeSnippetDataProps;
+
+export type ProcessLogPanelRootActionProps = {
+ onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+} & ProcessLogFormActionProps;
+
+export type ProcessLogPanelRootProps = ProcessLogPanelRootDataProps & ProcessLogPanelRootActionProps;
+
+export const ProcessLogPanelRoot = (props: ProcessLogPanelRootProps) =>
+ props.process
+ ? <Grid container spacing={16}>
+ <ProcessLogMainCard
+ process={props.process}
+ {...props} />
+ </Grid>
+ : <Grid container
+ alignItems='center'
+ justify='center'>
+ <DefaultView
+ icon={ProcessIcon}
+ messages={['Process Log not found']} />
+ </Grid>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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';
+
+const SELECT_OPTIONS = [
+ { label: 'Dispatch', value: 'dispatch' },
+ { label: 'Crunch-run', value: 'crunch-run' },
+ { label: 'Crunchstat', value: 'crunchstat' },
+ { label: 'Hoststat', value: 'hoststat' },
+ { label: 'Node-info', value: 'node-info' },
+ { label: 'Arv-mount', value: 'arv-mount' },
+ { label: 'Stdout', value: 'stdout' },
+ { label: 'Stderr', value: 'stderr' }
+];
+
+const lines = ['Lorem Ipsum', 'Lorem Ipsum', 'Lorem Ipsum', 'Lorem Ipsum', 'Lorem Ipsum', 'Lorem Ipsum', 'Lorem Ipsum', 'Lorem Ipsum'];
+
+export interface Log {
+ object_uuid: string;
+ event_at: string;
+ event_type: string;
+ summary: string;
+ properties: any;
+}
+
+export interface FilterOption {
+ label: string;
+ value: string;
+}
+
+const mapStateToProps = ({ router, resources }: RootState): ProcessLogPanelRootDataProps => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchProcessLogRoute(pathname);
+ const uuid = match ? match.params.id : '';
+ return {
+ process: getProcess(uuid)(resources),
+ selectedFilter: SELECT_OPTIONS[0],
+ filters: SELECT_OPTIONS,
+ lines
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ProcessLogPanelRootActionProps => ({
+ onContextMenu: (event: React.MouseEvent<HTMLElement>) => {
+ dispatch<any>(openProcessContextMenu(event));
+ },
+ onChange: (filter: FilterOption) => { return; }
+});
+
+export const ProcessLogPanel = connect(mapStateToProps, mapDispatchToProps)(ProcessLogPanelRoot);
</div>
}
title={
- <Tooltip title={process.containerRequest.name}>
- <Typography noWrap variant="title" color='inherit'>
+ <Tooltip title={process.containerRequest.name} placement="bottom-start" color='inherit'>
+ <Typography noWrap variant="title">
{process.containerRequest.name}
</Typography>
</Tooltip>
import { Routes } from '~/routes/routes';
import { SidePanel } from '~/views-components/side-panel/side-panel';
import { ProcessPanel } from '~/views/process-panel/process-panel';
+import { ProcessLogPanel } from '~/views/process-log-panel/process-log-panel';
import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-collection-dialog';
-
import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
<Route path={Routes.COLLECTIONS} component={CollectionPanel} />
<Route path={Routes.FAVORITES} component={FavoritePanel} />
<Route path={Routes.PROCESSES} component={ProcessPanel} />
+ <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
</Switch>
</div>
{user && <DetailsPanel />}