diff --git a/integration-tests/tests/run-analysis-azure.ts b/integration-tests/tests/run-analysis-azure.ts
index 427e7feb9c..5daa3883fd 100644
--- a/integration-tests/tests/run-analysis-azure.ts
+++ b/integration-tests/tests/run-analysis-azure.ts
@@ -21,7 +21,7 @@ const {
getAnimatedDrawer,
input,
noSpinnersAfter,
- waitForNoModal,
+ waitForNoModalDrawer,
waitForNoSpinners,
} = require('../utils/integration-utils');
const { registerTest } = require('../utils/jest-utils');
@@ -51,7 +51,7 @@ const testRunAnalysisAzure = _.flowRight(
timeout: Millis.ofMinute,
});
await click(page, clickable({ textContains: 'Close' }), { timeout: Millis.ofMinute });
- await waitForNoModal(page);
+ await waitForNoModalDrawer(page);
// Navigate to analysis launcher
await click(page, clickable({ textContains: `${notebookName}.ipynb` }));
@@ -63,7 +63,7 @@ const testRunAnalysisAzure = _.flowRight(
await click(page, clickable({ textContains: 'Open' }));
await findText(page, 'Azure Cloud Environment');
await click(page, clickable({ textContains: 'Create' }));
- await waitForNoModal(page);
+ await waitForNoModalDrawer(page);
// Wait for env to begin creating
await findElement(page, clickable({ textContains: 'JupyterLab Environment' }));
diff --git a/integration-tests/tests/run-analysis.js b/integration-tests/tests/run-analysis.js
index 351451c745..6d2c2a40e1 100644
--- a/integration-tests/tests/run-analysis.js
+++ b/integration-tests/tests/run-analysis.js
@@ -16,7 +16,7 @@ const {
getAnimatedDrawer,
input,
noSpinnersAfter,
- waitForNoModal,
+ waitForNoModalDrawer,
waitForNoSpinners,
} = require('../utils/integration-utils');
const { registerTest } = require('../utils/jest-utils');
@@ -47,7 +47,7 @@ const testRunAnalysisFn = _.flowRight(
timeout: Millis.ofMinute,
});
await click(page, clickable({ textContains: 'Close' }), { timeout: Millis.ofMinute });
- await waitForNoModal(page);
+ await waitForNoModalDrawer(page);
// Navigate to analysis launcher
await click(page, clickable({ textContains: `${notebookName}.ipynb` }));
@@ -61,7 +61,7 @@ const testRunAnalysisFn = _.flowRight(
});
await findText(page, 'Jupyter Cloud Environment');
await click(page, clickable({ text: 'Create' }));
- await waitForNoModal(page);
+ await waitForNoModalDrawer(page);
// Wait for env to begin creating
await findElement(page, clickable({ textContains: 'Jupyter Environment' }), { timeout: Millis.ofSeconds(40) });
diff --git a/integration-tests/tests/run-rstudio.js b/integration-tests/tests/run-rstudio.js
index 4399b3fdc2..3941577bd3 100644
--- a/integration-tests/tests/run-rstudio.js
+++ b/integration-tests/tests/run-rstudio.js
@@ -16,7 +16,7 @@ const {
getAnimatedDrawer,
input,
noSpinnersAfter,
- waitForNoModal,
+ waitForNoModalDrawer,
waitForNoSpinners,
} = require('../utils/integration-utils');
const { registerTest } = require('../utils/jest-utils');
@@ -50,7 +50,7 @@ const testRunRStudioFn = _.flowRight(
timeout: Millis.ofMinute,
});
await click(page, clickable({ textContains: 'Close' }), { timeout: Millis.ofMinute });
- await waitForNoModal(page);
+ await waitForNoModalDrawer(page);
// Navigate to analysis launcher
await click(page, clickable({ textContains: `${rFileName}.Rmd` }));
@@ -63,7 +63,7 @@ const testRunRStudioFn = _.flowRight(
action: () => click(page, clickable({ textContains: 'Open' })),
});
await click(page, clickable({ text: 'Create' }));
- await waitForNoModal(page);
+ await waitForNoModalDrawer(page);
// Wait for env to begin creating
await findElement(page, clickable({ textContains: 'RStudio Environment' }), { timeout: Millis.ofMinutes(2) });
diff --git a/integration-tests/utils/integration-utils.js b/integration-tests/utils/integration-utils.js
index 9fa654afdc..3594309f34 100644
--- a/integration-tests/utils/integration-utils.js
+++ b/integration-tests/utils/integration-utils.js
@@ -233,6 +233,12 @@ const waitForModal = (page, { timeout = 30000 } = {}) => {
return page.waitForSelector('.ReactModal__Overlay', { hidden: false, timeout });
};
+const waitForNoModalDrawer = async (page) => {
+ await waitForNoModal(page);
+ // Matches the animation transition time
+ await delay(200);
+};
+
// Puppeteer works by internally using MutationObserver. We are setting up the listener before
// the action to ensure that the spinner rendering is captured by the observer, followed by
// waiting for the spinner to be removed
@@ -631,6 +637,7 @@ module.exports = {
waitForNoModal,
waitForMenu,
waitForModal,
+ waitForNoModalDrawer,
waitForNoSpinners,
withPageLogging,
withScreenshot,
diff --git a/src/libs/notifications.test.tsx b/src/libs/notifications.test.tsx
index 44e1427722..0a3e66a072 100644
--- a/src/libs/notifications.test.tsx
+++ b/src/libs/notifications.test.tsx
@@ -147,6 +147,37 @@ describe('notify', () => {
// Assert
screen.getByText('Things went BOOM!');
});
+
+ it('renders navigation buttons if needed', () => {
+ // Arrange
+ notify('error', 'Test notification', {
+ id: 'test-notification',
+ message: 'first message',
+ });
+ notify('error', 'Test notification', {
+ id: 'test-notification',
+ message: 'second message',
+ });
+ render(
{notificationContent}
);
+
+ // Act and Assert
+ screen.getByText('first message');
+ screen.getByText('1/2');
+ const nextButton = screen.getByLabelText('Next notification');
+ const previousButton = screen.getByLabelText('Previous notification');
+ expect(nextButton).toHaveAttribute('aria-disabled', 'false');
+ expect(previousButton).toHaveAttribute('aria-disabled', 'true');
+
+ fireEvent.click(nextButton);
+ screen.getByText('second message');
+ screen.getByText('2/2');
+ expect(nextButton).toHaveAttribute('aria-disabled', 'true');
+ expect(previousButton).toHaveAttribute('aria-disabled', 'false');
+
+ fireEvent.click(previousButton);
+ screen.getByText('first message');
+ screen.getByText('1/2');
+ });
});
describe('clearNotification', () => {
diff --git a/src/libs/notifications.ts b/src/libs/notifications.tsx
similarity index 51%
rename from src/libs/notifications.ts
rename to src/libs/notifications.tsx
index a540157249..656c7a9b56 100644
--- a/src/libs/notifications.ts
+++ b/src/libs/notifications.tsx
@@ -1,7 +1,7 @@
import {
ButtonPrimary,
Clickable,
- icon,
+ Icon,
IconId,
Link,
Modal,
@@ -11,8 +11,7 @@ import {
import { DEFAULT, switchCase } from '@terra-ui-packages/core-utils';
import { NotificationType } from '@terra-ui-packages/notifications';
import _ from 'lodash/fp';
-import { ReactNode, useState } from 'react';
-import { div, h } from 'react-hyperscript-helpers';
+import React, { ReactNode, useState } from 'react';
import { Store } from 'react-notifications-component';
import ErrorView from 'src/components/ErrorView';
import { getLocalPref, setLocalPref } from 'src/libs/prefs';
@@ -56,27 +55,27 @@ export const notify = (type: NotificationType, title: ReactNode, props?: Notific
return notification.id;
};
-export const clearNotification = (id) => Store.removeNotification(id);
+export const clearNotification = (id: string) => Store.removeNotification(id);
-export const clearMatchingNotifications = (idPrefix) => {
+export const clearMatchingNotifications = (idPrefix: string) => {
const matchingNotificationIds = _.flow(_.map(_.get('id')), _.filter(_.startsWith(idPrefix)))(notificationStore.get());
matchingNotificationIds.forEach((id) => {
Store.removeNotification(id);
});
};
-const muteNotificationPreferenceKey = (id) => `mute-notification/${id}`;
+const muteNotificationPreferenceKey = (id: string) => `mute-notification/${id}`;
-export const isNotificationMuted = (id) => {
+export const isNotificationMuted = (id: string) => {
const mutedUntil = getLocalPref(muteNotificationPreferenceKey(id));
return switchCase(mutedUntil, [undefined, () => false], [-1, () => true], [DEFAULT, () => mutedUntil > Date.now()]);
};
-export const muteNotification = (id, until = -1) => {
+export const muteNotification = (id: string, until = -1) => {
setLocalPref(muteNotificationPreferenceKey(id), until);
};
-const NotificationDisplay = ({ id }) => {
+const NotificationDisplay = ({ id }: { id: string }) => {
const { colors } = useThemeFromContext();
const notificationState = useStore(notificationStore);
const [modal, setModal] = useState(false);
@@ -106,9 +105,9 @@ const NotificationDisplay = ({ id }) => {
const labelId = useUniqueId();
const descId = useUniqueId();
- return div(
- {
- style: {
+ return (
+ {
display: 'flex',
flexDirection: 'column',
fontSize: 12,
- },
- role: 'alert',
- 'aria-labelledby': labelId,
- 'aria-describedby': message ? descId : undefined,
- },
- [
- // content and close button
- div({ style: { display: 'flex', padding: '0.75rem 1rem' } }, [
- // content
- div({ style: { display: 'flex', flex: 1, flexDirection: 'column', overflow: 'hidden' } }, [
- // icon and title
- div({ style: { display: 'flex' } }, [
- !!iconType &&
- icon(iconType, {
- 'aria-hidden': false,
- 'aria-label': ariaLabel,
- size: 26,
- style: { color: baseColor(), flexShrink: 0, marginRight: '0.5rem' },
- }),
- div({ id: labelId, style: { fontWeight: 600, overflow: 'hidden', overflowWrap: 'break-word' } }, [title]),
- ]),
- !!message && div({ id: descId, style: { marginTop: '0.5rem', overflowWrap: 'break-word' } }, [message]),
- div({ style: { display: 'flex' } }, [
- !!detail &&
- h(
- Clickable,
- {
- style: { marginTop: '0.25rem', marginRight: '0.5rem', textDecoration: 'underline' },
- onClick: () => setModal(true),
- },
- ['Details']
- ),
- ]),
- ]),
- h(
- Link,
- {
- style: { alignSelf: 'start' },
- 'aria-label': type ? `Dismiss ${type} notification` : 'Dismiss notification',
- title: 'Dismiss notification',
- onClick: () => {
- Store.removeNotification(id);
- },
- },
- [icon('times', { size: 20 })]
- ),
- ]),
- notifications.length > 1 &&
- div(
- {
- style: {
- alignItems: 'center',
- borderTop: `1px solid ${baseColor()}`,
- display: 'flex',
- fontSize: 10,
- padding: '0.75rem 1rem',
- },
- },
- [
- h(
- Link,
- {
- disabled: onFirst,
- onClick: () => setNotificationNumber(notificationNumber - 1),
- },
- [icon('angle-left', { size: 12 })]
- ),
- div(
- {
- style: {
- backgroundColor: colors.accent(),
- color: 'white',
- fontWeight: 600,
- borderRadius: 10,
- padding: '0.2rem 0.5rem',
- },
- },
- [notificationNumber + 1, '/', notifications.length]
- ),
- h(
- Link,
- {
- disabled: onLast,
- onClick: () => setNotificationNumber(notificationNumber + 1),
- },
- [icon('angle-right', { size: 12 })]
- ),
- ]
- ),
- modal &&
- h(
- Modal,
- {
- width: 800,
- title,
- showCancel: false,
- showX: true,
- onDismiss: () => setModal(false),
- okButton: h(ButtonPrimary, { onClick: refreshPage }, ['Refresh Page']),
- },
- [h(ErrorView, { error: detail })]
- ),
- ]
+ }}
+ role='alert'
+ aria-labelledby={labelId}
+ aria-describedby={message ? descId : undefined}
+ >
+
+
+
+ {!!iconType && (
+
+ )}
+
+ {title}
+
+
+ {!!message && (
+
+ {message}
+
+ )}
+
+ {!!detail && (
+ setModal(true)}
+ >
+ Details
+
+ )}
+
+
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+
Store.removeNotification(id)}
+ >
+
+
+
+ {notifications.length > 1 && (
+
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+
setNotificationNumber(notificationNumber - 1)}
+ aria-label='Previous notification'
+ >
+
+
+
+ {notificationNumber + 1}/{notifications.length}
+
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+
setNotificationNumber(notificationNumber + 1)}
+ aria-label='Next notification'
+ >
+
+
+
+ )}
+ {modal && (
+
setModal(false)}
+ okButton={Refresh Page}
+ >
+
+
+ )}
+
);
};
@@ -231,7 +226,11 @@ const showNotification = ({ id, timeout }) => {
Store.addNotification({
id,
onRemoval: () => notificationStore.update(_.reject({ id })),
- content: div({ style: { width: '100%' } }, [h(NotificationDisplay, { id })]),
+ content: (
+
+
+
+ ),
container: 'top-right',
dismiss: { duration: timeout || 0, click: false, touch: false },
animationIn: ['animate__animated', 'animate__fadeIn'],
diff --git a/src/workspaces/common/state/useWorkspace.ts b/src/workspaces/common/state/useWorkspace.ts
index 1333f56d28..d242cb4689 100644
--- a/src/workspaces/common/state/useWorkspace.ts
+++ b/src/workspaces/common/state/useWorkspace.ts
@@ -228,7 +228,7 @@ export const useWorkspace = (namespace, name): WorkspaceDetails => {
{
onClick: () => {
refreshWorkspace();
- clearNotification(accessNotificationId.current);
+ clearNotification(accessNotificationId.current!);
},
},
['Click to refresh now']