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