1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React, { useEffect, useRef, useState } from 'react';
6 import { CustomStyleRulesCallback } from 'common/custom-theme';
7 import { ThemeProvider, Theme, StyledEngineProvider, createTheme, adaptV4Theme } 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';
21 declare module '@mui/styles/defaultTheme' {
22 // eslint-disable-next-line @typescript-eslint/no-empty-interface
23 interface DefaultTheme extends Theme {}
27 type CssRules = 'root' | 'wordWrapOn' | 'wordWrapOff' | 'logText';
29 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
31 boxSizing: 'border-box',
33 backgroundColor: '#000',
34 height: `calc(100% - ${theme.spacing(4)})`, // so that horizontal scollbar is visible
36 color: theme.palette.primary.main,
41 paddingY: theme.spacing(0.5),
44 overflowWrap: 'anywhere',
51 const theme = createTheme(adaptV4Theme({
60 fontFamily: 'monospace',
64 interface ProcessLogCodeSnippetProps {
70 interface ProcessLogCodeSnippetAuthProps {
71 auth: FederationConfig;
74 const renderLinks = (fontSize: number, auth: FederationConfig, dispatch: Dispatch) => (text: string) => {
75 // Matches UUIDs & PDHs
76 const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
77 const links = text.match(REGEX);
79 return <Typography style={{ fontSize: fontSize }}>{text}</Typography>;
81 return <Typography style={{ fontSize: fontSize }}>
82 {text.split(REGEX).map((part, index) =>
83 <React.Fragment key={index}>
86 <Link onClick={() => {
87 const url = getNavUrl(links[index], auth)
89 window.open(`${window.location.origin}${url}`, '_blank', "noopener");
91 dispatch(navigationNotAvailable(links[index]));
94 style={{ cursor: 'pointer' }}>
102 const mapStateToProps = (state: RootState): ProcessLogCodeSnippetAuthProps => ({
106 export const ProcessLogCodeSnippet = withStyles(styles)(connect(mapStateToProps)(
107 ({ classes, lines, fontSize, auth, dispatch, wordWrap }: ProcessLogCodeSnippetProps & WithStyles<CssRules> & ProcessLogCodeSnippetAuthProps & DispatchProp) => {
108 const [followMode, setFollowMode] = useState<boolean>(true);
109 const scrollRef = useRef<HTMLDivElement>(null);
112 if (followMode && scrollRef.current && lines.length > 0) {
114 scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
116 }, [followMode, lines, scrollRef]);
119 <StyledEngineProvider injectFirst>
120 <ThemeProvider theme={theme}>
121 <div ref={scrollRef} className={classes.root}
123 const elem = e.target as HTMLDivElement;
124 if (elem.scrollTop + (elem.clientHeight * 1.1) >= elem.scrollHeight) {
127 setFollowMode(false);
130 {lines.map((line: string, index: number) =>
131 <Typography key={index} component="span"
132 className={classNames(classes.logText, wordWrap ? classes.wordWrapOn : classes.wordWrapOff)}>
133 {renderLinks(fontSize, auth, dispatch)(line)}
138 </StyledEngineProvider>