Make DismissibleBanner persistently dismissed #1153 (#1168)

* Enables DismissableBanner instance in localstorage

When dismissed, a string representation of the DismissableBanner
is sent to localstorage using the storageapi, to allow persistance in
a user session.

* Adds tests and cleans up DismissableBanner changes

DismissbleBanner now has an id prop. This prop is used when
adding a dismissed banner instance to web storage.

The namespace dismissed banners are added to is now set to
"notifications".

Removed refrences to old settings.

Added a test to check web storage functionality.

Updates CatalogPage to add id prop.

Updates CatalogPage test to ensure storageApiRef is valid.

* Updates DismissableBanner stories to include id.

* Adds ApiProvider to DismissableBanner stories.

* Trigger CI

* Removed jest calls and replaced with ErrorApi.

* Updates imports to @backstage/core
This commit is contained in:
Benjamin Rowell
2020-06-08 19:16:51 +01:00
committed by GitHub
parent 2bbdf644c8
commit 056c794451
6 changed files with 192 additions and 75 deletions
@@ -17,50 +17,86 @@
import React from 'react';
import DismissableBanner from './DismissableBanner';
import { Link, Typography } from '@material-ui/core';
import {
ApiProvider,
ApiRegistry,
CreateStorageApiOptions,
ErrorApi,
storageApiRef,
StorageApi,
WebStorage,
} from '@backstage/core';
export default {
title: 'DismissableBanner',
component: DismissableBanner,
};
let errorApi: ErrorApi;
const containerStyle = { width: '70%' };
const createWebStorage = (
args?: Partial<CreateStorageApiOptions>,
): StorageApi => {
return WebStorage.create({
errorApi: errorApi,
...args,
});
};
const apis = ApiRegistry.from([[storageApiRef, createWebStorage()]]);
export const Default = () => (
<div style={containerStyle}>
<DismissableBanner message="This is a dismissable banner" variant="info" />
<ApiProvider apis={apis}>
<DismissableBanner
message="This is a dismissable banner"
variant="info"
id="default_dismissable"
/>
</ApiProvider>
</div>
);
export const Error = () => (
<div style={containerStyle}>
<DismissableBanner
message="This is a dismissable banner with an error message"
variant="error"
/>
<ApiProvider apis={apis}>
<DismissableBanner
message="This is a dismissable banner with an error message"
variant="error"
id="error_dismissable"
/>
</ApiProvider>
</div>
);
export const EmojisIncluded = () => (
<div style={containerStyle}>
<DismissableBanner
message="This is a dismissable banner with emojis: 🚀 💚 😆 "
variant="info"
/>
<ApiProvider apis={apis}>
<DismissableBanner
message="This is a dismissable banner with emojis: 🚀 💚 😆 "
variant="info"
id="emojis_dismissable"
/>
</ApiProvider>
</div>
);
export const WithLink = () => (
<div style={containerStyle}>
<DismissableBanner
message={
<Typography>
This is a dismissable banner with a link:{' '}
<Link href="http://example.com" color="textSecondary">
example.com
</Link>
</Typography>
}
variant="info"
/>
<ApiProvider apis={apis}>
<DismissableBanner
message={
<Typography>
This is a dismissable banner with a link:{' '}
<Link href="http://example.com" color="textSecondary">
example.com
</Link>
</Typography>
}
variant="info"
id="linked_dismissable"
/>
</ApiProvider>
</div>
);
@@ -1,46 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
// import { fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
// import { createSetting } from 'shared/apis/settings';
import DismissableBanner from './DismissableBanner';
describe('<DismissableBanner />', () => {
it('renders the message and the popover', async () => {
/*
const mockSetting = createSetting({
id: 'mockSetting',
defaultValue: true,
});
*/
const rendered = await renderWithEffects(
wrapInTestApp(
<DismissableBanner
variant="info"
// setting={mockSetting}
message="test message"
/>,
),
);
rendered.getByText('test message');
// fireEvent.click(rendered.getByTitle('Permanently dismiss this message'));
// await waitForElementToBeRemoved(rendered.queryByText('test message'));
});
});
@@ -0,0 +1,88 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import DismissableBanner from './DismissableBanner';
import {
ApiRegistry,
ApiProvider,
storageApiRef,
CreateStorageApiOptions,
StorageApi,
WebStorage,
} from '@backstage/core';
describe('<DismissableBanner />', () => {
let apis: ApiRegistry;
const mockErrorApi = { post: jest.fn(), error$: jest.fn() };
const createWebStorage = (
args?: Partial<CreateStorageApiOptions>,
): StorageApi => {
return WebStorage.create({
errorApi: mockErrorApi,
...args,
});
};
beforeEach(() => {
apis = ApiRegistry.from([[storageApiRef, createWebStorage()]]);
});
it('renders the message and the popover', async () => {
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<DismissableBanner
variant="info"
// setting={mockSetting}
message="test message"
id="catalog_page_welcome_banner"
/>
</ApiProvider>,
),
);
const element = await rendered.findByText('test message');
expect(element).toBeInTheDocument();
});
it('gets placed in local storage on dismiss', async () => {
const rendered = await renderWithEffects(
wrapInTestApp(
<ApiProvider apis={apis}>
<DismissableBanner
variant="info"
// setting={mockSetting}
message="test message"
id="catalog_page_welcome_banner"
/>
</ApiProvider>,
),
);
const webstore = apis.get(storageApiRef);
const notifications = webstore?.forBucket('notifications');
const button = await rendered.findByTitle(
'Permanently dismiss this message',
);
fireEvent.click(button);
const dismissedBanners =
notifications?.get<string[]>('dismissedBanners') ?? [];
expect(
dismissedBanners.includes('catalog_page_welcome_banner'),
).toBeTruthy();
});
});
@@ -14,14 +14,15 @@
* limitations under the License.
*/
import React, { FC, ReactNode, useState } from 'react';
import React, { FC, ReactNode, useState, useEffect } from 'react';
import { useApi, storageApiRef } from '@backstage/core';
import { useObservable } from 'react-use';
import classNames from 'classnames';
import { makeStyles, Theme } from '@material-ui/core';
import Snackbar from '@material-ui/core/Snackbar';
import SnackbarContent from '@material-ui/core/SnackbarContent';
import IconButton from '@material-ui/core/IconButton';
import Close from '@material-ui/icons/Close';
// import { useSetting, Setting } from 'shared/apis/settings';
const useStyles = makeStyles((theme: Theme) => ({
root: {
@@ -54,23 +55,40 @@ const useStyles = makeStyles((theme: Theme) => ({
type Props = {
variant: 'info' | 'error';
// setting: Setting<boolean>;
message: ReactNode;
id: string;
};
const DismissableBanner: FC<Props> = ({ variant, /* setting, */ message }) => {
// const [show, setShown, loading] = useSetting(setting);
const [show, setShown] = useState(true);
const DismissableBanner: FC<Props> = ({ variant, message, id }) => {
const classes = useStyles();
const storageApi = useApi(storageApiRef);
const notificationsStore = storageApi.forBucket('notifications');
const rawDismissedBanners =
notificationsStore.get<string[]>('dismissedBanners') ?? [];
const [dismissedBanners, setDismissedBanners] = useState(
new Set(rawDismissedBanners),
);
const observedItems = useObservable(
notificationsStore.observe$<string[]>('dismissedBanners'),
);
useEffect(() => {
if (observedItems?.newValue) {
const currentValue = observedItems?.newValue ?? [];
setDismissedBanners(new Set(currentValue));
}
}, [observedItems?.newValue]);
const handleClick = () => {
setShown(false);
notificationsStore.set('dismissedBanners', [...dismissedBanners, id]);
};
return (
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
open={show /* && !loading */}
open={!dismissedBanners.has(id)}
classes={{ root: classes.root }}
>
<SnackbarContent
@@ -17,7 +17,15 @@
import React from 'react';
import { render } from '@testing-library/react';
import CatalogPage from './CatalogPage';
import { ApiRegistry, ApiProvider, errorApiRef } from '@backstage/core';
import {
ApiProvider,
ApiRegistry,
CreateStorageApiOptions,
errorApiRef,
storageApiRef,
StorageApi,
WebStorage,
} from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { catalogApiRef } from '../..';
import { CatalogApi } from '../../api/types';
@@ -39,6 +47,17 @@ const catalogApi: Partial<CatalogApi> = {
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
};
const mockWebStorageErrorApi = { post: jest.fn(), error$: jest.fn() };
const createWebStorage = (
args?: Partial<CreateStorageApiOptions>,
): StorageApi => {
return WebStorage.create({
errorApi: mockWebStorageErrorApi,
...args,
});
};
const storageApi = createWebStorage();
describe('CatalogPage', () => {
// this test right now causes some red lines in the log output when running tests
// related to some theme issues in mui-table
@@ -50,6 +69,7 @@ describe('CatalogPage', () => {
apis={ApiRegistry.from([
[errorApiRef, errorApi],
[catalogApiRef, catalogApi],
[storageApiRef, storageApi],
])}
>
<CatalogPage />
@@ -149,6 +149,7 @@ const CatalogPage: FC<{}> = () => {
page.
</Typography>
}
id="catalog_page_welcome_banner"
/>
<ContentHeader title="Services">