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