Merge branch 'main' into 21720-material-ui-upgrade
[arvados.git] / services / workbench2 / src / components / code-snippet / code-snippet.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { CustomStyleRulesCallback } from 'common/custom-theme';
7 import { Typography, Link } from '@mui/material';
8 import { WithStyles } from '@mui/styles';
9 import withStyles from '@mui/styles/withStyles';
10 import { ArvadosTheme } from 'common/custom-theme';
11 import classNames from 'classnames';
12 import { connect, DispatchProp } from 'react-redux';
13 import { RootState } from 'store/store';
14 import { FederationConfig, getNavUrl } from 'routes/routes';
15 import { Dispatch } from 'redux';
16 import { navigationNotAvailable } from 'store/navigation/navigation-action';
17
18 type CssRules = 'root' | 'inlineRoot' | 'space' | 'inline';
19
20 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
21     root: {
22         boxSizing: 'border-box',
23         overflow: 'auto',
24         padding: theme.spacing(1),
25     },
26     inlineRoot: {
27         padding: "3px",
28         display: "inline",
29     },
30     space: {
31         marginLeft: '15px',
32     },
33     inline: {
34         display: 'inline',
35     },
36 });
37
38 export interface CodeSnippetDataProps {
39     lines: string[];
40     className?: string;
41     apiResponse?: boolean;
42     linked?: boolean;
43     children?: JSX.Element;
44     inline?: boolean;
45 }
46
47 interface CodeSnippetAuthProps {
48     auth: FederationConfig;
49 }
50
51 type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
52
53 const mapStateToProps = (state: RootState): CodeSnippetAuthProps => ({
54     auth: state.auth,
55 });
56
57 export const CodeSnippet = withStyles(styles)(connect(mapStateToProps)(
58     ({ classes, lines, linked, className, apiResponse, dispatch, auth, children, inline }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
59         <Typography
60             component="div"
61             className={classNames([classes.root, className, inline ? classes.inlineRoot : undefined])}>
62             <Typography className={apiResponse ? classes.space : classNames([className, inline ? classes.inline : undefined])} component="pre">
63                 {children}
64                 {linked ?
65                     lines.map((line, index) => <React.Fragment key={index}>{renderLinks(auth, dispatch)(line)}{`\n`}</React.Fragment>) :
66                     lines.join('\n')
67                 }
68             </Typography>
69         </Typography>
70 ));
71
72 export const renderLinks = (auth: FederationConfig, dispatch: Dispatch) => (text: string): JSX.Element => {
73     // Matches UUIDs & PDHs
74     const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
75     const links = text.match(REGEX);
76     if (!links) {
77         return <>{text}</>;
78     }
79     return <>
80         {text.split(REGEX).map((part, index) =>
81             <React.Fragment key={index}>
82                 {part}
83                 {links[index] &&
84                     <Link onClick={() => {
85                         const url = getNavUrl(links[index], auth)
86                         if (url) {
87                             window.open(`${window.location.origin}${url}`, '_blank', "noopener");
88                         } else {
89                             dispatch(navigationNotAvailable(links[index]));
90                         }
91                     }}
92                         style={{ cursor: 'pointer' }}>
93                         {links[index]}
94                     </Link>}
95             </React.Fragment>
96         )}
97     </>;
98 };