21720: replaced theme.spacing.unit * x with theme.spacing(x) everywhere
[arvados.git] / services / workbench2 / 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 { CustomStyleRulesCallback } from 'common/custom-theme';
7 import {
8     MuiThemeProvider,
9     createMuiTheme,
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 { navigationNotAvailable } from 'store/navigation/navigation-action';
17 import { Dispatch } from 'redux';
18 import { connect, DispatchProp } from 'react-redux';
19 import classNames from 'classnames';
20 import { FederationConfig, getNavUrl } from 'routes/routes';
21 import { RootState } from 'store/store';
22
23 type CssRules = 'root' | 'wordWrapOn' | 'wordWrapOff' | 'logText';
24
25 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
26     root: {
27         boxSizing: 'border-box',
28         overflow: 'auto',
29         backgroundColor: '#000',
30         height: `calc(100% - ${theme.spacing(4)}px)`, // so that horizontal scollbar is visible
31         "& a": {
32             color: theme.palette.primary.main,
33         },
34     },
35     logText: {
36         padding: `0 ${theme.spacing(0.5)}px`,
37     },
38     wordWrapOn: {
39         overflowWrap: 'anywhere',
40     },
41     wordWrapOff: {
42         whiteSpace: 'nowrap',
43     },
44 });
45
46 const theme = createMuiTheme({
47     overrides: {
48         MuiTypography: {
49             body2: {
50                 color: grey["200"]
51             }
52         }
53     },
54     typography: {
55         fontFamily: 'monospace',
56     }
57 });
58
59 interface ProcessLogCodeSnippetProps {
60     lines: string[];
61     fontSize: number;
62     wordWrap?: boolean;
63 }
64
65 interface ProcessLogCodeSnippetAuthProps {
66     auth: FederationConfig;
67 }
68
69 const renderLinks = (fontSize: number, auth: FederationConfig, dispatch: Dispatch) => (text: string) => {
70     // Matches UUIDs & PDHs
71     const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
72     const links = text.match(REGEX);
73     if (!links) {
74         return <Typography style={{ fontSize: fontSize }}>{text}</Typography>;
75     }
76     return <Typography style={{ fontSize: fontSize }}>
77         {text.split(REGEX).map((part, index) =>
78             <React.Fragment key={index}>
79                 {part}
80                 {links[index] &&
81                     <Link onClick={() => {
82                         const url = getNavUrl(links[index], auth)
83                         if (url) {
84                             window.open(`${window.location.origin}${url}`, '_blank', "noopener");
85                         } else {
86                             dispatch(navigationNotAvailable(links[index]));
87                         }
88                     }}
89                         style={{ cursor: 'pointer' }}>
90                         {links[index]}
91                     </Link>}
92             </React.Fragment>
93         )}
94     </Typography>;
95 };
96
97 const mapStateToProps = (state: RootState): ProcessLogCodeSnippetAuthProps => ({
98     auth: state.auth,
99 });
100
101 export const ProcessLogCodeSnippet = withStyles(styles)(connect(mapStateToProps)(
102     ({ classes, lines, fontSize, auth, dispatch, wordWrap }: ProcessLogCodeSnippetProps & WithStyles<CssRules> & ProcessLogCodeSnippetAuthProps & DispatchProp) => {
103         const [followMode, setFollowMode] = useState<boolean>(true);
104         const scrollRef = useRef<HTMLDivElement>(null);
105
106         useEffect(() => {
107             if (followMode && scrollRef.current && lines.length > 0) {
108                 // Scroll to bottom
109                 scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
110             }
111         }, [followMode, lines, scrollRef]);
112
113         return <MuiThemeProvider theme={theme}>
114             <div ref={scrollRef} className={classes.root}
115                 onScroll={(e) => {
116                     const elem = e.target as HTMLDivElement;
117                     if (elem.scrollTop + (elem.clientHeight * 1.1) >= elem.scrollHeight) {
118                         setFollowMode(true);
119                     } else {
120                         setFollowMode(false);
121                     }
122                 }}>
123                 {lines.map((line: string, index: number) =>
124                     <Typography key={index} component="span"
125                         className={classNames(classes.logText, wordWrap ? classes.wordWrapOn : classes.wordWrapOff)}>
126                         {renderLinks(fontSize, auth, dispatch)(line)}
127                     </Typography>
128                 )}
129             </div>
130         </MuiThemeProvider>
131     }));