Alerting: Storybook integration (#107024)

This commit is contained in:
Gilles De Mey
2025-06-25 12:59:07 +02:00
committed by GitHub
parent 0356e1302a
commit 092249b772
12 changed files with 166 additions and 34 deletions
+13
View File
@@ -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>
),
];
+3 -27
View File
@@ -1,37 +1,13 @@
/**
* ⚠️ @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 { getDefaultWrapper, store } from './provider';
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,
},
});
/**
* 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>;
};
};
/**
* Extended [@testing-library/react render](https://testing-library.com/docs/react-testing-library/api/#render)
* method which wraps the passed element in all of the necessary Providers,
@@ -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');
+18 -3
View File
@@ -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();
+10
View File
@@ -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;
+2
View File
@@ -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",
+15 -2
View File
@@ -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: