Alerting: Storybook integration (#107024)

renovate/ol-10.x
Gilles De Mey 2 days ago committed by GitHub
parent 0356e1302a
commit 092249b772
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      packages/grafana-alerting/src/Intro.mdx
  2. 6
      packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.mdx
  3. 37
      packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx
  4. 5
      packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.test.scenario.ts
  5. 41
      packages/grafana-alerting/tests/provider.tsx
  6. 13
      packages/grafana-alerting/tests/story-utils.tsx
  7. 30
      packages/grafana-alerting/tests/test-utils.tsx
  8. 5
      packages/grafana-ui/.storybook/copyAssets.ts
  9. 21
      packages/grafana-ui/.storybook/main.ts
  10. 10
      packages/grafana-ui/.storybook/preview.ts
  11. 2
      packages/grafana-ui/package.json
  12. 17
      yarn.lock

@ -0,0 +1,13 @@
# @grafana/alerting
This is the alerting package for Grafana. It provides components and utilities for creating and managing alerts within Grafana.
It is designed to be used in conjunction with Grafana's alerting system, allowing developers to create custom alerting solutions.
## Installation
To install the package:
```bash
npm install @grafana/alerting
```

@ -0,0 +1,6 @@
import { ArgTypes } from '@storybook/blocks';
import { ContactPointSelector } from './ContactPointSelector';
# ContactPointSelector
A component for selecting contact points in Grafana. Only works with the built-in Grafana Alertmanager.

@ -0,0 +1,37 @@
import type { Meta, StoryObj } from '@storybook/react';
import { defaultDecorators } from '../../../../../tests/story-utils';
import { ContactPointSelector } from './ContactPointSelector';
import mdx from './ContactPointSelector.mdx';
import { simpleContactPointsListScenario, withErrorScenario } from './ContactPointSelector.test.scenario';
const meta: Meta<typeof ContactPointSelector> = {
component: ContactPointSelector,
title: 'ContactPointSelector',
decorators: defaultDecorators,
parameters: {
docs: {
page: mdx,
},
},
};
export default meta;
type Story = StoryObj<typeof ContactPointSelector>;
export const Basic: Story = {
parameters: {
msw: {
handlers: simpleContactPointsListScenario,
},
},
};
export const WithError: Story = {
parameters: {
msw: {
handlers: withErrorScenario,
},
},
};

@ -1,3 +1,5 @@
import { HttpResponse } from 'msw';
import {
ContactPointFactory,
EmailIntegrationFactory,
@ -30,5 +32,4 @@ export const simpleContactPointsList = ListReceiverApiResponseFactory.build({
// export the simple contact points list as a separate list of handlers (scenario) so we can load it in the front-end
export const simpleContactPointsListScenario = [listReceiverHandler(simpleContactPointsList)];
// the default export will allow us to load this scenario on the front-end using the MSW web worker
export default simpleContactPointsListScenario;
export const withErrorScenario = [listReceiverHandler(() => new HttpResponse(null, { status: 500 }))];

@ -0,0 +1,41 @@
import { configureStore } from '@reduxjs/toolkit';
import { useEffect } from 'react';
import { Provider } from 'react-redux';
import { alertingAPIv0alpha1 } from '../src/unstable';
// create an empty store
export const store = configureStore({
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(alertingAPIv0alpha1.middleware),
reducer: {
[alertingAPIv0alpha1.reducerPath]: alertingAPIv0alpha1.reducer,
},
});
/**
* Get a wrapper component that implements all of the providers that components
* within the app will need
*/
export const getDefaultWrapper = () => {
/**
* Returns a wrapper that should (eventually?) match the main `AppWrapper`, so any tests are rendering
* in mostly the same providers as a "real" hierarchy
*/
return function Wrapper({ children }: React.PropsWithChildren) {
useResetQueryCacheAfterUnmount();
return <Provider store={store}>{children}</Provider>;
};
};
/**
* Whenever the test wrapper unmounts, we also want to clear the RTKQ cache entirely.
* if we don't then we won't be able to test components / stories with different responses for the same endpoint since
* the responses will be cached between renders / components / stories.
*/
function useResetQueryCacheAfterUnmount() {
useEffect(() => {
return () => {
store.dispatch(alertingAPIv0alpha1.util.resetApiState());
};
}, []);
}

@ -0,0 +1,13 @@
import type { Meta } from '@storybook/react';
import { getDefaultWrapper } from './provider';
const Wrapper = getDefaultWrapper();
export const defaultDecorators: Meta['decorators'] = [
(Story) => (
<Wrapper>
<Story />
</Wrapper>
),
];

@ -1,36 +1,12 @@
/**
* @TODO this will eventually be replaced with "@grafana/test-utils", consider helping out instead of adding things here!
*/
import { configureStore } from '@reduxjs/toolkit';
import { type RenderOptions, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import '@testing-library/jest-dom';
import { alertingAPIv0alpha1 } from '../src/unstable';
// create an empty store
const store = configureStore({
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(alertingAPIv0alpha1.middleware),
reducer: {
[alertingAPIv0alpha1.reducerPath]: alertingAPIv0alpha1.reducer,
},
});
import { getDefaultWrapper, store } from './provider';
/**
* Get a wrapper component that implements all of the providers that components
* within the app will need
*/
const getDefaultWrapper = ({}: RenderOptions) => {
/**
* Returns a wrapper that should (eventually?) match the main `AppWrapper`, so any tests are rendering
* in mostly the same providers as a "real" hierarchy
*/
return function Wrapper({ children }: React.PropsWithChildren) {
return <Provider store={store}>{children}</Provider>;
};
};
import '@testing-library/jest-dom';
/**
* Extended [@testing-library/react render](https://testing-library.com/docs/react-testing-library/api/#render)
@ -39,7 +15,7 @@ const getDefaultWrapper = ({}: RenderOptions) => {
*/
const customRender = (ui: React.ReactNode, renderOptions: RenderOptions = {}) => {
const user = userEvent.setup();
const Providers = renderOptions.wrapper || getDefaultWrapper(renderOptions);
const Providers = renderOptions.wrapper || getDefaultWrapper();
return {
renderResult: render(ui, { wrapper: Providers, ...renderOptions }),

@ -45,6 +45,11 @@ export function copyAssetsSync() {
to: './static/public/lib',
},
...iconPaths,
// copy over the MSW mock service worker so we can mock requests in Storybook
{
from: '../../../public/mockServiceWorker.js',
to: './static/mockServiceWorker.js',
},
];
const staticDir = resolve(__dirname, 'static', 'public');

@ -3,12 +3,27 @@ import type { StorybookConfig } from '@storybook/react-webpack5';
import { copyAssetsSync } from './copyAssets';
// Internal stories should only be visible during development
const storyGlob =
const coreComponentsGlobs: StorybookConfig['stories'] = [
'../src/Intro.mdx',
process.env.NODE_ENV === 'production'
? '../src/components/**/!(*.internal).story.tsx'
: '../src/components/**/*.story.tsx';
: '../src/components/**/*.story.tsx',
];
const stories = ['../src/Intro.mdx', storyGlob];
const alertingComponentsGlobs: StorybookConfig['stories'] = [
{
titlePrefix: 'Alerting',
directory: '../../grafana-alerting/src',
files: 'Intro.mdx',
},
{
titlePrefix: 'Alerting',
directory: '../../grafana-alerting/src',
files: process.env.NODE_ENV === 'production' ? '**/!(*.internal).story.tsx' : '**/*.story.tsx',
},
];
const stories = [...coreComponentsGlobs, ...alertingComponentsGlobs];
// Copy the assets required by storybook before starting the storybook server.
copyAssetsSync();

@ -1,4 +1,6 @@
import { Preview } from '@storybook/react';
import { initialize, mswLoader } from 'msw-storybook-addon';
import 'jquery';
import { getBuiltInThemes, getTimeZone, getTimeZones, GrafanaTheme2 } from '@grafana/data';
@ -42,6 +44,13 @@ if (process.env.NODE_ENV === 'development') {
allowedExtraThemes.push('tron');
}
/*
* Initializes MSW
* See https://github.com/mswjs/msw-storybook-addon#configuring-msw
* to learn how to customize it
*/
initialize();
const preview: Preview = {
decorators: [withTheme(handleThemeChange), withTimeZone()],
parameters: {
@ -102,6 +111,7 @@ const preview: Preview = {
},
},
tags: ['autodocs'],
loaders: [mswLoader],
};
export default preview;

@ -186,6 +186,8 @@
"expose-loader": "5.0.0",
"fs-extra": "^11.2.0",
"mock-raf": "1.0.1",
"msw": "^2.10.2",
"msw-storybook-addon": "^2.0.5",
"process": "^0.11.10",
"react": "18.3.1",
"react-dom": "18.3.1",

@ -3717,6 +3717,8 @@ __metadata:
mock-raf: "npm:1.0.1"
moment: "npm:2.30.1"
monaco-editor: "npm:0.34.1"
msw: "npm:^2.10.2"
msw-storybook-addon: "npm:^2.0.5"
ol: "npm:7.4.0"
prismjs: "npm:1.30.0"
process: "npm:^0.11.10"
@ -19979,7 +19981,7 @@ __metadata:
languageName: node
linkType: hard
"is-node-process@npm:^1.2.0":
"is-node-process@npm:^1.0.1, is-node-process@npm:^1.2.0":
version: 1.2.0
resolution: "is-node-process@npm:1.2.0"
checksum: 10/930765cdc6d81ab8f1bbecbea4a8d35c7c6d88a3ff61f3630e0fc7f22d624d7661c1df05c58547d0eb6a639dfa9304682c8e342c4113a6ed51472b704cee2928
@ -23164,7 +23166,18 @@ __metadata:
languageName: node
linkType: hard
"msw@npm:2.10.2":
"msw-storybook-addon@npm:^2.0.5":
version: 2.0.5
resolution: "msw-storybook-addon@npm:2.0.5"
dependencies:
is-node-process: "npm:^1.0.1"
peerDependencies:
msw: ^2.0.0
checksum: 10/c7c2c77fbe64775f6d01a2724c1e43d67c2590dc5961d4cd14c654d954b84b7938eba0404888d056752e360a41d4a8e4535ebc27a80d4108ce78552ac904be32
languageName: node
linkType: hard
"msw@npm:2.10.2, msw@npm:^2.10.2":
version: 2.10.2
resolution: "msw@npm:2.10.2"
dependencies:

Loading…
Cancel
Save