Merge remote-tracking branch 'origin/main' into 19051-handle-quotes-in-search
[arvados-workbench2.git] / src / views / process-panel / process-log-code-snippet.tsx
index 1ea839122125c6ba8b270a7448776e9519120b9e..2b7391c294ec0ac2ffd120fac25085b00b574e5c 100644 (file)
@@ -2,21 +2,42 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React from 'react';
-import { MuiThemeProvider, createMuiTheme, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
-import { CodeSnippet } from 'components/code-snippet/code-snippet';
+import React, { useEffect, useRef, useState } from 'react';
+import {
+    MuiThemeProvider,
+    createMuiTheme,
+    StyleRulesCallback,
+    withStyles,
+    WithStyles
+} from '@material-ui/core/styles';
 import grey from '@material-ui/core/colors/grey';
 import { ArvadosTheme } from 'common/custom-theme';
+import { Link, Typography } from '@material-ui/core';
+import { navigationNotAvailable } from 'store/navigation/navigation-action';
+import { Dispatch } from 'redux';
+import { connect, DispatchProp } from 'react-redux';
+import classNames from 'classnames';
+import { FederationConfig, getNavUrl } from 'routes/routes';
+import { RootState } from 'store/store';
 
-type CssRules = 'wordWrap' | 'codeSnippetContainer';
+type CssRules = 'root' | 'wordWrap' | 'logText';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        boxSizing: 'border-box',
+        overflow: 'auto',
+        backgroundColor: '#000',
+        height: `calc(100% - ${theme.spacing.unit * 4}px)`, // so that horizontal scollbar is visible
+        "& a": {
+            color: theme.palette.primary.main,
+        },
+    },
+    logText: {
+        padding: theme.spacing.unit * 0.5,
+    },
     wordWrap: {
         whiteSpace: 'pre-wrap',
     },
-    codeSnippetContainer: {
-        height: `calc(100% - ${theme.spacing.unit * 4}px)`, // so that horizontal scollbar is visible
-    },
 });
 
 const theme = createMuiTheme({
@@ -24,9 +45,6 @@ const theme = createMuiTheme({
         MuiTypography: {
             body2: {
                 color: grey["200"]
-            },
-            root: {
-                backgroundColor: '#000'
             }
         }
     },
@@ -42,10 +60,70 @@ interface ProcessLogCodeSnippetProps {
     wordWrap?: boolean;
 }
 
-export const ProcessLogCodeSnippet = withStyles(styles)(
-    (props: ProcessLogCodeSnippetProps & WithStyles<CssRules>) =>
-        <MuiThemeProvider theme={theme}>
-            <CodeSnippet lines={props.lines} fontSize={props.fontSize}
-                className={props.wordWrap ? props.classes.wordWrap : undefined}
-                containerClassName={props.classes.codeSnippetContainer} />
-        </MuiThemeProvider>);
\ No newline at end of file
+interface ProcessLogCodeSnippetAuthProps {
+    auth: FederationConfig;
+}
+
+const renderLinks = (fontSize: number, auth: FederationConfig, dispatch: Dispatch) => (text: string) => {
+    // Matches UUIDs & PDHs
+    const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
+    const links = text.match(REGEX);
+    if (!links) {
+        return <Typography style={{ fontSize: fontSize }}>{text}</Typography>;
+    }
+    return <Typography style={{ fontSize: fontSize }}>
+        {text.split(REGEX).map((part, index) =>
+        <React.Fragment key={index}>
+            {part}
+            {links[index] &&
+            <Link onClick={() => {
+                const url = getNavUrl(links[index], auth)
+                if (url) {
+                    window.open(`${window.location.origin}${url}`, '_blank');
+                } else {
+                    dispatch(navigationNotAvailable(links[index]));
+                }
+            }}
+                style={ {cursor: 'pointer'} }>
+                {links[index]}
+            </Link>}
+        </React.Fragment>
+        )}
+    </Typography>;
+};
+
+const mapStateToProps = (state: RootState): ProcessLogCodeSnippetAuthProps => ({
+    auth: state.auth,
+});
+
+export const ProcessLogCodeSnippet = withStyles(styles)(connect(mapStateToProps)(
+    ({classes, lines, fontSize, auth, dispatch, wordWrap}: ProcessLogCodeSnippetProps & WithStyles<CssRules> & ProcessLogCodeSnippetAuthProps & DispatchProp) => {
+        const [followMode, setFollowMode] = useState<boolean>(true);
+        const scrollRef = useRef<HTMLDivElement>(null);
+
+        useEffect(() => {
+            if (followMode && scrollRef.current && lines.length > 0) {
+                // Scroll to bottom
+                scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+            }
+        }, [followMode, lines, scrollRef]);
+
+        return <MuiThemeProvider theme={theme}>
+            <div ref={scrollRef} className={classes.root}
+                onScroll={(e) => {
+                    const elem = e.target as HTMLDivElement;
+                    if (elem.scrollTop + (elem.clientHeight*1.1) >= elem.scrollHeight) {
+                        setFollowMode(true);
+                    } else {
+                        setFollowMode(false);
+                    }
+                }}>
+                { lines.map((line: string, index: number) =>
+                <Typography key={index} component="pre"
+                    className={classNames(classes.logText, wordWrap ? classes.wordWrap : undefined)}>
+                    {renderLinks(fontSize, auth, dispatch)(line)}
+                </Typography>
+                ) }
+            </div>
+        </MuiThemeProvider>
+    }));
\ No newline at end of file