Merge branch 'master' into 14098-log-view
authorJanicki Artur <artur.janicki@contractors.roche.com>
Fri, 31 Aug 2018 13:52:56 +0000 (15:52 +0200)
committerJanicki Artur <artur.janicki@contractors.roche.com>
Fri, 31 Aug 2018 13:52:56 +0000 (15:52 +0200)
refs #14098

Arvados-DCO-1.1-Signed-off-by: Janicki Artur <artur.janicki@contractors.roche.com>

src/common/api/common-resource-service.ts
src/components/code-snippet/code-snippet.tsx [new file with mode: 0644]
src/components/default-code-snippet/default-code-snippet.tsx [new file with mode: 0644]
src/index.tsx
src/store/processes/process.ts
src/store/processes/processes-actions.ts
src/websocket/resource-event-message.ts [new file with mode: 0644]
src/websocket/websocket-service.ts [new file with mode: 0644]
src/websocket/websocket.ts [new file with mode: 0644]

index 2c9bfb51679f16bef721b4499f0cf00181ddc3f4..8c4b65d14866caf2b31b6479216819645bb299a4 100644 (file)
@@ -38,7 +38,7 @@ export enum CommonResourceServiceError {
 
 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) =>
diff --git a/src/components/code-snippet/code-snippet.tsx b/src/components/code-snippet/code-snippet.tsx
new file mode 100644 (file)
index 0000000..bf9b612
--- /dev/null
@@ -0,0 +1,36 @@
+// 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
diff --git a/src/components/default-code-snippet/default-code-snippet.tsx b/src/components/default-code-snippet/default-code-snippet.tsx
new file mode 100644 (file)
index 0000000..474c38a
--- /dev/null
@@ -0,0 +1,31 @@
+// 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
index d3115a6754bf70feb6731e8910e71b04859534f4..20d2c1e9e1f1fc2bf0ee31023a58583e92dc5988 100644 (file)
@@ -31,6 +31,9 @@ import { processActionSet } from './views-components/context-menu/action-sets/pr
 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);
@@ -56,8 +59,7 @@ fetchConfig()
         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} />;
@@ -83,12 +85,13 @@ fetchConfig()
 
     });
 
-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);
         }
index 46d8a257172ec011e3fd163a61d96309cf3f7b37..0f12b3fe2f30c3f9821292b946ee0fbe2c74509b 100644 (file)
@@ -28,13 +28,17 @@ export const getProcess = (uuid: string) => (resources: ResourcesState): Process
 };
 
 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) =>
@@ -42,6 +46,6 @@ 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;
index d667517191179cca2c020df3b0507934d76e5d78..be9266b64ec2cb9058ec5c23f8bea6fae2aa3af6 100644 (file)
@@ -9,20 +9,21 @@ import { updateResources } from '~/store/resources/resources-actions';
 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 }) =>
diff --git a/src/websocket/resource-event-message.ts b/src/websocket/resource-event-message.ts
new file mode 100644 (file)
index 0000000..274b2e1
--- /dev/null
@@ -0,0 +1,23 @@
+// 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;
+}
diff --git a/src/websocket/websocket-service.ts b/src/websocket/websocket-service.ts
new file mode 100644 (file)
index 0000000..77c1fd3
--- /dev/null
@@ -0,0 +1,47 @@
+// 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"}');
+    }
+
+}
diff --git a/src/websocket/websocket.ts b/src/websocket/websocket.ts
new file mode 100644 (file)
index 0000000..d7072d7
--- /dev/null
@@ -0,0 +1,35 @@
+// 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 ;
+};