Compare commits

...

6 Commits

Author SHA1 Message Date
Vinícius Lourenço
0aacce75f4 chore(ci): add support to run sharded by default 2026-09-11 09:29:50 -03:00
Vinícius Lourenço
075e91cd2e fix(home): add rowKey to services table 2026-09-10 23:56:32 -03:00
Vinícius Lourenço
b6bd48e20d fix(storybook): serve the webfonts from public/fonts 2026-09-10 23:56:31 -03:00
Vinícius Lourenço
785982e194 fix(storybook): answer the google fonts stylesheet 2026-09-10 23:56:31 -03:00
Vinícius Lourenço
85681dd316 fix(src): little fixes to pass the storybook test 2026-09-10 23:56:31 -03:00
Vinícius Lourenço
f2a38323b7 ci(storybook): run storybook test-runner in CI 2026-09-10 23:56:31 -03:00
13 changed files with 1297 additions and 32 deletions

View File

@@ -79,6 +79,36 @@ jobs:
run: |
cd frontend && pnpm generate:api
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run pnpm generate:api in frontend/ locally and commit."; exit 1)
storybook:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: mcr.microsoft.com/playwright:v1.57.0-noble
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: install-pnpm
uses: pnpm/action-setup@v6
with:
version: 10
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: install-frontend
run: cd frontend && pnpm install
- name: test-storybook
run: cd frontend && pnpm test:storybook --shard=${{ matrix.shard }}/${{ strategy.job-total }}
web-settings:
if: |
github.event_name == 'merge_group' ||

View File

@@ -1,9 +1,11 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet"
/>
<!--
`index.html` links Inter from Google Fonts here. The link is parsed before the
msw worker has started, so that one request escapes the iframe while every
other font request the app makes is answered with an empty stylesheet; the
local faces stand in for both.
-->
<link rel="stylesheet" href="storybook-fonts.css" />
<link rel="stylesheet" href="css/uPlot.min.css" />
<script>

View File

@@ -0,0 +1,50 @@
/*
* The five families the app pulls from Google Fonts, served from the files in
* `public/fonts` instead. `msw/appShellHandlers.ts` answers the CDN with an
* empty stylesheet so no request leaves the iframe, which without this left
* every story on a fallback for the four families `src/styles.scss` imports at
* runtime, and on whatever the Inter link in `index.html` happened to fetch
* before the worker had started.
*/
@font-face {
font-family: 'Inter';
/* The comma is part of the filename and the dev server serves it raw, so it
stays literal inside the quoted url rather than percent-encoded. */
src: url('fonts/Inter-VariableFont_opsz,wght.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Work Sans';
src: url('fonts/WorkSans-VariableFont_wght.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Space Mono';
src: url('fonts/SpaceMono-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Fira Code';
src: url('fonts/FiraCode-VariableFont_wght.ttf') format('truetype');
font-weight: 300 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('fonts/GeistMonoVF.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}

View File

@@ -0,0 +1,85 @@
import { getStoryContext } from '@storybook/test-runner';
import type { TestRunnerConfig } from '@storybook/test-runner';
import type { Page } from 'playwright';
const IGNORED_MESSAGES = [
// `preview-head.html` swaps a local stylesheet in for Google Fonts, but the
// browser still warns on the real cross-origin one it briefly requests
// before msw starts (no CORS headers), regardless of story content.
/Can't access cssRules/,
// Pre-existing dev-server noise, unrelated to any story.
/Couldn't load preload assets/,
// Fires because a Jest-driven browser sets a global testing flag React
// checks for; unrelated to anything a story does.
/current testing environment is not configured to support act/,
// React and antd route dev-only warnings (missing keys, DOM nesting, API
// deprecations) through `console.error` under this prefix; app-wide and
// tracked separately from story regressions.
/^Warning: /,
// msw's own warning when its response listener count grows across many
// story visits in one browser session; not a story defect.
/MaxListenersExceededWarning/,
// `preview-head.html`'s CSP intentionally blocks third-party iframes
// (YouTube embeds, the docs pane) so they hit the real network instead of
// an unanswered msw request; the block is the point, not a bug.
/violates the following Content Security Policy directive/,
];
const messagesByPage = new WeakMap<Page, string[]>();
/**
* Only `console.error` fails a story. `console.warn` is dev-time advice from
* app code (e.g. `aggregateData is null`) and from the runner itself; an
* unmocked `/api/` call is a `console.error` in `src/storybook/msw/handlers.ts`.
*/
const config: TestRunnerConfig = {
// msw logs every mocked request at `log`; keep it out of the failure dump
// unless the job is re-run with debug logging (GitHub sets RUNNER_DEBUG=1).
logLevel: process.env.RUNNER_DEBUG === '1' ? 'info' : 'warn',
async preVisit(page): Promise<void> {
const existing = messagesByPage.get(page);
if (existing) {
existing.length = 0;
return;
}
const messages: string[] = [];
messagesByPage.set(page, messages);
page.on('console', (message) => {
if (
message.type() === 'error' &&
!IGNORED_MESSAGES.some((pattern) => pattern.test(message.text()))
) {
messages.push(`[error] ${message.text()}`);
}
});
// The console message alone ("Failed to load resource") doesn't name the
// URL; pairing it with the response is what makes a missing mock
// actionable instead of just a status code.
page.on('response', (response) => {
if (response.status() >= 400) {
messages.push(`[response] ${response.status()} ${response.url()}`);
}
});
},
async postVisit(page, context): Promise<void> {
const messages = messagesByPage.get(page) ?? [];
if (messages.length === 0) {
return;
}
// A story that deliberately mocks a failure response (e.g. a 500 to test
// an error state) logs the error it's testing for; opt it out per-story
// with `parameters: { allowConsoleErrors: true }`.
const storyContext = await getStoryContext(page, context);
if (storyContext.parameters?.allowConsoleErrors) {
return;
}
throw new Error(
`Story "${context.name}" logged console error/warning:\n${messages.join('\n')}`,
);
},
};
export default config;

View File

@@ -9,6 +9,7 @@
"dev": "vite",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build -o storybook-static",
"test:storybook": "bash scripts/test-storybook.sh",
"build": "vite build",
"preview": "vite preview",
"prettify": "oxfmt",
@@ -162,6 +163,7 @@
"@jest/types": "30.2.0",
"@storybook/addon-a11y": "10.5.9",
"@storybook/react-vite": "10.5.9",
"@storybook/test-runner": "0.24.5",
"@testing-library/dom": "8.20.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
@@ -233,7 +235,7 @@
},
"overrides": {
"@babel/core@<=7.29.0": ">=7.29.6 <8",
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
"@istanbuljs/load-nyc-config>js-yaml": ">=4.3.1 <5",
"cookie@<0.7.0": ">=0.7.1 <1",
"dompurify@<=3.4.10": ">=3.4.11 <4",
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
@@ -242,6 +244,14 @@
"prismjs@<1.30.0": ">=1.30.0 <2",
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2",
"brace-expansion@<1.1.18": ">=1.1.18 <2",
"brace-expansion@>=2.0.0 <2.1.4": ">=2.1.4 <3",
"brace-expansion@>=5.0.0 <5.0.9": ">=5.0.9 <6",
"fast-uri@<3.1.5": ">=3.1.5 <4",
"immutable@<5.1.8": ">=5.1.8 <6",
"js-yaml@>=4.0.0 <4.3.1": ">=4.3.1 <5",
"less@<4.5.0": ">=4.5.0 <5",
"nanoid@<3.3.18": ">=3.3.18 <4"
}
}

1076
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
story_count=$(find src -name '*.stories.tsx' | wc -l)
if [ "$story_count" -eq 0 ]; then
echo "No *.stories.tsx found under src/" >&2
exit 1
fi
# jest splits the sorted story files into contiguous shards and exits 1 when a
# shard is empty, which happens on every shard above the file count. The runner
# rejects jest's own `--passWithNoTests`, so skip those shards here.
for arg in "$@"; do
if [[ $arg == --shard=* ]]; then
shard_index=${arg#--shard=}
if [ "${shard_index%%/*}" -gt "$story_count" ]; then
echo "Skipping ${arg#--shard=}: only ${story_count} story files"
exit 0
fi
fi
done
pnpm storybook --ci --quiet &
SB_PID=$!
trap 'kill "$SB_PID" 2>/dev/null || true' EXIT
until curl -sf http://127.0.0.1:6006/index.json >/dev/null 2>&1; do
sleep 1
done
pnpm exec test-storybook --ci --maxWorkers=2 "$@"

View File

@@ -1,4 +1,4 @@
import { AxiosError, AxiosResponse } from 'axios';
import { AxiosError, AxiosResponse, isCancel } from 'axios';
import { ErrorResponse } from 'types/api';
import { ErrorStatusCode } from 'types/common';
@@ -42,6 +42,16 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
};
}
if (request) {
// Avoid logging error when the request was just cancelled for whatever reason
if (isCancel(error)) {
return {
statusCode: 500,
payload: null,
error: 'Something went wrong',
message: null,
};
}
// client never received a response, or request never left
console.error('client never received a response, or request never left');

View File

@@ -376,7 +376,9 @@ export default function BillingContainer(): JSX.Element {
</Typography.Link>
</AuthZTooltip>
{` if your payment information has changed. Email us at `}
<Typography.Text color="muted">cloud-support@signoz.io</Typography.Text>
<Typography.Text as="span" color="muted">
cloud-support@signoz.io
</Typography.Text>
{` otherwise. Be sure to provide this information immediately to avoid interruption to your service.`}
</Typography>
);

View File

@@ -174,6 +174,7 @@ export default function ServiceTraces({
columns={columns}
dataSource={top5Services}
pagination={false}
rowKey="serviceName"
className="services-table"
onRow={(record: ServicesList): Record<string, unknown> => ({
onClick: (event: React.MouseEvent): void => {

View File

@@ -216,7 +216,7 @@ function K8sOptionsSidePanel<TData>({
);
return (
<div className={styles.columnItem} key={column.id}>
<Typography.Text size="sm" className={styles.columnLabel}>
<Typography.Text as="span" size="sm" className={styles.columnLabel}>
{column.label}
</Typography.Text>
{column.canBeHidden ? (

View File

@@ -10,7 +10,7 @@ function Shortcuts(): JSX.Element {
const tableData = generateTableData(shortcutSection);
return (
<section className="shortcut-section">
<section key={shortcutSection} className="shortcut-section">
<Typography.Text className="shortcut-section-heading">
{shortcutSection}
</Typography.Text>

View File

@@ -45,4 +45,12 @@ export const appShellHandlers = [
rest.get('https://cms.signoz.cloud/api/release-changelogs', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(changelogResponse)),
),
// The webfonts `index.html` links and `styles.scss` imports. The story
// declares the same families over `public/fonts` in
// `.storybook/public/storybook-fonts.css`, so answering the CDN with nothing
// keeps a request from leaving the browser on every story.
rest.get('https://fonts.googleapis.com/css2', (_req, res, ctx) =>
res(ctx.status(200), ctx.text('')),
),
];