|
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 |
|
| 17 | +import { Button } from '@mui/material'; |
17 | 18 | import { Meta, StoryFn } from '@storybook/react'; |
18 | | -import { SnackbarProvider } from 'notistack'; |
| 19 | +import { SnackbarProvider, useSnackbar } from 'notistack'; |
| 20 | +import React, { useEffect } from 'react'; |
| 21 | +import { Provider } from 'react-redux'; |
| 22 | +import { MemoryRouter } from 'react-router-dom'; |
| 23 | +import store from '../../redux/stores/store'; |
19 | 24 | import { TestContext } from '../../test'; |
20 | 25 | import { PureAlertNotification, PureAlertNotificationProps } from './AlertNotification'; |
21 | 26 |
|
| 27 | +const SnackbarDisplay: React.FC<{ |
| 28 | + show: boolean; |
| 29 | + message: string; |
| 30 | + variant: 'error' | 'success' | 'info' | 'warning'; |
| 31 | +}> = ({ show, message, variant }) => { |
| 32 | + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); |
| 33 | + const key = 'alert-notification-story'; |
| 34 | + |
| 35 | + useEffect(() => { |
| 36 | + if (show) { |
| 37 | + enqueueSnackbar(message, { |
| 38 | + key, |
| 39 | + variant, |
| 40 | + persist: true, |
| 41 | + preventDuplicate: true, |
| 42 | + anchorOrigin: { vertical: 'top', horizontal: 'center' }, |
| 43 | + action: snackbarId => ( |
| 44 | + <Button |
| 45 | + onClick={() => closeSnackbar(snackbarId)} |
| 46 | + size="small" |
| 47 | + sx={{ color: 'common.white' }} |
| 48 | + > |
| 49 | + Dismiss (Story Action) |
| 50 | + </Button> |
| 51 | + ), |
| 52 | + }); |
| 53 | + } else { |
| 54 | + closeSnackbar(key); |
| 55 | + } |
| 56 | + return () => closeSnackbar(key); |
| 57 | + }, [show, message, variant, enqueueSnackbar, closeSnackbar]); |
| 58 | + return null; |
| 59 | +}; |
| 60 | + |
22 | 61 | export default { |
23 | 62 | title: 'AlertNotification', |
24 | 63 | component: PureAlertNotification, |
25 | | - argTypes: { |
26 | | - dispatch: { action: 'dispatch' }, |
27 | | - }, |
28 | 64 | decorators: [ |
29 | | - Story => ( |
30 | | - <TestContext> |
31 | | - <SnackbarProvider> |
32 | | - <Story /> |
33 | | - </SnackbarProvider> |
34 | | - </TestContext> |
| 65 | + (Story, context: { args: StoryArgs }) => ( |
| 66 | + <Provider store={store}> |
| 67 | + <MemoryRouter initialEntries={[context.args.initialRoute || '/cluster/test-cluster/pods']}> |
| 68 | + <TestContext routerMap={{ cluster: 'test-cluster' }}> |
| 69 | + <SnackbarProvider |
| 70 | + maxSnack={3} |
| 71 | + anchorOrigin={{ vertical: 'top', horizontal: 'center' }} |
| 72 | + autoHideDuration={null} |
| 73 | + > |
| 74 | + <div |
| 75 | + style={{ |
| 76 | + position: 'relative', |
| 77 | + minHeight: '100px', |
| 78 | + border: '1px dashed lightgray', |
| 79 | + padding: '10px', |
| 80 | + }} |
| 81 | + > |
| 82 | + <Story /> |
| 83 | + {context.args.simulateSnackbar && ( |
| 84 | + <SnackbarDisplay |
| 85 | + show={context.args.simulateSnackbar.show} |
| 86 | + message={context.args.simulateSnackbar.message} |
| 87 | + variant={context.args.simulateSnackbar.variant} |
| 88 | + /> |
| 89 | + )} |
| 90 | + <div |
| 91 | + id="snackbar-container" |
| 92 | + style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1400 }} |
| 93 | + ></div> |
| 94 | + </div> |
| 95 | + </SnackbarProvider> |
| 96 | + </TestContext> |
| 97 | + </MemoryRouter> |
| 98 | + </Provider> |
35 | 99 | ), |
36 | 100 | ], |
37 | | -} as Meta; |
| 101 | + argTypes: { |
| 102 | + checkerFunction: { table: { disable: true } }, |
| 103 | + initialRoute: { control: 'text' }, |
| 104 | + simulateOffline: { control: 'boolean' }, |
| 105 | + simulateSnackbar: { control: 'object' }, |
| 106 | + }, |
| 107 | +} as Meta<typeof PureAlertNotification & StoryArgs>; |
| 108 | + |
| 109 | +interface StoryArgs extends PureAlertNotificationProps { |
| 110 | + initialRoute?: string; |
| 111 | + simulateOffline?: boolean; |
| 112 | + simulateSnackbar?: { |
| 113 | + show: boolean; |
| 114 | + message: string; |
| 115 | + variant: 'error' | 'success' | 'info' | 'warning'; |
| 116 | + }; |
| 117 | +} |
| 118 | + |
| 119 | +const Template: StoryFn<StoryArgs> = args => { |
| 120 | + const { checkerFunction, simulateOffline, ...rest } = args; |
| 121 | + let originalOnLine: PropertyDescriptor | undefined; |
| 122 | + |
| 123 | + if (typeof simulateOffline === 'boolean') { |
| 124 | + originalOnLine = Object.getOwnPropertyDescriptor(window.navigator, 'onLine'); |
| 125 | + Object.defineProperty(window.navigator, 'onLine', { |
| 126 | + configurable: true, |
| 127 | + get: () => !simulateOffline, |
| 128 | + }); |
| 129 | + } |
38 | 130 |
|
39 | | -const Template: StoryFn<PureAlertNotificationProps> = args => <PureAlertNotification {...args} />; |
| 131 | + useEffect(() => { |
| 132 | + return () => { |
| 133 | + if (typeof simulateOffline === 'boolean' && originalOnLine) { |
| 134 | + Object.defineProperty(window.navigator, 'onLine', originalOnLine); |
| 135 | + } |
| 136 | + }; |
| 137 | + }, [simulateOffline, originalOnLine]); |
| 138 | + |
| 139 | + return <PureAlertNotification checkerFunction={checkerFunction} {...rest} />; |
| 140 | +}; |
| 141 | + |
| 142 | +export const NoErrorInitially = Template.bind({}); |
| 143 | +NoErrorInitially.args = { |
| 144 | + checkerFunction: async () => Promise.resolve({ statusText: 'OK' }), |
| 145 | + simulateSnackbar: { show: false, message: '', variant: 'info' }, |
| 146 | +}; |
| 147 | +NoErrorInitially.storyName = 'No Error (Checker Resolves)'; |
| 148 | + |
| 149 | +export const ErrorOnCheck = Template.bind({}); |
| 150 | +ErrorOnCheck.args = { |
| 151 | + checkerFunction: async () => { |
| 152 | + await new Promise(resolve => setTimeout(resolve, 50)); |
| 153 | + return Promise.reject('Cluster unreachable (Simulated Error)'); |
| 154 | + }, |
| 155 | + simulateSnackbar: { show: true, message: 'Lost connection to the cluster.', variant: 'error' }, |
| 156 | +}; |
| 157 | +ErrorOnCheck.storyName = 'Error on Check (Checker Rejects)'; |
| 158 | + |
| 159 | +export const SimulatingOffline = Template.bind({}); |
| 160 | +SimulatingOffline.args = { |
| 161 | + checkerFunction: async () => Promise.resolve({ statusText: 'OK' }), |
| 162 | + simulateOffline: true, |
| 163 | +}; |
| 164 | +SimulatingOffline.storyName = 'Navigator Offline'; |
40 | 165 |
|
41 | | -export const Error = Template.bind({}); |
42 | | -Error.args = { |
43 | | - checkerFunction: () => Promise.reject('offline'), |
| 166 | +export const OnExcludedRoute = Template.bind({}); |
| 167 | +OnExcludedRoute.args = { |
| 168 | + checkerFunction: async () => Promise.reject('Cluster unreachable (Simulated Error)'), |
| 169 | + initialRoute: '/c/test-cluster/login', |
44 | 170 | }; |
| 171 | +OnExcludedRoute.storyName = 'On Excluded Route (e.g., Login)'; |
45 | 172 |
|
46 | | -export const NoError = Template.bind({}); |
47 | | -NoError.args = { |
48 | | - checkerFunction: () => Promise.resolve({ statusText: 'OK' }), |
| 173 | +export const OnNonClusterRoute = Template.bind({}); |
| 174 | +OnNonClusterRoute.args = { |
| 175 | + checkerFunction: async () => Promise.reject('Cluster unreachable (Simulated Error)'), |
| 176 | + initialRoute: '/settings/plugins', |
| 177 | + simulateSnackbar: { show: false, message: '', variant: 'info' }, |
49 | 178 | }; |
| 179 | +OnNonClusterRoute.storyName = 'On Non-Cluster Route (e.g., Global Settings)'; |
0 commit comments