import yellow from '@material-ui/core/colors/yellow';
import red from '@material-ui/core/colors/red';
-interface ArvadosThemeOptions extends ThemeOptions {
+export interface ArvadosThemeOptions extends ThemeOptions {
customs: any;
}
interface Colors {
green700: string;
yellow700: string;
+ red900: string;
+ blue500: string;
+ grey500: string;
}
const red900 = red["900"];
const grey900 = grey["900"];
const rocheBlue = '#06C';
-const themeOptions: ArvadosThemeOptions = {
+export const themeOptions: ArvadosThemeOptions = {
customs: {
colors: {
green700: green["700"],
- yellow700: yellow["700"]
+ yellow700: yellow["700"],
+ red900: red['900'],
+ blue500: blue['500'],
+ grey500,
}
},
overrides: {
return "";
};
+export const formatTime = (time: number) => {
+ const minutes = Math.floor(time / (1000 * 60) % 60).toFixed(0);
+ const hours = Math.floor(time / (1000 * 60 * 60)).toFixed(0);
+
+ return hours + "h " + minutes + "m";
+};
+
+export const getTimeDiff = (endTime: string, startTime: string) => {
+ return new Date(endTime).getTime() - new Date(startTime).getTime();
+};
+
export const formatProgress = (loaded: number, total: number) => {
const progress = loaded >= 0 && total > 0 ? loaded * 100 / total : 0;
return `${progress.toFixed(2)}%`;
--- /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="pre">{line}</Typography>;
+ })
+ }
+ </Typography>
+ );
\ No newline at end of file
defaultIcon: ProjectIcon,
onSetColumns: jest.fn(),
defaultMessages: ['testing'],
+ contextMenuColumn: true
});
--- /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 { loadWorkbench } from './store/workbench/workbench-actions';
import { Routes } from '~/routes/routes';
import { trashActionSet } from "~/views-components/context-menu/action-sets/trash-action-set";
+import { ServiceRepository } from '~/services/services';
+import { initWebSocket } from '~/websocket/websocket';
+import { Config } from '~/common/config';
const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
const services = createServices(config);
const store = configureStore(history, services);
- store.subscribe(initListener(history, store));
-
+ store.subscribe(initListener(history, store, services, config));
store.dispatch(initAuth());
const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
});
-const initListener = (history: History, store: RootStore) => {
+const initListener = (history: History, store: RootStore, services: ServiceRepository, config: Config) => {
let initialized = false;
return async () => {
const { router, auth } = store.getState();
if (router.location && auth.user && !initialized) {
initialized = true;
+ initWebSocket(config, services.authService, store);
await store.dispatch(loadWorkbench());
addRouteChangeHandlers(history, store);
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "./resource";
+import { ResourceKind } from '~/models/resource';
+
+export enum LogEventType {
+ CREATE = 'create',
+ UPDATE = 'update',
+ DISPATCH = 'dispatch',
+ CRUNCH_RUN = 'crunch-run',
+ CRUNCHSTAT = 'crunchstat',
+ HOSTSTAT = 'hoststat',
+ NODE_INFO = 'node-info',
+ ARV_MOUNT = 'arv-mount',
+ STDOUT = 'stdout',
+ STDERR = 'stderr',
+}
+
+export interface LogResource extends Resource {
+ kind: ResourceKind.LOG;
+ objectUuid: string;
+ eventAt: string;
+ eventType: string;
+ summary: string;
+ properties: any;
+}
CONTAINER = "arvados#container",
CONTAINER_REQUEST = "arvados#containerRequest",
GROUP = "arvados#group",
+ LOG = "arvados#log",
PROCESS = "arvados#containerRequest",
PROJECT = "arvados#group",
USER = "arvados#user",
CONTAINER = 'dz642',
CONTAINER_REQUEST = 'xvhdp',
GROUP = 'j7d0g',
+ LOG = '57u5n',
USER = 'tpzed',
}
return ResourceKind.CONTAINER_REQUEST;
case ResourceObjectType.CONTAINER:
return ResourceKind.CONTAINER;
+ case ResourceObjectType.LOG:
+ return ResourceKind.LOG;
default:
return undefined;
}
import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
import { getProjectUrl } from '~/models/project';
import { getCollectionUrl } from '~/models/collection';
-import { loadProject, loadFavorites, loadCollection, loadTrash } from '~/store/workbench/workbench-actions';
+import { loadProject, loadFavorites, loadCollection, loadTrash, 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',
- TRASH: '/trash'
+ TRASH: '/trash',
+ 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 favoriteMatch = matchFavoritesRoute(pathname);
const trashMatch = matchTrashRoute(pathname);
const processMatch = matchProcessRoute(pathname);
+ const processLogMatch = matchProcessLogRoute(pathname);
+
if (projectMatch) {
store.dispatch(loadProject(projectMatch.params.id));
} else if (collectionMatch) {
store.dispatch(loadTrash());
} else if (processMatch) {
store.dispatch(loadProcess(processMatch.params.id));
+ } else if (processLogMatch) {
+ store.dispatch(loadProcessLog(processLogMatch.params.id));
}
};
export class CommonResourceService<T extends Resource> {
- static mapResponseKeys = (response: any): Promise<any> =>
+ static mapResponseKeys = (response: { data: any }): Promise<any> =>
CommonResourceService.mapKeys(_.camelCase)(response.data)
static mapKeys = (mapFn: (key: string) => string) =>
it("#contents", async () => {
axiosMock
- .onGet("/groups/1/contents/")
+ .onGet("/groups/1/contents")
.reply(200, {
kind: "kind",
offset: 2,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { LogResource } from '~/models/log';
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
+
+export class LogService extends CommonResourceService<LogResource> {
+ constructor(serverApi: AxiosInstance) {
+ super(serverApi, "logs");
+ }
+}
import { ResourceKind } from "~/models/resource";
import { ContainerRequestService } from './container-request-service/container-request-service';
import { ContainerService } from './container-service/container-service';
+import { LogService } from './log-service/log-service';
export type ServiceRepository = ReturnType<typeof createServices>;
const webdavClient = new WebDAV();
webdavClient.defaults.baseURL = config.keepWebServiceUrl;
+ const containerRequestService = new ContainerRequestService(apiClient);
+ const containerService = new ContainerService(apiClient);
const groupsService = new GroupsService(apiClient);
const keepService = new KeepService(apiClient);
const linkService = new LinkService(apiClient);
+ const logService = new LogService(apiClient);
const projectService = new ProjectService(apiClient);
const userService = new UserService(apiClient);
- const containerRequestService = new ContainerRequestService(apiClient);
- const containerService = new ContainerService(apiClient);
const ancestorsService = new AncestorService(groupsService, userService);
const authService = new AuthService(apiClient, config.rootUrl);
groupsService,
keepService,
linkService,
+ logService,
projectService,
tagService,
userService,
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: '',
ownerUuid: '',
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
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+import { ProcessLogs, getProcessLogsPanelCurrentUuid } from './process-logs-panel';
+import { LogEventType } from '~/models/log';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { Dispatch } from 'redux';
+import { groupBy } from 'lodash';
+import { loadProcess } from '~/store/processes/processes-actions';
+import { LogResource } from '~/models/log';
+import { LogService } from '~/services/log-service/log-service';
+import { ResourceEventMessage } from '~/websocket/resource-event-message';
+import { getProcess } from '~/store/processes/process';
+import { FilterBuilder } from "~/services/api/filter-builder";
+import { OrderBuilder } from "~/services/api/order-builder";
+
+export const processLogsPanelActions = unionize({
+ RESET_PROCESS_LOGS_PANEL: ofType<{}>(),
+ INIT_PROCESS_LOGS_PANEL: ofType<{ filters: string[], logs: ProcessLogs }>(),
+ SET_PROCESS_LOGS_PANEL_FILTER: ofType<string>(),
+ ADD_PROCESS_LOGS_PANEL_ITEM: ofType<{ logType: string, log: string }>(),
+});
+
+export type ProcessLogsPanelAction = UnionOf<typeof processLogsPanelActions>;
+
+export const setProcessLogsPanelFilter = (filter: string) =>
+ processLogsPanelActions.SET_PROCESS_LOGS_PANEL_FILTER(filter);
+
+export const initProcessLogsPanel = (processUuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => {
+ dispatch(processLogsPanelActions.RESET_PROCESS_LOGS_PANEL());
+ const process = await dispatch<any>(loadProcess(processUuid));
+ if (process.container) {
+ const logResources = await loadContainerLogs(process.container.uuid, logService);
+ const initialState = createInitialLogPanelState(logResources);
+ dispatch(processLogsPanelActions.INIT_PROCESS_LOGS_PANEL(initialState));
+ }
+ };
+
+export const addProcessLogsPanelItem = (message: ResourceEventMessage<{ text: string }>) =>
+ async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => {
+ if (PROCESS_PANEL_LOG_EVENT_TYPES.indexOf(message.eventType) > -1) {
+ const uuid = getProcessLogsPanelCurrentUuid(getState());
+ if (uuid) {
+ const process = getProcess(uuid)(getState().resources);
+ if (process) {
+ const { containerRequest, container } = process;
+ if (message.objectUuid === containerRequest.uuid
+ || container && message.objectUuid === container.uuid) {
+ dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+ logType: SUMMARIZED_FILTER_TYPE,
+ log: message.properties.text
+ }));
+ dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+ logType: message.eventType,
+ log: message.properties.text
+ }));
+ }
+ }
+ }
+ }
+ };
+
+const loadContainerLogs = async (containerUuid: string, logService: LogService) => {
+ const requestFilters = new FilterBuilder()
+ .addEqual('objectUuid', containerUuid)
+ .addIn('eventType', PROCESS_PANEL_LOG_EVENT_TYPES)
+ .getFilters();
+ const requestOrder = new OrderBuilder<LogResource>()
+ .addAsc('eventAt')
+ .getOrder();
+ const requestParams = {
+ limit: MAX_AMOUNT_OF_LOGS,
+ filters: requestFilters,
+ order: requestOrder,
+ };
+ const { items } = await logService.list(requestParams);
+ return items;
+};
+
+const createInitialLogPanelState = (logResources: LogResource[]) => {
+ const allLogs = logsToLines(logResources);
+ const groupedLogResources = groupBy(logResources, log => log.eventType);
+ const groupedLogs = Object
+ .keys(groupedLogResources)
+ .reduce((grouped, key) => ({
+ ...grouped,
+ [key]: logsToLines(groupedLogResources[key])
+ }), {});
+ const filters = [SUMMARIZED_FILTER_TYPE, ...Object.keys(groupedLogs)];
+ const logs = { [SUMMARIZED_FILTER_TYPE]: allLogs, ...groupedLogs };
+ return { filters, logs };
+};
+
+const logsToLines = (logs: LogResource[]) =>
+ logs.map(({ properties }) => properties.text);
+
+const MAX_AMOUNT_OF_LOGS = 10000;
+
+const SUMMARIZED_FILTER_TYPE = 'Summarized';
+
+const PROCESS_PANEL_LOG_EVENT_TYPES = [
+ LogEventType.ARV_MOUNT,
+ LogEventType.CRUNCH_RUN,
+ LogEventType.CRUNCHSTAT,
+ LogEventType.DISPATCH,
+ LogEventType.HOSTSTAT,
+ LogEventType.NODE_INFO,
+ LogEventType.STDERR,
+ LogEventType.STDOUT,
+];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ProcessLogsPanel } from './process-logs-panel';
+import { RootState } from '~/store/store';
+import { ProcessLogsPanelAction, processLogsPanelActions } from './process-logs-panel-actions';
+
+const initialState: ProcessLogsPanel = {
+ filters: [],
+ selectedFilter: '',
+ logs: { '': [] },
+};
+
+export const processLogsPanelReducer = (state = initialState, action: ProcessLogsPanelAction): ProcessLogsPanel =>
+ processLogsPanelActions.match(action, {
+ RESET_PROCESS_LOGS_PANEL: () => initialState,
+ INIT_PROCESS_LOGS_PANEL: ({ filters, logs }) => ({
+ filters,
+ logs,
+ selectedFilter: filters[0] || '',
+ }),
+ SET_PROCESS_LOGS_PANEL_FILTER: selectedFilter => ({
+ ...state,
+ selectedFilter
+ }),
+ ADD_PROCESS_LOGS_PANEL_ITEM: ({ logType, log }) => {
+ const currentLogs = state.logs[logType] || [];
+ const logsOfType = [...currentLogs, log];
+ const logs = { ...state.logs, [logType]: logsOfType };
+ return { ...state, logs };
+ },
+ default: () => state,
+ });
--- /dev/null
+import { RootState } from '../store';
+import { matchProcessLogRoute } from '~/routes/routes';
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface ProcessLogsPanel {
+ filters: string[];
+ selectedFilter: string;
+ logs: ProcessLogs;
+}
+
+export interface ProcessLogs {
+ [logType: string]: string[];
+}
+
+export const getProcessPanelLogs = ({ selectedFilter, logs }: ProcessLogsPanel) => {
+ return logs[selectedFilter];
+};
+
+export const getProcessLogsPanelCurrentUuid = ({ router }: RootState) => {
+ const pathname = router.location ? router.location.pathname : '';
+ const match = matchProcessLogRoute(pathname);
+ return match ? match.params.id : undefined;
+};
import { ResourcesState, getResource } from '~/store/resources/resources';
import { filterResources } from '../resources/resources';
import { ResourceKind, Resource } from '~/models/resource';
+import { getTimeDiff } from '~/common/formatters';
+import { SubprocessesStatus } from '~/views/process-panel/process-subprocesses-card';
+import { ArvadosTheme } from '~/common/custom-theme';
export interface Process {
containerRequest: ContainerRequestResource;
};
export const getSubprocesses = (uuid: string) => (resources: ResourcesState) => {
- const containerRequests = filterResources(isSubprocess(uuid))(resources) as ContainerRequestResource[];
- return containerRequests.reduce((subprocesses, { uuid }) => {
- const process = getProcess(uuid)(resources);
- return process
- ? [...subprocesses, process]
- : subprocesses;
- }, []);
+ const process = getProcess(uuid)(resources);
+ if (process && process.container) {
+ const containerRequests = filterResources(isSubprocess(process.container.uuid))(resources) as ContainerRequestResource[];
+ return containerRequests.reduce((subprocesses, { uuid }) => {
+ const process = getProcess(uuid)(resources);
+ return process
+ ? [...subprocesses, process]
+ : subprocesses;
+ }, []);
+ }
+ return [];
+};
+
+export const getProcessRuntime = ({ container }: Process) =>
+ container
+ ? getTimeDiff(container.finishedAt || '', container.startedAt || '')
+ : 0;
+
+export const getProcessStatusColor = (status: string, { customs }: ArvadosTheme) => {
+ switch (status) {
+ case SubprocessesStatus.COMPLETED:
+ return customs.colors.green700;
+ case SubprocessesStatus.CANCELED:
+ return customs.colors.red900;
+ case SubprocessesStatus.QUEUED:
+ return customs.colors.grey500;
+ case SubprocessesStatus.FAILED:
+ return customs.colors.red900;
+ case SubprocessesStatus.ACTIVE:
+ return customs.colors.blue500;
+ default:
+ return customs.colors.grey500;
+ }
};
export const getProcessStatus = (process: Process) =>
? process.container.state
: process.containerRequest.state;
-const isSubprocess = (uuid: string) => (resource: Resource) =>
+const isSubprocess = (containerUuid: string) => (resource: Resource) =>
resource.kind === ResourceKind.CONTAINER_REQUEST
- && (resource as ContainerRequestResource).requestingContainerUuid === uuid;
+ && (resource as ContainerRequestResource).requestingContainerUuid === containerUuid;
+
import { updateResources } from '~/store/resources/resources-actions';
import { FilterBuilder } from '~/services/api/filter-builder';
import { ContainerRequestResource } from '../../models/container-request';
+import { Process } from './process';
-export const loadProcess = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const containerRequest = await services.containerRequestService.get(uuid);
+export const loadProcess = (containerRequestUuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
+ const containerRequest = await services.containerRequestService.get(containerRequestUuid);
dispatch<any>(updateResources([containerRequest]));
if (containerRequest.containerUuid) {
const container = await services.containerService.get(containerRequest.containerUuid);
dispatch<any>(updateResources([container]));
+ await dispatch<any>(loadSubprocesses(containerRequest.containerUuid));
+ return { containerRequest, container };
}
+ return { containerRequest };
};
-export const loadSubprocesses = (uuid: string) =>
+export const loadSubprocesses = (containerUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const containerRequests = await dispatch<any>(loadContainerRequests(
- new FilterBuilder().addEqual('requestingContainerUuid', uuid).getFilters()
+ new FilterBuilder().addEqual('requestingContainerUuid', containerUuid).getFilters()
)) as ContainerRequestResource[];
const containerUuids: string[] = containerRequests.reduce((uuids, { containerUuid }) =>
import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service";
import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
+import { processLogsPanelReducer } from './process-logs-panel/process-logs-panel-reducer';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
dialog: dialogReducer,
favorites: favoritesReducer,
form: formReducer,
+ processLogsPanel: processLogsPanelReducer,
properties: propertiesReducer,
resources: resourcesReducer,
router: routerReducer,
import { getProcess } from '../processes/process';
import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions';
export const loadWorkbench = () =>
}
};
+export const loadProcessLog = (uuid: string) =>
+ async (dispatch: Dispatch) => {
+ dispatch<any>(initProcessLogsPanel(uuid));
+ };
+
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: 'monospace'
+ }
+});
+
+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={({ target }) => onChange({ label: target.innerText, value: target.value })}
+ 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';
+import { DefaultView } from '~/components/default-view/default-view';
+
+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>
+ {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
+ </Typography>
+ </Grid>
+ <Grid item xs={12}>
+ <ProcessLogCodeSnippet lines={lines} />
+ </Grid>
+ </Grid>
+ : <DefaultView
+ icon={ProcessIcon}
+ messages={['No logs yet']} />
+ }
+ </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';
+import { getProcessPanelLogs } from '~/store/process-logs-panel/process-logs-panel';
+import { setProcessLogsPanelFilter } from '~/store/process-logs-panel/process-logs-panel-actions';
+import { getProcessLogsPanelCurrentUuid } from '../../store/process-logs-panel/process-logs-panel';
+
+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 = (state: RootState): ProcessLogPanelRootDataProps => {
+ const { resources, processLogsPanel } = state;
+ const uuid = getProcessLogsPanelCurrentUuid(state) || '';
+ return {
+ process: getProcess(uuid)(resources),
+ selectedFilter: { label: processLogsPanel.selectedFilter, value: processLogsPanel.selectedFilter },
+ filters: processLogsPanel.filters.map(filter => ({ label: filter, value: filter })),
+ lines: getProcessPanelLogs(processLogsPanel)
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ProcessLogPanelRootActionProps => ({
+ onContextMenu: (event: React.MouseEvent<HTMLElement>) => {
+ dispatch<any>(openProcessContextMenu(event));
+ },
+ onChange: (filter: FilterOption) => {
+ dispatch(setProcessLogsPanelFilter(filter.value));
+ }
+});
+
+export const ProcessLogPanel = connect(mapStateToProps, mapDispatchToProps)(ProcessLogPanelRoot);
import { ArvadosTheme } from '~/common/custom-theme';
import { MoreOptionsIcon, ProcessIcon } from '~/components/icon/icon';
import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
-import { Process } from '~/store/processes/process';
-import { getProcessStatus } from '../../store/processes/process';
+import { Process, getProcessStatusColor } from '~/store/processes/process';
+import { getProcessStatus } from '~/store/processes/process';
-type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'chip' | 'headerText' | 'link' | 'content' | 'title' | 'avatar';
+
+type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'chip' | 'link' | 'content' | 'title' | 'avatar';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
color: theme.customs.colors.green700,
},
avatar: {
- alignSelf: 'flex-start'
+ alignSelf: 'flex-start',
+ paddingTop: theme.spacing.unit * 0.5
},
label: {
display: 'flex',
chip: {
height: theme.spacing.unit * 3,
width: theme.spacing.unit * 12,
- backgroundColor: theme.customs.colors.green700,
color: theme.palette.common.white,
fontSize: '0.875rem',
borderRadius: theme.spacing.unit * 0.625,
},
- headerText: {
- fontSize: '0.875rem',
- marginLeft: theme.spacing.unit * 3,
- },
content: {
'&:last-child': {
paddingBottom: theme.spacing.unit * 2,
}
},
title: {
- overflow: 'hidden'
+ overflow: 'hidden',
+ paddingTop: theme.spacing.unit * 0.5
}
});
type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles<CssRules>;
-export const ProcessInformationCard = withStyles(styles)(
- ({ classes, process, onContextMenu }: ProcessInformationCardProps) =>
+export const ProcessInformationCard = withStyles(styles, { withTheme: true })(
+ ({ classes, process, onContextMenu, theme }: ProcessInformationCardProps) =>
<Card className={classes.card}>
<CardHeader
classes={{
avatar={<ProcessIcon className={classes.iconHeader} />}
action={
<div>
- <Chip label={getProcessStatus(process)} className={classes.chip} />
+ <Chip label={getProcessStatus(process)}
+ className={classes.chip}
+ style={{ backgroundColor: getProcessStatusColor(getProcessStatus(process), theme as ArvadosTheme) }}/>
<IconButton
aria-label="More options"
onClick={event => onContextMenu(event)}>
</div>
}
title={
- <Tooltip title={process.containerRequest.name}>
+ <Tooltip title={process.containerRequest.name} placement="bottom-start" color='inherit'>
<Typography noWrap variant="title">
{process.containerRequest.name}
</Typography>
import { ProcessIcon } from '~/components/icon/icon';
import { Process } from '~/store/processes/process';
import { SubprocessesCard } from './subprocesses-card';
+import { ProcessSubprocesses } from '~/views/process-panel/process-subprocesses';
+import { SubprocessesStatus } from '~/views/process-panel/process-subprocesses-card';
+
+type CssRules = 'headerActive' | 'headerCompleted' | 'headerQueued' | 'headerFailed' | 'headerCanceled';
export interface ProcessPanelRootDataProps {
process?: Process;
+ subprocesses: Array<Process>;
}
export interface ProcessPanelRootActionProps {
onToggle={() => { return; }}
/>
</Grid>
+ <Grid item xs={12}>
+ <ProcessSubprocesses
+ subprocesses={props.subprocesses}
+ onContextMenu={props.onContextMenu} />
+ </Grid>
</Grid>
: <Grid container
alignItems='center'
- justify='center'>
+ justify='center'
+ style={{ minHeight: '100%' }}>
<DefaultView
icon={ProcessIcon}
messages={['Process not found']} />
import * as React from 'react';
import { RootState } from '~/store/store';
import { connect } from 'react-redux';
-import { getProcess } from '~/store/processes/process';
+import { getProcess, getSubprocesses } from '~/store/processes/process';
import { Dispatch } from 'redux';
import { openProcessContextMenu } from '~/store/context-menu/context-menu-actions';
import { matchProcessRoute } from '~/routes/routes';
const match = matchProcessRoute(pathname);
const uuid = match ? match.params.id : '';
return {
- process: getProcess(uuid)(resources)
+ process: getProcess(uuid)(resources),
+ subprocesses: getSubprocesses(uuid)(resources)
};
};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+ StyleRulesCallback, WithStyles, withStyles, Card,
+ CardHeader, IconButton, CardContent, Typography, Tooltip
+} from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { MoreOptionsIcon } from '~/components/icon/icon';
+import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
+import { Process, getProcessStatus, getProcessRuntime } from '~/store/processes/process';
+import { formatTime } from '~/common/formatters';
+import { getProcessStatusColor } from '~/store/processes/process';
+
+export type CssRules = 'label' | 'value' | 'title' | 'content' | 'action' | 'options' | 'status' | 'rightSideHeader' | 'titleHeader'| 'header';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ label: {
+ fontSize: '0.875rem',
+ },
+ value: {
+ textTransform: 'none',
+ fontSize: '0.875rem',
+ },
+ title: {
+ overflow: 'hidden'
+ },
+ content: {
+ paddingTop: theme.spacing.unit * 0.5,
+ '&:last-child': {
+ paddingBottom: 0
+ }
+ },
+ action: {
+ marginTop: 0
+ },
+ options: {
+ width: theme.spacing.unit * 4,
+ height: theme.spacing.unit * 4,
+ color: theme.palette.common.white,
+ },
+ status: {
+ paddingTop: theme.spacing.unit * 0.5,
+ color: theme.palette.common.white,
+ },
+ rightSideHeader: {
+ display: 'flex'
+ },
+ titleHeader: {
+ color: theme.palette.common.white,
+ fontWeight: 600
+ },
+ header: {
+ paddingTop: 0,
+ paddingBottom: 0,
+ },
+});
+
+export enum SubprocessesStatus {
+ ACTIVE = 'Active',
+ COMPLETED = 'Completed',
+ QUEUED = 'Queued',
+ FAILED = 'Failed',
+ CANCELED = 'Canceled'
+}
+
+export interface SubprocessItemProps {
+ title: string;
+ status: string;
+ runtime?: string;
+}
+
+export interface ProcessSubprocessesCardDataProps {
+ onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+ subprocess: Process;
+}
+
+type ProcessSubprocessesCardProps = ProcessSubprocessesCardDataProps & WithStyles<CssRules>;
+
+export const ProcessSubprocessesCard = withStyles(styles, { withTheme: true })(
+ ({ classes, onContextMenu, subprocess, theme }: ProcessSubprocessesCardProps) => {
+ return <Card>
+ <CardHeader
+ className={classes.header}
+ style={{ backgroundColor: getProcessStatusColor(getProcessStatus(subprocess), theme as ArvadosTheme) }}
+ classes={{ content: classes.title, action: classes.action }}
+ action={
+ <div className={classes.rightSideHeader}>
+ <Typography noWrap variant="body2" className={classes.status}>
+ {getProcessStatus(subprocess)}
+ </Typography>
+ <IconButton
+ className={classes.options}
+ aria-label="More options"
+ onClick={onContextMenu}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </div>
+ }
+ title={
+ <Tooltip title={subprocess.containerRequest.name}>
+ <Typography noWrap variant="body2" 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))} />
+ </CardContent>
+ </Card>;
+ });
\ 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 { ProcessSubprocessesCard } from '~/views/process-panel/process-subprocesses-card';
+import { Process } from '~/store/processes/process';
+
+export interface ProcessSubprocessesDataProps {
+ subprocesses: Array<Process>;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
+}
+
+export const ProcessSubprocesses = ({ onContextMenu, subprocesses }: ProcessSubprocessesDataProps) => {
+ return <Grid container spacing={16}>
+ {subprocesses.map(subprocess =>
+ <Grid item xs={2} key={subprocess.containerRequest.uuid}>
+ <ProcessSubprocessesCard onContextMenu={onContextMenu} subprocess={subprocess}/>
+ </Grid>
+ )}
+ </Grid>;
+};
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.FAVORITES} component={FavoritePanel} />
<Route path={Routes.PROCESSES} component={ProcessPanel} />
<Route path={Routes.TRASH} component={TrashPanel} />
+ <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
</Switch>
</div>
{user && <DetailsPanel />}
--- /dev/null
+import { LogEventType } from '../models/log';
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface ResourceEventMessage<Properties = {}> {
+ eventAt: string;
+ eventType: LogEventType;
+ id: string;
+ msgID: string;
+ objectKind: string;
+ objectOwnerUuid: string;
+ objectUuid: string;
+ properties: Properties;
+ uuid: string;
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AuthService } from '~/services/auth-service/auth-service';
+import { ResourceEventMessage } from './resource-event-message';
+import { camelCase } from 'lodash';
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
+
+type MessageListener = (message: ResourceEventMessage) => void;
+
+export class WebSocketService {
+ private ws: WebSocket;
+ private messageListener: MessageListener;
+
+ constructor(private url: string, private authService: AuthService) { }
+
+ connect() {
+ if (this.ws) {
+ this.ws.close();
+ }
+ this.ws = new WebSocket(this.getUrl());
+ this.ws.addEventListener('message', this.handleMessage);
+ this.ws.addEventListener('open', this.handleOpen);
+ }
+
+ setMessageListener = (listener: MessageListener) => {
+ this.messageListener = listener;
+ }
+
+ private getUrl() {
+ return `${this.url}?api_token=${this.authService.getApiToken()}`;
+ }
+
+ private handleMessage = (event: MessageEvent) => {
+ if (this.messageListener) {
+ const data = JSON.parse(event.data);
+ const message = CommonResourceService.mapKeys(camelCase)(data);
+ this.messageListener(message);
+ }
+ }
+
+ private handleOpen = () => {
+ this.ws.send('{"method":"subscribe"}');
+ }
+
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootStore } from '~/store/store';
+import { AuthService } from '~/services/auth-service/auth-service';
+import { Config } from '~/common/config';
+import { WebSocketService } from './websocket-service';
+import { ResourceEventMessage } from './resource-event-message';
+import { ResourceKind } from '~/models/resource';
+import { loadProcess } from '~/store/processes/processes-actions';
+import { loadContainers } from '~/store/processes/processes-actions';
+import { LogEventType } from '~/models/log';
+import { addProcessLogsPanelItem } from '../store/process-logs-panel/process-logs-panel-actions';
+import { FilterBuilder } from "~/services/api/filter-builder";
+
+export const initWebSocket = (config: Config, authService: AuthService, store: RootStore) => {
+ const webSocketService = new WebSocketService(config.websocketUrl, authService);
+ webSocketService.setMessageListener(messageListener(store));
+ webSocketService.connect();
+};
+
+const messageListener = (store: RootStore) => (message: ResourceEventMessage) => {
+ if (message.eventType === LogEventType.CREATE || message.eventType === LogEventType.UPDATE) {
+ switch (message.objectKind) {
+ case ResourceKind.CONTAINER_REQUEST:
+ return store.dispatch(loadProcess(message.objectUuid));
+ case ResourceKind.CONTAINER:
+ return store.dispatch(loadContainers(
+ new FilterBuilder().addIn('uuid', [message.objectUuid]).getFilters()
+ ));
+ default:
+ return;
+ }
+ } else {
+ return store.dispatch(addProcessLogsPanelItem(message as ResourceEventMessage<{text: string}>));
+ }
+};