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) =>
--- /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: '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["200"]
+ },
+ root: {
+ backgroundColor: '#000'
+ }
+ }
+ },
+ 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 { addRouteChangeHandlers } from './routes/routes';
import { loadWorkbench } from './store/workbench/workbench-actions';
import { Routes } from '~/routes/routes';
+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);
}
};
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 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 { FilterBuilder } from '~/common/api/filter-builder';
import { ContainerRequestResource } from '../../models/container-request';
-export const loadProcess = (uuid: string) =>
+export const loadProcess = (containerRequestUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const containerRequest = await services.containerRequestService.get(uuid);
+ 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));
}
};
-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 }) =>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export enum ResourceEventMessageType {
+ CREATE = 'create',
+ UPDATE = 'update',
+ HOTSTAT = 'hotstat',
+ CRUNCH_RUN = 'crunch-run',
+ NODE_INFO = 'node-info',
+}
+
+export interface ResourceEventMessage {
+ eventAt: string;
+ eventType: ResourceEventMessageType;
+ id: string;
+ msgID: string;
+ objectKind: string;
+ objectOwnerUuid: string;
+ objectUuid: string;
+ 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 { CommonResourceService } from '~/common/api/common-resource-service';
+import { camelCase } from 'lodash';
+
+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, ResourceEventMessageType } from './resource-event-message';
+import { ResourceKind } from '~/models/resource';
+import { loadProcess } from '~/store/processes/processes-actions';
+import { loadContainers } from '../store/processes/processes-actions';
+import { FilterBuilder } from '~/common/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 === ResourceEventMessageType.CREATE || message.eventType === ResourceEventMessageType.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;
+ }
+ }
+ return ;
+};