Merge branch '21128-toolbar-context-menu'
[arvados-workbench2.git] / src / views-components / snackbar / snackbar.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 { Dispatch } from "redux";
7 import { connect } from "react-redux";
8 import { RootState } from "store/store";
9 import { Button, IconButton, StyleRulesCallback, WithStyles, withStyles, SnackbarContent } from '@material-ui/core';
10 import MaterialSnackbar, { SnackbarOrigin } from "@material-ui/core/Snackbar";
11 import { snackbarActions, SnackbarKind, SnackbarMessage } from "store/snackbar/snackbar-actions";
12 import { navigateTo } from 'store/navigation/navigation-action';
13 import WarningIcon from '@material-ui/icons/Warning';
14 import CheckCircleIcon from '@material-ui/icons/CheckCircle';
15 import ErrorIcon from '@material-ui/icons/Error';
16 import InfoIcon from '@material-ui/icons/Info';
17 import CloseIcon from '@material-ui/icons/Close';
18 import { ArvadosTheme } from "common/custom-theme";
19 import { amber, green } from "@material-ui/core/colors";
20 import classNames from 'classnames';
21
22 interface SnackbarDataProps {
23     anchorOrigin?: SnackbarOrigin;
24     autoHideDuration?: number;
25     open: boolean;
26     messages: SnackbarMessage[];
27 }
28
29 interface SnackbarEventProps {
30     onClose?: (event: React.SyntheticEvent<any>, reason: string, message?: string) => void;
31     onExited: () => void;
32     onClick: (uuid: string) => void;
33 }
34
35 const mapStateToProps = (state: RootState): SnackbarDataProps => {
36     const messages = state.snackbar.messages;
37     return {
38         anchorOrigin: { vertical: "bottom", horizontal: "right" },
39         open: state.snackbar.open,
40         messages,
41         autoHideDuration: messages.length > 0 ? messages[0].hideDuration : 0
42     };
43 };
44
45 const mapDispatchToProps = (dispatch: Dispatch): SnackbarEventProps => ({
46     onClose: (event: any, reason: string, id: undefined) => {
47         if (reason !== "clickaway") {
48             dispatch(snackbarActions.CLOSE_SNACKBAR(id));
49         }
50     },
51     onExited: () => {
52         dispatch(snackbarActions.SHIFT_MESSAGES());
53     },
54     onClick: (uuid: string) => {
55         dispatch<any>(navigateTo(uuid));
56     }
57 });
58
59 type CssRules = "success" | "error" | "info" | "warning" | "icon" | "iconVariant" | "message" | "linkButton" | "snackbarContent";
60
61 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
62     success: {
63         backgroundColor: green[600]
64     },
65     error: {
66         backgroundColor: theme.palette.error.dark
67     },
68     info: {
69         backgroundColor: theme.palette.primary.main
70     },
71     warning: {
72         backgroundColor: amber[700]
73     },
74     icon: {
75         fontSize: 20
76     },
77     iconVariant: {
78         opacity: 0.9,
79         marginRight: theme.spacing.unit
80     },
81     message: {
82         display: 'flex',
83         alignItems: 'center'
84     },
85     linkButton: {
86         fontWeight: 'bolder'
87     },
88     snackbarContent: {
89         marginBottom: '1rem'
90     }
91 });
92
93 type SnackbarProps = SnackbarDataProps & SnackbarEventProps & WithStyles<CssRules>;
94
95 export const Snackbar = withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(
96     (props: SnackbarProps) => {
97         const { classes } = props;
98
99         const variants = {
100             [SnackbarKind.INFO]: [InfoIcon, classes.info],
101             [SnackbarKind.WARNING]: [WarningIcon, classes.warning],
102             [SnackbarKind.SUCCESS]: [CheckCircleIcon, classes.success],
103             [SnackbarKind.ERROR]: [ErrorIcon, classes.error]
104         };
105
106         return (
107             <MaterialSnackbar
108                 open={props.open}
109                 onClose={props.onClose}
110                 onExited={props.onExited}
111                 anchorOrigin={props.anchorOrigin}
112                 autoHideDuration={props.autoHideDuration}>
113                 <div data-cy="snackbar">
114                     {
115                          props.messages.map((message, index) => {
116                             const [Icon, cssClass] = variants[message.kind];
117
118                             return <SnackbarContent
119                                 key={`${index}-${message.message}`}
120                                 className={classNames(cssClass, classes.snackbarContent)}
121                                 aria-describedby="client-snackbar"
122                                 message={
123                                     <span id="client-snackbar" className={classes.message}>
124                                         <Icon className={classNames(classes.icon, classes.iconVariant)} />
125                                         {message.message}
126                                     </span>
127                                 }
128                                 action={actions(message, props.onClick, props.onClose, classes, index, props.autoHideDuration)}
129                             />
130                          })
131                     }
132                 </div>
133             </MaterialSnackbar>
134         );
135     }
136 ));
137
138 const actions = (props: SnackbarMessage, onClick, onClose, classes, index, autoHideDuration) => {
139     if (onClose && autoHideDuration) {
140         setTimeout(onClose, autoHideDuration);
141     }
142
143     const actions = [
144         <IconButton
145             key="close"
146             aria-label="Close"
147             color="inherit"
148             onClick={e => onClose && onClose(e, '', index)}>
149             <CloseIcon className={classes.icon} />
150         </IconButton>
151     ];
152     if (props.link) {
153         actions.splice(0, 0,
154             <Button key="goTo"
155                 aria-label="goTo"
156                 size="small"
157                 color="inherit"
158                 className={classes.linkButton}
159                 onClick={() => onClick(props.link)}>
160                 <span data-cy='snackbar-goto-action'>Go To</span>
161             </Button>
162         );
163     }
164     return actions;
165 };