* 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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user