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