Merge branch '21846-rightclick-context-menu'
[arvados.git] / services / workbench2 / src / components / details-attribute / details-attribute.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 { connect, DispatchProp } from 'react-redux';
7 import Typography from '@material-ui/core/Typography';
8 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
9 import { Tooltip } from '@material-ui/core';
10 import { CopyIcon } from 'components/icon/icon';
11 import CopyToClipboard from 'react-copy-to-clipboard';
12 import { ArvadosTheme } from 'common/custom-theme';
13 import classnames from "classnames";
14 import { Link } from 'react-router-dom';
15 import { RootState } from "store/store";
16 import { FederationConfig, getNavUrl } from "routes/routes";
17 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
18
19 type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link' | 'copyIcon';
20
21 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
22     attribute: {
23         marginBottom: ".6 rem"
24     },
25     label: {
26         boxSizing: 'border-box',
27         color: theme.palette.grey["600"],
28         width: '100%',
29         marginTop: "0.4em",
30     },
31     value: {
32         boxSizing: 'border-box',
33         alignItems: 'flex-start'
34     },
35     lowercaseValue: {
36         textTransform: 'lowercase'
37     },
38     link: {
39         color: theme.palette.primary.main,
40         textDecoration: 'none',
41         overflowWrap: 'break-word',
42         cursor: 'pointer'
43     },
44     copyIcon: {
45         marginLeft: theme.spacing.unit,
46         color: theme.palette.grey["600"],
47         cursor: 'pointer',
48         display: 'inline',
49         '& svg': {
50             fontSize: '1rem'
51         }
52     }
53 });
54
55 interface DetailsAttributeDataProps {
56     label: string;
57     classLabel?: string;
58     value?: React.ReactNode;
59     classValue?: string;
60     lowercaseValue?: boolean;
61     link?: string;
62     children?: React.ReactNode;
63     onValueClick?: () => void;
64     linkToUuid?: string;
65     copyValue?: string;
66     uuidEnhancer?: Function;
67 }
68
69 type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules> & FederationConfig & DispatchProp;
70
71 const mapStateToProps = ({ auth }: RootState): FederationConfig => ({
72     localCluster: auth.localCluster,
73     remoteHostsConfig: auth.remoteHostsConfig,
74     sessions: auth.sessions
75 });
76
77 export const DetailsAttribute = connect(mapStateToProps)(withStyles(styles)(
78     class extends React.Component<DetailsAttributeProps> {
79
80         onCopy = (message: string) => {
81             this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
82                 message,
83                 hideDuration: 2000,
84                 kind: SnackbarKind.SUCCESS
85             }));
86         }
87
88         render() {
89             const { uuidEnhancer, link, value, classes, linkToUuid,
90                 localCluster, remoteHostsConfig, sessions } = this.props;
91             let valueNode: React.ReactNode;
92
93             if (linkToUuid) {
94                 const uuid = uuidEnhancer ? uuidEnhancer(linkToUuid) : linkToUuid;
95                 const linkUrl = getNavUrl(linkToUuid || "", { localCluster, remoteHostsConfig, sessions });
96                 if (linkUrl[0] === '/') {
97                     valueNode = <Link to={linkUrl} className={classes.link}>{uuid}</Link>;
98                 } else {
99                     valueNode = <a href={linkUrl} className={classes.link} target='_blank' rel="noopener noreferrer">{uuid}</a>;
100                 }
101             } else if (link) {
102                 valueNode = <a href={link} className={classes.link} target='_blank' rel="noopener noreferrer">{value}</a>;
103             } else {
104                 valueNode = value;
105             }
106
107             return <DetailsAttributeComponent {...this.props} value={valueNode} onCopy={this.onCopy} />;
108         }
109     }
110 ));
111
112 interface DetailsAttributeComponentProps {
113     value: React.ReactNode;
114     onCopy?: (msg: string) => void;
115 }
116
117 export const DetailsAttributeComponent = withStyles(styles)(
118     (props: DetailsAttributeDataProps & WithStyles<CssRules> & DetailsAttributeComponentProps) =>
119         <Typography component="div" className={props.classes.attribute} data-cy={`details-panel-${props.label.toLowerCase()}`}>
120             <Typography component="div" className={classnames([props.classes.label, props.classLabel])}>{props.label}</Typography>
121             <Typography
122                 onClick={props.onValueClick}
123                 component="div"
124                 className={classnames([props.classes.value, props.classValue, { [props.classes.lowercaseValue]: props.lowercaseValue }])}>
125                 {props.value}
126                 {props.children}
127                 {(props.linkToUuid || props.copyValue) && props.onCopy && <Tooltip title="Copy link to clipboard">
128                     <span className={props.classes.copyIcon}>
129                         <CopyToClipboard text={props.linkToUuid || props.copyValue || ""} onCopy={() => props.onCopy!("Copied")}>
130                             <CopyIcon />
131                         </CopyToClipboard>
132                     </span>
133                 </Tooltip>}
134             </Typography>
135         </Typography>);