본문으로 건너뛰기
cccskills
무료GitHub에 공개

signoz-page-story

Build Storybook page stories for SigNoz by mapping API requests, display states, and URL parameters. Use msw mock responses, which simulate API replies, and controls to inspect loading, errors, permissions, and other page states.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

원문원문 설명 보기

Explore a SigNoz page, map the endpoints, states and query params it has, then write its Storybook page story with control-driven msw mocks that reach every state. Use when asked to create, extend or review a Storybook story for a page under frontend/src/pages, to add controls to an existing page story, or to write defineStoryMocks handlers and mock data for a page.

실행 점수 미검증

설치 방법 보기

이럴 때 유용합니다

  • Creating Storybook stories for SigNoz pages
  • Switching between loading, empty, and error states
  • Checking how permissions affect buttons and controls
  • Adding mock API responses and controls to existing stories

포함된 파일(4)

  • SKILL.md15.9 KB
  • references/controls.md8.6 KB
  • references/discovery.md3.0 KB
  • references/verify.md4.7 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

SigNoz page stories

A page story renders the real page inside the real app shell against msw, and its controls panel can reach every state the page has. The panel is the deliverable, not the story list.

frontend/src/storybook/README.md is the API surface (providers, parameters, control builders, module mocks, navigation). Read it first; this skill is the process on top of it.

Workflow

  1. Map the page: references/discovery.md. Produce the inventory (endpoints, states, params, permissions, caps) before writing code. No inventory, no story.
  2. Skeleton first: story + empty defineStoryMocks, then run it. The console names the endpoints step 1 missed.
  3. Inventory to controls: references/controls.md.
  4. Mock data and handlers: builders in __story_mockdata__, handlers in the page's mocks module.
  5. Verify in the browser: references/verify.md. Never report the story as done without it.

Where it lands in the sidebar

The sidebar mirrors the app's own side nav (container/SideNav/menuItems.tsx), so a page sits where someone would click it in the product. Four things decide that, and all four are part of writing the story, not a follow-up.

Title. Pages/<Area>/<Page>, where <Area> is the nav section and <Page> is the label the nav gives it.

  • The leaf is the product's label, never the component's name: MetricsExplorer is Metrics/Explorer, MeterExplorer is Metering/Cost Meter, AIAssistantPage is Noz.
  • Never repeat the area in the leaf: Alerts/Rules, not Alerts/AlertRules.
  • A leaf never shares its name with a sibling folder. The folder wins and the page becomes List, or Overview for a tab strip: Services/List beside Services/Detail.
  • Title Case with spaces. No camelCase, no kebab.
  • Four levels is the floor to stay under: Pages/Alerts/Channels/New is as deep as it goes.
  • Pages nobody navigates to on purpose go under Pages/System (Status, Unauthorized, Workspace Locked), and the pre-session pages under Pages/Auth.
  • A page whose permission stories earn their own folder becomes one: Pages/Settings/Billing/Overview beside Pages/Settings/Billing/Authz. See Permission stories below.

Order. The storySort.order literal in .storybook/preview.tsx carries the order for every level. A new page in an existing area is appended to that area's array, in the order the product lists it; a new area goes where the side nav puts it. Storybook parses the order out of the file statically, so it has to stay an inline literal. Missing entries fall to the end of their level rather than disappearing, so a forgotten edit is a page at the bottom of its area, not a broken sidebar.

Tags. Declared on the meta, right under title, and what the sidebar's tag filter answers questions with. Only these:

TagWhen
authzThe page gates UI on permission checks through lib/authz (AuthZButton, AuthZGuard, useAuthZ). Both the page's file and its Authz file carry it.
role-gatedThe page still branches on the legacy role (user.role, hasEditPermission) and has no authz check.
betaisBeta on its nav entry. Drop the tag when the product drops the badge.
legacySuperseded by another page but still routed. The doc comment names the page to start from instead.
playThe story file has a play function, so at least one state is reached by an interaction.

autodocs comes from preview.tsx and is never written on a meta.

Doc comment on the meta. What the page is, in the page's own terms, then a blank line, then the route:

const pageStory = storyMocks(logsExplorerMocks, {
	route: explorerRoute('explorer'),
	layout: 'app',
});

/**
 * The logs explorer: the query builder, the list, the frequency chart and the log
 * detail drawer, with quick filters and saved views beside them.
 *
 * Route: `/logs/logs-explorer`.
 */
const meta = {
	title: 'Pages/Logs/Explorer',
	tags: ['play'],
	component: LogsModulePage,
	...pageStory,
	parameters: { ...pageStory.parameters },
} satisfies Meta<LogsExplorerArgs>;

The pageStory const and the trailing parameters line are what make the doc comment safe. The comment compiles to a parameters property that the csf plugin appends after the spread, so a meta that spreads storyMocks(...) and stops there loses parameters.signoz and renders the page against the global handlers alone: every one of the page's endpoints misses. Restating parameters as a literal gives the plugin something to merge into. resolveStory logs the combination that says it happened, so the console names it rather than leaving it to be found by reading the page.

It is the description on the page's Docs page, which is the only place a reader who is not in the code finds out what the page is for. Two or three sentences: what it shows, what drives it, and the gating worth knowing about (Gated on authz permissions, follows the legacy editor role). A control-driven route says so instead of a path: Route: `/metrics-explorer/*`, the tab control picks which.

Permission stories

A page that gates UI on lib/authz keeps its permission states in a folder of their own, so the page's own file stays about the page and the sidebar answers "what does this permission do" in one place.

Layout. A second story file at stories/authz/<Page>.authz.stories.tsx, titled Pages/<Area>/<Page>/Authz, which turns the page into a folder: its own file is retitled Pages/<Area>/<Page>/Overview, and .storybook/preview.tsx gains the sub-order ('Billing', ['Overview', 'Authz']). Both files carry the authz tag and share the page's one mocks module, which the authz file imports as ../<Page>.stories.mocks. It declares no controls and no mock data of its own: a permission story that needs a new response is a control the page's mocks were missing.

One story per permission the page reads, named for what is gone: NoRead, NoList, NoUpdate, NoCreate, NoDelete. Then the combinations the page itself distinguishes, and only those: NoManage where two permissions gate one button, ReadOnly where everything but reading is denied, NoSubscriptionAccess where none of the resource's permissions are held, and CheckFailed for authzState: 'error', which is the page's fail-open path rather than a denial.

Revoke, never allow-list. Each story is a full grant minus what its name says: args: { revoked: ['read:subscription'] }. The Revoked control subtracts from the preset, so the story stays "an admin missing one permission" as the catalogue grows, and the diff against the page's Default is the one permission. Rebuilding the allow-list by hand drifts the moment a resource is added.

Never a role preset in this folder. access: 'viewer' moves the legacy role, the side nav and every other resource's permissions at the same time, so the story no longer shows what its name claims. A persona is a story on the page's own file, and only when the product has that persona.

Pair the revocation with the state that renders the gated control. A button that only exists on a trial needs the plan too: args: { plan: 'on-trial', revoked: ['create:subscription'] }. A permission whose denial changes nothing on screen gets no story: say so in the PR.

Check these by what is turned off, not by what the page says. The page reads the same either way, so a story that is wrong still looks right. Check that the control the permission guards is off, and that the denial message is there or gone.

Do not use toBeEnabled or toBeDisabled. They only look at the disabled attribute. @signozhq/ui turns a button off with aria-disabled instead, so these matchers pass on a button nobody can click, and the test goes green while clicking straight through a denial. Check the attribute the component really sets:

expect(save).toHaveAttribute('aria-disabled', 'true');

Some controls are off with no attribute at all. The browser turns off everything inside a disabled <fieldset> without marking it, and a switch is a <button> underneath. For those, an attribute check proves nothing, and so does looking for a parent fieldset[disabled]. Click it and check nothing happened:

const before = toggle.getAttribute('aria-checked');
await userEvent.click(toggle);
await expect(toggle.getAttribute('aria-checked')).toBe(before);

Check that a new assertion can fail. Flip it once and run it. If that story goes red and nothing else does, the assertion is real; put it back. A test that cannot fail is worse than no test, because it looks like coverage.

Delete a story once its difference disappears. Say a story checks that a button reads "View" rather than "Edit" when a permission is missing. If the label later becomes the same either way, that story passes no matter which permissions are held, including all of them, and it is testing nothing. When a behaviour stops depending on a permission, delete the story or rewrite it to lock in the new behaviour, and say which one you did in the PR.

Rules

  • Default is the loaded page. export const Default: Story = {} with no args, every widget carrying data. Empty, loading and failed are variants or control values, never the default.

  • A control is a knob on a response, resolved through handlers, config or effect. Never a component prop, never a module mock added for one story.

  • Every branch in the inventory is reachable from the panel. A state that needs a code edit to see is a missing control.

  • Never re-declare what every story already has: banner, side nav, data state (loaded/loading/error), access preset, permissions, check state.

  • A variant earns a story only when it is worth linking to: an empty workspace, a viewer, a page mid-load. Everything else stays a control.

  • Endpoints the page owns go through response.json, so the Data control covers loaded, loading and failed in one declaration. Endpoints the page cannot render without (ingestion detection, preferences, feature payloads) take a plain resolver so the shell survives the loading and error states.

  • Query-param state starts from route (/logs?tab=explorer). In-page param navigation works inside a story; a different pathname is blocked and reported by the overlay. A control for a param is worth it only when the param is a page mode someone would want to flip.

  • File layout: every story file for a page lives under src/pages/<Page>/stories/: <Page>.stories.tsx, <Page>.stories.mocks.tsx, payload builders in stories/__story_mockdata__/<page>.ts. Nothing page-specific in src/storybook/controls/. A page that is a tab strip over several routes gets one story file per tab, in its own folder under the module page (LogsModulePage/Pipelines/stories/Pipelines.stories.tsx), each with its own mocks and __story_mockdata__/; the builders more than one tab needs stay in the module page's own stories/__story_mockdata__/ (AlertList/stories/__story_mockdata__/alerts.ts), which a tab reaches as ../../stories/__story_mockdata__/alerts. Every one of them renders the module page, so the tab strip is there, and the route its mocks return decides which tab is open. A page's permission stories go one level further down, in stories/authz/<Page>.authz.stories.tsx, on the page's own mocks: see Permission stories.

  • A state only a click reaches is a story with a play function, not a control: a drawer, a modal, an edit mode the page holds in component state. Drive it with userEvent and the queries from storybook/test, take the first of a repeated row action, and wait on the state's own text. The page fetches before it renders a row, so the finder needs a timeout past the 1s default. A state the app drops again on its own, such as one keyed on an array identity that a refetch replaces, does not get a story: it would not survive being looked at. A sequence of such states, a wizard's steps or a questionnaire's pages, is still a control: declare the steps in the mocks module and walk them from a play on the meta that destructures mount, which is what makes Storybook replay it on an arg change. See references/controls.md.

  • The mocks are AI-owned and say so. <Page>.stories.mocks.tsx and every file under a __story_mockdata__/ open with this banner, above the imports:

    /**
     * AI-owned. Generated and maintained by the `signoz-page-story` skill.
     * Do not hand-edit: regenerate instead.
     */
    

    The root .gitattributes marks both paths linguist-generated=true, so the reviewer gets them collapsed and spends the attention on the rendered page. The story file is the human surface and never carries the banner. A file with the banner has to stay regenerable from the page alone: no page knowledge in src/storybook/, and builders typed from src/api/generated where the endpoint has types, so a contract change is a compile error instead of a mock that lies.

  • Reuse fixtures from src/mocks-server/ and src/tests/fixtures/ where they exist. An endpoint jest needs too belongs in src/mocks-server/handlers.ts.

  • Shared response builders live in src/storybook/msw/__story_mockdata__/: typed helpers like queryRangeV5ScalarResponse that multiple pages need. Before writing a response shape inline, check if a builder exists; if not and the shape will repeat, add it there. Page-specific builders stay in the page's __story_mockdata__/.

  • The story's own doc comment is per state. Every export const gets one: what that state shows, not how it is built. It renders in the States list on the page's Docs page, so Undocumented. there is a story nobody described.

  • Story names come from a fixed vocabulary where one fits: Default, Viewer, Empty, Loading, Error. Page-specific states get page-specific names (NoIngestion, Unlicensed), never a second spelling of one of those (ViewerAccess, NonAdmin).

  • No comment is the default. Write one only for what the code cannot show: a shape the backend dictates, an app bug the mock reproduces, an ordering or cap the page depends on, a workaround and the reason for it. Never restate a name, a type, or what a builder plainly builds; if the sentence reads as the signature in prose, delete it. Nothing addressed to a reviewer. The one comment a story always gets is its own doc comment: what it shows, in the page's own terms.

Done means

  • Default shows the page with data, checked in dark and light
  • title follows the sidebar rules, tags declared, and the page's entry added to the storySort.order literal in .storybook/preview.tsx
  • the meta carries its doc comment with the Route: line, the meta restates parameters: { ...pageStory.parameters } after the spread, and every story export carries its own doc comment
  • the page's Docs page renders: description, controls table, and one row per state with no Undocumented.
  • a page tagged authz has its Authz folder: one story per permission it reads, each reached by revoked, none of them a role preset, and each one checked by the disabled state of what the permission gates
  • the mocks module and every __story_mockdata__ file carry the AI-owned banner
  • every control flipped once, its effect seen on screen
  • console clean: no [storybook] no msw handler, no 501, no msw unhandled request, no React warning
  • no navigation overlay on mount
  • pnpm tsgo --noEmit, pnpm exec oxlint <files>, pnpm exec oxfmt --check <files> all clean. The repo has no prettier: pnpm exec prettier prints a pass while exiting 254

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Refactor state held in Redux or React Context by separating API data, URL settings, shared interface state, and local component state. Move each part to React Query, nuqs, Zustand, or React’s local state hooks.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Migrating away from Redux or React Context
  • Removing duplicate storage of API responses
  • Sharing filters and pagination through URLs
SigNoz/signoz3.2만2026년 10월 23일 업데이트

Compare Storybook screenshots before and after a code change in both dark and light themes. Check rendering noise, annotate meaningful differences, and export a shareable visual report that helps reviewers see what changed and what stayed identical.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Compare screens before and after a code change
  • Check visual regressions in dark and light themes
  • Share annotated screenshot comparisons in a PR
SigNoz/signoz3.2만2026년 10월 23일 업데이트

Generate page and component folder structures with a command, keeping related files together by feature. Create tabbed page shells, starter tests, and README files, and register page routes within the supplied frontend structure.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Starting a page with a consistent folder structure
  • Creating a page shell with multiple tabs
  • Adding components within a feature
SigNoz/signoz3.2만2026년 10월 23일 업데이트

SigNoz의 스킬 모두 보기

이 스킬의 문제 신고