grafana-flamegraph: Add storybook stories for major components (#113375)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// This script is used to copy assets from the public folder to a temporary static folder within
|
||||
// the .storybook directory.
|
||||
// We selectively limit assets that are uploaded to the Storybook bucket to prevent rate limiting
|
||||
// when publishing new storybook.
|
||||
// Note: Storybook has a static copying feature but it copies entire directories which can contain thousands of icons.
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// avoid importing from @grafana/data to prevent error: window is not defined
|
||||
import { availableIconsIndex, IconName } from '../../grafana-data/src/types/icon.ts';
|
||||
import { getIconSubDir } from '../../grafana-ui/src/components/Icon/utils.ts';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
// doesn't require uploading 1000s of unused assets.
|
||||
const iconPaths = Object.keys(availableIconsIndex)
|
||||
.filter((iconName) => !iconName.startsWith('fa '))
|
||||
.map((iconName) => {
|
||||
const subDir = getIconSubDir(iconName as IconName, 'default');
|
||||
return {
|
||||
from: `../../../public/img/icons/${subDir}/${iconName}.svg`,
|
||||
to: `./static/public/build/img/icons/${subDir}/${iconName}.svg`,
|
||||
};
|
||||
});
|
||||
|
||||
export function copyAssetsSync() {
|
||||
const assets = [
|
||||
{
|
||||
from: '../../../public/fonts',
|
||||
to: './static/public/fonts',
|
||||
},
|
||||
{
|
||||
from: '../../../public/img/grafana_text_logo-dark.svg',
|
||||
to: './static/public/img/grafana_text_logo-dark.svg',
|
||||
},
|
||||
{
|
||||
from: '../../../public/img/grafana_text_logo-light.svg',
|
||||
to: './static/public/img/grafana_text_logo-light.svg',
|
||||
},
|
||||
{
|
||||
from: '../../../public/img/fav32.png',
|
||||
to: './static/public/img/fav32.png',
|
||||
},
|
||||
{
|
||||
from: '../../../public/lib',
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
for (const asset of assets) {
|
||||
const fromPath = resolve(__dirname, asset.from);
|
||||
const toPath = resolve(__dirname, asset.to);
|
||||
if (!fs.existsSync(toPath)) {
|
||||
fs.copySync(fromPath, toPath, {
|
||||
filter: (src) => !fs.lstatSync(src).isSymbolicLink(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { StorybookConfig } from '@storybook/react-webpack5';
|
||||
import { copyAssetsSync } from './copyAssets.ts';
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
copyAssetsSync();
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.story.tsx'],
|
||||
addons: [
|
||||
{
|
||||
name: '@storybook/preset-scss',
|
||||
options: {
|
||||
styleLoaderOptions: {
|
||||
// this is required for theme switching .use() and .unuse()
|
||||
injectType: 'lazyStyleTag',
|
||||
},
|
||||
cssLoaderOptions: {
|
||||
url: false,
|
||||
importLoaders: 2,
|
||||
},
|
||||
sassLoaderOptions: {
|
||||
sassOptions: {
|
||||
// silencing these warnings since we're planning to remove sass when angular is gone
|
||||
silenceDeprecations: ['import', 'global-builtin'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'@storybook/addon-webpack5-compiler-swc',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ['static'],
|
||||
webpackFinal: async (config) => {
|
||||
const webpack = await import('webpack');
|
||||
|
||||
// Define process.env for browser context
|
||||
config.plugins?.push(
|
||||
new webpack.default.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
|
||||
'process.env.STORYBOOK_THEME': JSON.stringify(process.env.STORYBOOK_THEME || 'system'),
|
||||
})
|
||||
);
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Preview } from '@storybook/react';
|
||||
|
||||
import { getBuiltInThemes, getTimeZone, getTimeZones, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { withTheme } from '../src/utils/storybook/withTheme';
|
||||
import { withTimeZone } from '../src/utils/storybook/withTimeZone';
|
||||
|
||||
const allowedExtraThemes: string[] = [];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
allowedExtraThemes.push('debug');
|
||||
allowedExtraThemes.push('desertbloom');
|
||||
allowedExtraThemes.push('gildedgrove');
|
||||
allowedExtraThemes.push('gloom');
|
||||
allowedExtraThemes.push('sapphiredusk');
|
||||
allowedExtraThemes.push('tron');
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [withTheme(), withTimeZone()],
|
||||
globalTypes: {
|
||||
theme: {
|
||||
name: 'Theme',
|
||||
description: 'Global theme for components',
|
||||
defaultValue: 'system',
|
||||
toolbar: {
|
||||
icon: 'paintbrush',
|
||||
items: getBuiltInThemes(allowedExtraThemes).map((theme) => ({
|
||||
value: theme.id,
|
||||
title: theme.name,
|
||||
})),
|
||||
showName: true,
|
||||
},
|
||||
},
|
||||
timeZone: {
|
||||
description: 'Set the timezone for the storybook preview',
|
||||
defaultValue: getTimeZone(),
|
||||
toolbar: {
|
||||
icon: 'globe',
|
||||
items: getTimeZones(true)
|
||||
.filter((timezone) => !!timezone)
|
||||
.map((timezone) => ({
|
||||
title: timezone,
|
||||
value: timezone,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
initialGlobals: {
|
||||
theme: process.env.STORYBOOK_THEME || 'system',
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -36,7 +36,8 @@
|
||||
"clean": "rimraf ./dist ./compiled ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
"postpack": "mv package.json.bak package.json",
|
||||
"storybook": "storybook dev -p 6006"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults",
|
||||
@@ -60,11 +61,15 @@
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@rollup/plugin-node-resolve": "16.0.1",
|
||||
"@storybook/addon-webpack5-compiler-swc": "^4.0.2",
|
||||
"@storybook/react": "^10.0.8",
|
||||
"@storybook/react-webpack5": "^10.0.8",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "^6.1.2",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@types/d3": "^7",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/node": "24.10.1",
|
||||
@@ -73,11 +78,13 @@
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"babel-jest": "29.7.0",
|
||||
"esbuild": "0.25.8",
|
||||
"fs-extra": "^11.3.2",
|
||||
"jest": "^29.6.4",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-esbuild": "6.2.1",
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
"storybook": "^10.0.8",
|
||||
"ts-jest": "29.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.2"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
|
||||
import { ColorScheme, SelectedView } from '../types';
|
||||
|
||||
import FlameGraph from './FlameGraph';
|
||||
import { CollapsedMap, FlameGraphDataContainer } from './dataTransform';
|
||||
import { data } from './testData/dataBasic';
|
||||
|
||||
const meta: Meta<typeof FlameGraph> = {
|
||||
title: 'FlameGraph',
|
||||
component: FlameGraph,
|
||||
args: {
|
||||
rangeMin: 0,
|
||||
rangeMax: 1,
|
||||
textAlign: 'left',
|
||||
colorScheme: ColorScheme.PackageBased,
|
||||
selectedView: SelectedView.Both,
|
||||
search: '',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const Basic: StoryObj<typeof meta> = {
|
||||
render: (args) => {
|
||||
const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: false });
|
||||
|
||||
return <FlameGraph {...args} data={dataContainer} collapsedMap={new CollapsedMap()} />;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { DataFrameDTO } from '@grafana/data';
|
||||
|
||||
export const data: DataFrameDTO = {
|
||||
name: 'response',
|
||||
refId: 'A',
|
||||
// @ts-ignore
|
||||
meta: { preferredVisualisationType: 'flamegraph' },
|
||||
fields: [
|
||||
{
|
||||
name: 'level',
|
||||
values: [0, 1, 2, 3, 2, 1],
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
values: [10000, 5000, 4000, 3000, 500, 3000],
|
||||
config: {
|
||||
unit: 'ms',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'self',
|
||||
values: [10000, 500, 1000, 3000, 500, 3000],
|
||||
config: {
|
||||
unit: 'ms',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'label',
|
||||
values: ['total', 'fn_1', 'fn_2', 'fn_3', 'fn_4', 'fn_5'],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { data } from './FlameGraph/testData/dataNestedSet';
|
||||
import FlameGraphContainer, { Props } from './FlameGraphContainer';
|
||||
|
||||
const WrappedFlameGraph = (props: Omit<Props, 'getTheme'>) => {
|
||||
const theme = useTheme2();
|
||||
const df = createDataFrame(data);
|
||||
return <FlameGraphContainer {...props} data={df} getTheme={() => theme} />;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof FlameGraphContainer> = {
|
||||
title: 'FlameGraphContainer',
|
||||
render: (args) => {
|
||||
return <WrappedFlameGraph {...args} />;
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const Basic: StoryObj<typeof meta> = {};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { createDataFrame } from '@grafana/data';
|
||||
|
||||
import { FlameGraphDataContainer } from '../FlameGraph/dataTransform';
|
||||
import { data } from '../FlameGraph/testData/dataNestedSet';
|
||||
import { ColorScheme } from '../types';
|
||||
|
||||
import FlameGraphTopTableContainer from './FlameGraphTopTableContainer';
|
||||
|
||||
const meta: Meta<typeof FlameGraphTopTableContainer> = {
|
||||
title: 'TopTable',
|
||||
component: FlameGraphTopTableContainer,
|
||||
args: {
|
||||
colorScheme: ColorScheme.ValueBased,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ width: '100%', height: '600px' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
export const Basic: StoryObj<typeof meta> = {
|
||||
render: (args) => {
|
||||
const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: true });
|
||||
|
||||
return (
|
||||
<FlameGraphTopTableContainer
|
||||
{...args}
|
||||
data={dataContainer}
|
||||
onSymbolClick={() => {}}
|
||||
onSearch={() => {}}
|
||||
onSandwich={() => {}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -144,6 +144,7 @@ function buildFilteredTable(data: FlameGraphDataContainer, matchedLabels?: Set<s
|
||||
// Add current call to the stack
|
||||
callStack.push(label);
|
||||
}
|
||||
|
||||
return filteredTable;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Decorator } from '@storybook/react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { getThemeById, ThemeContext } from '@grafana/data';
|
||||
import { GlobalStyles } from '@grafana/ui';
|
||||
|
||||
interface ThemeableStoryProps {
|
||||
themeId: string;
|
||||
}
|
||||
const ThemeableStory = ({ children, themeId }: React.PropsWithChildren<ThemeableStoryProps>) => {
|
||||
const theme = getThemeById(themeId);
|
||||
|
||||
const css = `
|
||||
#storybook-root {
|
||||
padding: ${theme.spacing(2)};
|
||||
}
|
||||
|
||||
body {
|
||||
background: ${theme.colors.background.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
<GlobalStyles />
|
||||
|
||||
<style>{css}</style>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const withTheme =
|
||||
(): Decorator =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
(story, context) => <ThemeableStory themeId={context.globals.theme}>{story()}</ThemeableStory>;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Decorator } from '@storybook/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { setTimeZoneResolver } from '@grafana/data';
|
||||
|
||||
export const withTimeZone = (): Decorator => (Story, context) => {
|
||||
useEffect(() => {
|
||||
setTimeZoneResolver(() => context.globals.timeZone ?? 'browser');
|
||||
}, [context.globals.timeZone]);
|
||||
|
||||
return Story();
|
||||
};
|
||||
Reference in New Issue
Block a user