diff --git a/packages/grafana-alerting/src/Intro.mdx b/packages/grafana-alerting/src/Intro.mdx new file mode 100644 index 00000000000..3e537cd3563 --- /dev/null +++ b/packages/grafana-alerting/src/Intro.mdx @@ -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 +``` diff --git a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.mdx b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.mdx new file mode 100644 index 00000000000..c7f407c0a5e --- /dev/null +++ b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.mdx @@ -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. diff --git a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx new file mode 100644 index 00000000000..11033d82739 --- /dev/null +++ b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx @@ -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 = { + component: ContactPointSelector, + title: 'ContactPointSelector', + decorators: defaultDecorators, + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Basic: Story = { + parameters: { + msw: { + handlers: simpleContactPointsListScenario, + }, + }, +}; + +export const WithError: Story = { + parameters: { + msw: { + handlers: withErrorScenario, + }, + }, +}; diff --git a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.test.scenario.ts b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.test.scenario.ts index b8fb0803908..026a0cf7221 100644 --- a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.test.scenario.ts +++ b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.test.scenario.ts @@ -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 }))]; diff --git a/packages/grafana-alerting/tests/provider.tsx b/packages/grafana-alerting/tests/provider.tsx new file mode 100644 index 00000000000..039015eb8bf --- /dev/null +++ b/packages/grafana-alerting/tests/provider.tsx @@ -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 {children}; + }; +}; + +/** + * 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()); + }; + }, []); +} diff --git a/packages/grafana-alerting/tests/story-utils.tsx b/packages/grafana-alerting/tests/story-utils.tsx new file mode 100644 index 00000000000..514f9447b22 --- /dev/null +++ b/packages/grafana-alerting/tests/story-utils.tsx @@ -0,0 +1,13 @@ +import type { Meta } from '@storybook/react'; + +import { getDefaultWrapper } from './provider'; + +const Wrapper = getDefaultWrapper(); + +export const defaultDecorators: Meta['decorators'] = [ + (Story) => ( + + + + ), +]; diff --git a/packages/grafana-alerting/tests/test-utils.tsx b/packages/grafana-alerting/tests/test-utils.tsx index ed25c27db8c..e0f0e12ac9c 100644 --- a/packages/grafana-alerting/tests/test-utils.tsx +++ b/packages/grafana-alerting/tests/test-utils.tsx @@ -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'; +import { getDefaultWrapper, store } from './provider'; -// 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 {children}; - }; -}; +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 }), diff --git a/packages/grafana-ui/.storybook/copyAssets.ts b/packages/grafana-ui/.storybook/copyAssets.ts index 165af560dc6..e49cd32b5d0 100644 --- a/packages/grafana-ui/.storybook/copyAssets.ts +++ b/packages/grafana-ui/.storybook/copyAssets.ts @@ -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'); diff --git a/packages/grafana-ui/.storybook/main.ts b/packages/grafana-ui/.storybook/main.ts index 401a79ef7ea..1c75daef688 100644 --- a/packages/grafana-ui/.storybook/main.ts +++ b/packages/grafana-ui/.storybook/main.ts @@ -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(); diff --git a/packages/grafana-ui/.storybook/preview.ts b/packages/grafana-ui/.storybook/preview.ts index fbf3c2a3379..f66b4b8422a 100644 --- a/packages/grafana-ui/.storybook/preview.ts +++ b/packages/grafana-ui/.storybook/preview.ts @@ -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; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ab3c96cb19e..20eea242476 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 7f0998d88cc..ab65542ead0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: