92e4ffba02b4cbf53d158c4cef627f3a08960c10
[arvados-workbench2.git] / src / views / process-panel / process-log-code-snippet.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React, { useEffect, useRef, useState } from 'react';
6 import {
7     MuiThemeProvider,
8     createMuiTheme,
9     StyleRulesCallback,
10     withStyles,
11     WithStyles
12 } from '@material-ui/core/styles';
13 import grey from '@material-ui/core/colors/grey';
14 import { ArvadosTheme } from 'common/custom-theme';
15 import { Link, Typography } from '@material-ui/core';
16 import { navigateTo } from 'store/navigation/navigation-action';
17 import { Dispatch } from 'redux';
18 import { connect, DispatchProp } from 'react-redux';
19 import classNames from 'classnames';
20
21 type CssRules = 'root' | 'wordWrap' | 'logText';
22
23 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
24     root: {
25         boxSizing: 'border-box',
26         overflow: 'auto',
27         backgroundColor: '#000',
28         height: `calc(100% - ${theme.spacing.unit * 4}px)`, // so that horizontal scollbar is visible
29         "& a": {
30             color: theme.palette.primary.main,
31         },
32     },
33     logText: {
34         padding: theme.spacing.unit * 0.5,
35     },
36     wordWrap: {
37         whiteSpace: 'pre-wrap',
38     },
39 });
40
41 const theme = createMuiTheme({
42     overrides: {
43         MuiTypography: {
44             body2: {
45                 color: grey["200"]
46             }
47         }
48     },
49     typography: {
50         fontFamily: 'monospace',
51         useNextVariants: true,
52     }
53 });
54
55 interface ProcessLogCodeSnippetProps {
56     lines: string[];
57     fontSize: number;
58     wordWrap?: boolean;
59 }
60
61 const renderLinks = (fontSize: number, dispatch: Dispatch) => (text: string) => {
62     // Matches UUIDs & PDHs
63     const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
64     const links = text.match(REGEX);
65     if (!links) {
66         return <Typography style={{ fontSize: fontSize }}>{text}</Typography>;
67     }
68     return <Typography style={{ fontSize: fontSize }}>
69         {text.split(REGEX).map((part, index) =>
70         <React.Fragment key={index}>
71             {part}
72             {links[index] &&
73             <Link onClick={() => dispatch<any>(navigateTo(links[index]))}
74                 style={ {cursor: 'pointer'} }>
75                 {links[index]}
76             </Link>}
77         </React.Fragment>
78         )}
79     </Typography>;
80 };
81
82 export const ProcessLogCodeSnippet = withStyles(styles)(connect()(
83     ({classes, lines, fontSize, dispatch, wordWrap}: ProcessLogCodeSnippetProps & WithStyles<CssRules> & DispatchProp) => {
84         const [followMode, setFollowMode] = useState<boolean>(true);
85         const scrollRef = useRef<HTMLDivElement>(null);
86
87         useEffect(() => {
88             if (followMode && scrollRef.current && lines.length > 0) {
89                 // Scroll to bottom
90                 scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
91             }
92         }, [followMode, lines, scrollRef]);
93
94         return <MuiThemeProvider theme={theme}>
95             <div ref={scrollRef} className={classes.root}
96                 onScroll={(e) => {
97                     const elem = e.target as HTMLDivElement;
98                     if (elem.scrollTop + (elem.clientHeight*1.1) >= elem.scrollHeight) {
99                         setFollowMode(true);
100                     } else {
101                         setFollowMode(false);
102                     }
103                 }}>
104                 { lines.map((line: string, index: number) =>
105                 <Typography key={index} component="pre"
106                     className={classNames(classes.logText, wordWrap ? classes.wordWrap : undefined)}>
107                     {renderLinks(fontSize, dispatch)(line)}
108                 </Typography>
109                 ) }
110             </div>
111         </MuiThemeProvider>
112     }));