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