Compare commits

...

22 Commits

Author SHA1 Message Date
nityanandagohain
960711ae21 Merge remote-tracking branch 'origin/main' into issue_6107_5 2026-09-23 15:49:35 +05:30
nityanandagohain
d833c2d5b3 fix: add ai-o11y quick filter migration 2026-09-23 15:34:01 +05:30
Naman Verma
362d3a4fdf fix: backfill notification channel tuples (#12960)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
cacheci / tests (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

Same migration logic as number 128. Needed for enterprise servers as
these tuples decide whether a role may create, list, read, update or
delete channels at all. An existing org with zero channels still needs
them, otherwise its admin can't create the first one or even list the
empty page.
2026-09-23 09:48:11 +00:00
Jugal Kishore
ff80758e1c feat(onboarding): add Cursor, Cline, vLLM, SGLang, Karpenter datasources (#12944)
#### Description

- Adds 20 data sources to the onboarding picker: Cursor, Antigravity
CLI, Qwen Code, DeepSeek Harness, Meta Muse Code, Meta Muse Spark,
OpenAI Agents SDK, Cline, E2B Sandbox, Daytona Sandbox, Vercel Sandbox,
Modal, vLLM, SGLang, Karpenter, Podman, EMQX, AWS RDS Aurora, GCP Cloud
Storage, and AWS Lambda MicroVMs.
- Go now asks for an instrumentation method (SDK / compile-time `otelc`
/ eBPF) before the environment question, so the new zero-code guides are
reachable. The card links to the new comparison overview.
- The Lambda → Traces question now asks how the function is packaged
instead of which runtime it uses, and gains a Container Image option.
Layers do not attach to container images, so runtime alone did not pick
the right guide.
- GCP Cloud Storage points at `/integrations/gcp?service=cloudstorage`
rather than a doc, matching the Cloud SQL and Memorystore cards.
`cloudstorage` is a first-class cloud integration with its own
dashboard, so the in-app flow is the one users want.
- New logos: `antigravity`, `aurora`, `cline`, `cursor`, `daytona`,
`e2b`, `emqx`, `karpenter`, `meta`, `modal`, `podman`, `sglang`, `vllm`.
Qwen Code, OpenAI Agents SDK and Lambda MicroVMs reuse existing marks.
- Recolours 15 pre-existing logos that were pure white or near-black and
disappeared against one of the two card backgrounds (`--l2-background`
is `#121317` dark, `#F9F9FB` light): `anthropic-api-monitoring`,
`clickhouse`, `confluent-kafka`, `datadog`, `deno`, `document-load`,
`from-log-file`, `haystack`, `kafka`, `langchain`, `ollama`, `openai`,
`openrouter`, `vercel`, `zap`. Brand hue kept where the brand has one,
adjusted to clear 3:1 against both; black-or-white marks use a neutral
grey.

#### Issues closed by this PR

Closes SigNoz/signoz.io#4205
Closes SigNoz/signoz.io#4197
Closes SigNoz/signoz.io#4178
Closes SigNoz/signoz.io#4171
Closes SigNoz/signoz.io#4167
Closes SigNoz/signoz.io#4153
Closes SigNoz/signoz.io#4144
Closes SigNoz/signoz.io#4133
Closes SigNoz/signoz.io#4131
Closes SigNoz/signoz.io#4129
Closes SigNoz/signoz.io#4125
Closes SigNoz/signoz.io#4108
Closes SigNoz/signoz.io#4092
Closes SigNoz/signoz.io#4088
Closes SigNoz/signoz.io#4082
Closes SigNoz/signoz.io#4074
Closes SigNoz/signoz.io#4063
Closes SigNoz/signoz.io#4032
Closes SigNoz/signoz.io#4024
2026-09-23 07:54:02 +00:00
Vinicius Lourenço
f9d425bf5c fix(storybook): remove timing races from story tests (#12954)
#### Description

A few tweaks to improve the resiliency of the storybook tests under CI
stress.
2026-09-23 07:46:27 +00:00
Abhi kumar
7ce73f3470 fix(dashboard): don't re-run an errored panel query on scroll back into view (#12958)
#### Description

- Lazy-loaded panels toggle `enabled` on viewport visibility.
react-query treats a key with no data as stale regardless of
`staleTime`, so an errored panel refetched (with retries on 5xx) every
time it scrolled back into view.
- `useGetQueryRangeV5` now keeps an errored key enabled, so only a key
change (time, variables, query) or the Retry button re-runs it. A new
key still stays gated while off-screen.
- Adds a `useGetQueryRangeV5` test suite covering the gating paths.
2026-09-23 07:04:51 +00:00
Nityananda Gohain
5aca8b0d3c chore: remove ai-o11y ff (#12947)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
Remove ai-o11y FF and enable it by default

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Part of https://github.com/SigNoz/engineering-pod/issues/6107
2026-09-23 06:39:16 +00:00
Gaurav Tewari
f1c9e0f1d0 feat(llm-observability): add ai o11y analytics events (#12952)
#### Description

- Renames AI Observability explorer events from `Traces Explorer: *` to
`AI Observability Explorer: *`, so they no longer mix with the regular
Traces Explorer events.
- Adds page-visit events for Overview, Attribute Mapping and Model
Pricing.
- Adds action events: attribute mapping saved and test run; model cost
saved and deleted; unpriced model mapped.

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-23 05:05:06 +00:00
Gaurav Tewari
10c0af327b fix: failing e2e for llm (#12953)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

- Attribute-mapping e2e now adds a condition key when creating its
group.
- Since #12809 the backend rejects groups without conditions (`400
condition must list at least one attribute or resource substring`), so
the spec failed on save.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

<!--If applicable, include screenshots or screen recordings that clearly
show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings

<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information

- Follow-up: we should add check on frontend as well for #12809 ( we
have already decided to add this later )

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-23 04:43:36 +00:00
Abhi kumar
370b278f28 fix(dashboard): reserve legend rows the grid actually lays out (#12951)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Bottom legends could silently drop series. The legend box reserved
fewer rows than the grid actually laid out, and the surplus row was
clipped away by the wrapper's `overflow: hidden` — nothing indicated the
series were still there apart from a scrollbar.
- The cause is two different formulas for the same quantity: how many
legend items fit on one row. The height reservation in
`calculateChartDimensions` used `floor((containerWidth - padding) /
itemWidth)`. The grid resolves `auto-fill` over `--legend-item-width`,
which is `itemWidth + LEGEND_ITEM_EXTRA_WIDTH`, separated by a column
gap, inside a scroller with its own gutter. Ignoring the extra width,
the gap and the gutter, the reservation over-counts and reserves one row
where the grid needs two.
- The two formulas only diverge over a narrow band of widths, so the
failure is width-dependent and its boundary is a single pixel. A panel
sitting near that boundary flips between states as the layout reflows,
which is seen as flickering rather than as a fixed layout bug.
- Fix: `legendItemsPerRow` now mirrors the `auto-fill` track count. The
two CSS values it depends on are pinned as constants beside the existing
`LEGEND_ROW_HEIGHT` / `LEGEND_ROW_GAP`, which already carry the same
"must match the stylesheet" caveat.

#### Additional Information

- Adds a regression test at a width where the two formulas diverge; it
fails on `main`.
- Two pre-existing gaps left out of scope and unchanged by this PR:
- `MAX_SHORT_PANEL_LEGEND_RATIO` deliberately reserves a single row on
very short panels while the grid still lays out two, so the clip remains
there. Closing it needs somewhere for the dropped row's series to go —
an overflow affordance, which is a design decision.
2026-09-23 02:22:18 +00:00
Pandey
f2229a1064 fix(analytics): format segment logger messages before passing to slog (#12950)
#### Description

- segment's `Logger` interface is printf-style, but the adapter passed
`format` as the slog message and `args` as key-value pairs.
- slog never substituted the `%d` placeholders and rendered each
positional arg as a `!BADKEY` attr.
- `Logf` and `Errorf` now `fmt.Sprintf` the message first, matching the
opamp logger adapter.
2026-09-22 19:18:36 +00:00
Vinicius Lourenço
099832b26b chore(codeowners): change ownership of storybook (#12949)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
## Description

Add myself as owner of storybook structural files, the stories still
belongs to each pod.
2026-09-22 18:04:23 +00:00
Ashwin Bhatkal
057571cf6d fix(dashboard): restore related values and API search in dynamic variable dropdowns (#12935)
#### Description

The V1 to V2 dashboard rewrite carried over the *request* for a dynamic
variable's values but not the *response* handling — `relatedValues` and
`complete` were fetched and then thrown away. Both issues below are that
single regression.

- **Related values.** The dropdown now splits a dynamic variable's
values into "Related Values" (scoped by the sibling dynamic variables'
selections) and "All Values", as V1 did. The `existingQuery` that scopes
them was already being sent; only the response was ignored. Worth
knowing while reviewing: the backend never narrows the main list by
`existingQuery` — `GetAllValues` doesn't see it, and `GetRelatedValues`
returns nothing when it is empty — so the scoping is only ever visible
as the second section.
- **Value search.** A variable whose list the backend truncated
(`complete: false`) could only be filtered against the values already
fetched, so typing anything outside that first batch found nothing.
Search now goes to the API. It runs on its own react-query, deliberately
not the fetch engine's, so a keystroke cannot settle the variable's
fetch cycle and re-cascade its dependent variables and panels.
- **Retry action.** Restores V1's gating: the shared select defaults
`showRetryButton` to `true`, so a 4xx offered a retry that could only
fail again.

Commits are split by concern in that order.

#### Screen Recording


https://github.com/user-attachments/assets/ef51f481-de66-4334-9a59-dc98a7c7e50f

#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/352
Closes https://github.com/SigNoz/pulse-pod/issues/249
2026-09-22 17:31:51 +00:00
Vinicius Lourenço
ccb6ef68c1 feat(storybook): add stories for each existing page (#12734)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### How to review this PR

You can pretty much ignore anything inside `<parent>/stories` folder
since this was generated by AI and should be maintained by AI. If you
want to suggest a change in a specific story, let's have a follow-up for
it.

You should focus to review the non-essential files that were changed,
such as the skills or storybook files.

#### Description

This adds stories for all reachable pages in the app, with few
variations in the state of the page.

This can be used as ground-work to later each team add more
customizations/states for their controlled pages, to ensure we are
covering all the states available and having a good coverage during
visual testing.

Closes https://github.com/SigNoz/engineering-pod/issues/6093

<!--If applicable, include screenshots or screen recordings that clearly
show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings

<img width="1867" height="1268" alt="image"
src="https://github.com/user-attachments/assets/78f2f10e-e8bb-4353-a78b-1a88df4bb545"
/>
2026-09-22 16:50:00 +00:00
Gaurav Tewari
c6f448409a fix: AI o11y ui testing bug bash (#12933)
## Description

this PR fixes multiple things we wanted to polish on llm o11y. 

- **fix: test json being send** — have fixed a bug. where if we add an
extra key other then attribute / resource we were sending the entire
json. we should still breakdown the json and send them in attribute and
resources key and skip extra feilds.


https://github.com/user-attachments/assets/5e45883a-9d01-49cc-b996-43628df0cb2c




- **fix: create new mapping color** — "Create pricing for X" becomes a
styled sticky footer item labelled "Create a new pricing model" in the
map-to-billing-model combobox.

prev - 

<img width="387" height="225" alt="image"
src="https://github.com/user-attachments/assets/e608c8b9-092e-44eb-b286-5fa81b304eef"
/>

now -
<img width="447" height="406" alt="image"
src="https://github.com/user-attachments/assets/830a213a-285e-4f02-9871-a040e5c1731a"
/>

- **chore: make label of add model cost look same** — Shared label
styles extracted into `shared.module.scss` so every field in the model
cost drawer renders its label identically.

- **chore: migrate tabs in model pricing & attribute mapping to antD
tabs** — Attribute mapping and model pricing now use antD tabs;
`AttributeMappingHeader` is replaced by `AttributeMappingActions`
rendered as tab bar extras.
before - 
<img width="1679" height="493" alt="image"
src="https://github.com/user-attachments/assets/af446ef4-ff25-4f5a-8c81-fb05ba8f2114"
/>

now - 
<img width="1675" height="563" alt="image"
src="https://github.com/user-attachments/assets/4e3a3382-8bcd-4e95-ada5-e04c93765cf1"
/>



- **chore: removed unused padding in mapping tabel** — Drops a stray
padding rule from the mappings table.
- **chore: update colors for drawers inputs** — Aligns input/border
colors across the group, mapper and model cost drawers.
before - 

new - 
<img width="566" height="1001" alt="image"
src="https://github.com/user-attachments/assets/7b5b614b-68bc-4699-9251-1e7c08f527ff"
/>
filled  & disabled - 
<img width="681" height="1032" alt="image"
src="https://github.com/user-attachments/assets/811a5dfe-ee46-4ef3-b94b-e604bc976ca1"
/>

new group - 
<img width="732" height="1008" alt="image"
src="https://github.com/user-attachments/assets/26544ce6-17d1-4502-8b78-b97c4b377be9"
/>

filled group - 
<img width="725" height="1000" alt="image"
src="https://github.com/user-attachments/assets/148125e8-230d-454d-a383-10289726e9cf"
/>

now - 

new - 
<img width="692" height="1019" alt="image"
src="https://github.com/user-attachments/assets/fcd4b817-7b3a-4ed2-8702-3f1e651570f8"
/>
filled & disabled - 
<img width="664" height="989" alt="image"
src="https://github.com/user-attachments/assets/7152a918-73d1-45a0-a5d0-f61ba9dcf560"
/>

video would be better - 


https://github.com/user-attachments/assets/0c72f829-71bb-4c19-a806-c61a98f625c3



- **chore: allow users to move trace id** — Trace ID column stays
non-removable and non-hideable but is now draggable.
prev - 
<img width="1490" height="975" alt="image"
src="https://github.com/user-attachments/assets/b0c26483-bf75-4db3-aca5-3be05ca1341a"
/>

now - 
<img width="1654" height="951" alt="image"
src="https://github.com/user-attachments/assets/c26dfbab-0c2f-4fc6-bad4-5595cb67d6ab"
/>


## Issues closed by this PR


https://github.com/orgs/SigNoz/projects/39/views/20?pane=issue&itemId=252008677&issue=SigNoz%7Cengineering-pod%7C6111

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-22 14:53:22 +00:00
Yunus M
6820fbd091 chore: add scaffolding tool for feature structure in frontend (#12669)
#### Description

Script to help scaffold the co-located feature structure in
frontend/src. Use when creating a new page, feature, view (tab), or
component folder, when a feature needs a shell with tabs, or when moving
existing code out of src/container into src/pages. Generates the full
folder tree
(components/hooks/store/types/utils/constants/__tests__/README) with one
command.

```
pnpm scaffold page ApiMonitoring                                    # leaf page, no shell
pnpm scaffold page Traces --views Explorer,Funnels,Views            # shell + 3 views
pnpm scaffold page Traces/Explorer                                  # one more view under an existing shell
pnpm scaffold component DataTable                                   # global, src/components/DataTable
pnpm scaffold component QueryBar --parent pages/Traces/Explorer     # feature-local component
```


https://app.notion.com/p/signoz/TDD-Feature-First-Frontend-Architecture-3aefcc6bcd198016a2a1fede8ba17715?source=copy_link#3aefcc6bcd1981b5aa91e579b8835bf5

Ex:
<img width="1359" height="759" alt="Screenshot 2026-08-24 at 15 59 32"
src="https://github.com/user-attachments/assets/33de7728-0363-4c2b-89ac-065184476561"
/>




https://github.com/user-attachments/assets/694dcf70-fac6-424e-a00f-e3a0af2f7875
2026-09-22 13:27:57 +00:00
Vikrant Gupta
e2e9173986 fix(user): revoke sessions when a user is deleted (#12943)
#### Description

- `DeleteUser` never told the tokenizer about the deletion.
`SoftDeleteUser` removed the `auth_token` rows with raw SQL, so the
opaque tokenizer kept serving the deleted user's session from cache
until rotation forced a DB read, up to `rotation.interval` later.
- The tokenizer eviction now runs before the soft delete, inside one
transaction; `SoftDeleteUser` joins the caller's transaction instead of
opening its own.
- The hourly last-observed-at flush returned an error for any org with
nothing to flush because bun rejects an empty `VALUES` slice. It now
returns early.
- Adds an integration test asserting a deleted user's held token is
rejected on the next request.

#### Additional Information

Only affects the opaque tokenizer; under the JWT tokenizer
`DeleteTokensByUserID` is a no-op.
2026-09-22 12:14:51 +00:00
Nityananda Gohain
905e935658 fix: use db upsert for model pricing (#12942)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
- Each pricing rule ran a SELECT then an INSERT or UPDATE. Rules are now
written with `INSERT ... ON CONFLICT DO UPDATE`, two statements per
request at most.
- Rules without `isOverride` match on `source_id` and skip rows the user
has overridden. Rules with it match on `id`.
- Dropped the `default:` bun tags. bun turns zero values into SQL
`DEFAULT` on insert, so a rule created disabled was stored as enabled.
- Added an integration suite for sync, override, hand-back and bulk
writes.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Part of https://github.com/SigNoz/engineering-pod/issues/6107
2026-09-22 11:15:20 +00:00
Vikrant Gupta
e8324581b3 fix(tokenizer): accept the previous token pair only within the rotation duration (#12941)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- `Token.Rotate` accepted the previous token pair only when the rotation
was older than `rotation.duration` and rejected it inside the window,
the inverse of the documented intent.
- With the opaque tokenizer, two holders of the same pair (browser tabs,
the axios interceptor and the SSE wrapper) racing at the rotation
boundary meant the loser got 401 on `/sessions/rotate` and the frontend
logged the user out. It also left a stale pair exchangeable for the live
session until the next rotation.
- `RotateToken` now detects that `Rotate` left the stored row untouched
and returns the current pair without rewriting it; the previous check
compared against the input and could never match.
- Adds a unit test for the previous-pair path inside and outside the
window.

#### Additional Information

The JWT tokenizer is unaffected since its rotation is stateless.
2026-09-22 09:58:52 +00:00
Naman Verma
13a57ebb9c chore: remove v1 dashboards code from backend (#12932)
<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/321
2026-09-22 08:46:28 +00:00
Aditya Singh
d39467f5ef chore(logs): delete the old logs explorer (#12930)
#### Description

- `/logs/old-logs-explorer` has been rendering nothing since #8299
gutted its two endpoints. deleted the page along with its containers,
the logs redux slice, the legacy `api/logs` clients and the logql
parser. `LogViewMode` and the restricted field constants move out first
since they have consumers elsewhere.
- deleted what the removal orphans.. the logql test fixture,
`CategoryHeading` and the antd table behind `components/Logs/TableView`.
that folder does not go entirely, `useLogsTableColumns` and
`ColumnTypeRender` still have consumers.
- `ROUTES.LOGS` and `ROUTES.LOGS_EXPLORER` are the same path and both
were registered exact. Switch takes the first so the second entry never
ran and its chunk just duplicated the first. dropped the entry, both
constants stay.

#### Issues closed by this PR

Part of https://github.com/SigNoz/engineering-pod/issues/6103

#### Additional Information

- frontend only. `GET /api/v1/logs` and `/api/v1/logs/aggregate` stay
registered and still return empty payloads, so this can go in without a
backend change.
- overlaps #12919 (old trace explorer) on the `NewExplorerCTA` cleanup,
and overlaps the log details v1 removal on a few files it deletes
outright. whichever merges second needs a rebase.
- cc. @therealpandey
2026-09-22 04:44:25 +00:00
Gaurav Tewari
d457ce6144 feat(llm-observability): move module tabs to RouteTab , add tab icons & other ui changes (#12762)
#### Description

- Replaces the bespoke LLM Observability tab shell
(`container/LLMObservability/LLMObservability.tsx` +
`useLLMObservabilityTabs`) with the shared `RouteTab` component that
every other module page already uses. Tabs are now declared as
`TabRoutes` in `pages/LLMObservability/constants.tsx`.
- Adds icons to the tab labels across LLM Observability, Infrastructure
Monitoring, All Errors, Messaging Queues, Meter Explorer and Logs
Settings, following the existing `.tab-item` pattern.
- Fixes tab-panel padding and aligns the source-filter select height
with the adjacent search input.
- Removed the dashboardInfoWithActions


#### Additional Information

`AllErrors/config.ts` is renamed to `.tsx` since the tab labels now
contain JSX.

Visual Diff - 
1. Look into the wrapper  - 
 a. Before 
<img width="1474" height="997" alt="image"
src="https://github.com/user-attachments/assets/39fe854b-fa79-4e24-9227-4da77825f726"
/>
b. Now 
<img width="1489" height="991" alt="image"
src="https://github.com/user-attachments/assets/4c02e991-3764-4f66-9efc-bef934ca5e2a"
/>



2. Removed the dashboardInfoWithActions 
a. Before 
<img width="1392" height="304" alt="image"
src="https://github.com/user-attachments/assets/bb2efef8-5126-4728-b221-b2cc2b3da18f"
/>
b. now
<img width="1420" height="290" alt="image"
src="https://github.com/user-attachments/assets/50412047-5eff-4035-ac6c-ed79393070ac"
/>


3. After feedback from nitya we have to hide add variable and the
actions button as well.
a. Before
<img width="1368" height="597" alt="image"
src="https://github.com/user-attachments/assets/669de39b-bbc2-4b29-a16e-139457fbb483"
/>

b. Now
<img width="1382" height="728" alt="image"
src="https://github.com/user-attachments/assets/7973dc3a-9914-42ed-a334-887ea73b1ba0"
/>

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-22 04:44:00 +00:00
560 changed files with 38208 additions and 7393 deletions

View File

@@ -26,6 +26,135 @@ process on top of it.
5. **Verify in the browser**: [references/verify.md](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:
| Tag | When |
| --- | --- |
| `authz` | The page gates UI on permission checks through `lib/authz` (`AuthZButton`, `AuthZGuard`, `useAuthZ`). Both the page's file and its `Authz` file carry it. |
| `role-gated` | The page still branches on the legacy role (`user.role`, `hasEditPermission`) and has no authz check. |
| `beta` | `isBeta` on its nav entry. Drop the tag when the product drops the badge. |
| `legacy` | Superseded by another page but still routed. The doc comment names the page to start from instead. |
| `play` | The 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:
```tsx
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.
Verify these by their disabled states, not their text. The page reads the same
either way, so a story that is wrong looks right: read `disabled` off the buttons
the permission gates, and check the denial callout is there or gone.
## Rules
- **Default is the loaded page.** `export const Default: Story = {}` with no args,
@@ -50,7 +179,29 @@ process on top of it.
- **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/`.
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](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:
@@ -74,6 +225,13 @@ process on top of it.
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
@@ -85,6 +243,16 @@ process on top of it.
## 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

View File

@@ -128,20 +128,83 @@ export const servicesMocks = defineStoryMocks({
// src/pages/Services/stories/Services.stories.tsx
type ServicesArgs = PageStoryArgs<typeof servicesMocks>;
const pageStory = storyMocks(servicesMocks, {
route: ROUTES.APPLICATION,
layout: 'app',
});
/**
* Every instrumented service with its p99, error rate and throughput.
*
* Route: `/services`.
*/
const meta = {
title: 'Pages/Services',
title: 'Pages/Services/List',
component: Services,
...storyMocks(servicesMocks, { route: ROUTES.APPLICATION, layout: 'app' }),
...pageStory,
parameters: { ...pageStory.parameters },
} satisfies Meta<ServicesArgs>;
```
`PageStoryArgs` folds in the global controls, so a story's `args` can set
`access`, `dataState` or `banner` next to the page's own knobs and stay typed.
## A step the page keeps in component state
A wizard's step, a questionnaire's page, a picker's next question: the page holds
it in `useState` and nothing in the URL says which one is open. It is still a
control. Declare the steps in the mocks module and drive them from a `play` on
the **meta**, so every story of the page inherits the walk and only sets `args`:
```tsx
// <Page>.stories.mocks.tsx
export const SETUP_STEPS = ['pick-source', 'pick-framework', 'configure'] as const;
export type SetupStep = (typeof SETUP_STEPS)[number];
controls: {
step: choiceControl<SetupStep>('Setup step', { group: SETUP, options: SETUP_STEPS, value: 'pick-source' }),
},
```
```tsx
// <Page>.stories.tsx
const meta = {
play: async ({ mount, args, canvasElement }): Promise<void> => {
await mount();
await advanceToSetupStep(canvasElement, args.step);
},
...storyMocks(pageMocks),
} satisfies Meta<PageArgs>;
export const Configure: Story = { args: { step: 'configure' } };
```
**Destructuring `mount` is what makes it a control.** Storybook re-runs a play
function on an arg change only for a story whose play asks to be remounted
(`usesMount`); otherwise it re-renders the tree the previous walk left behind and
the panel looks broken. With `mount` destructured, the story renders when `play`
calls it, and every arg change replays the walk from a fresh mount.
The walk itself:
- one `answer` function per step, in an array indexed the same as the step list,
so reaching step *n* is `answers.slice(0, STEPS.indexOf(step))`;
- answer each step with the least its Next button accepts, and prefer a "do this
later" over filling a slider;
- run them sequentially (`reduce` over a promise), since each answer is what
renders the step the next one reads;
- bail out when the page did not start where the walk expects, such as a source
deep-linked past the questions. Check for the first step's own text rather than
reading another control's value.
An endpoint that only settles the transition between two steps (the profile a
questionnaire saves before its last page) takes a plain resolver, or the Data
control on `loading` strands the walk halfway.
## Not a control
- Anything the global controls already cover: banner, side nav, data state,
access preset, permissions, check state.
access preset, granted permissions, revoked permissions, check state.
- A knob whose effect nobody can see on the page. Delete it or find the widget it
was supposed to drive.
- A raw payload as an object control. Controls carry intent (`5 dashboards`,
@@ -155,9 +218,12 @@ const meta = {
Default to a control. Write a story when the state is worth a link:
- the fresh workspace, because that is what a new user sees
- the restricted user, when permissions visibly change the page
- a page-defining mode (a tab, a category) that has its own layout
A permission that visibly changes the page is a story too, but it goes in the
page's `Authz` folder, one per permission, turned with the `Revoked` control.
See **Permission stories** in SKILL.md.
Combinations of controls do not need stories, which is what the panel is for.
Each story gets one prose doc comment: what it shows, in the page's own terms.

View File

@@ -12,11 +12,12 @@ cd frontend && pnpm storybook --ci --quiet # :6006, background it
A newly added `.stories.tsx` takes a few seconds to appear in `index.json` on an
already-running server; an empty first poll is not a broken `stories` glob.
Story ids come from the meta title: `Pages/Services` `pages-services`, plus the
story export in kebab-case. Render one story on its own:
Story ids come from the meta title: `Pages/Services/List`
`pages-services-list`, plus the story export in kebab-case. Render one story on
its own:
```
http://localhost:6006/iframe.html?id=pages-services--default&viewMode=story
http://localhost:6006/iframe.html?id=pages-services-list--default&viewMode=story
```
## Flip controls from the URL

6
.github/CODEOWNERS vendored
View File

@@ -280,3 +280,9 @@ go.mod @therealpandey
/frontend/src/components/MessagingQueues/ @SigNoz/events-frontend
/frontend/src/components/MessagingQueueHealthCheck/ @SigNoz/events-frontend
/frontend/src/hooks/messagingQueue/ @SigNoz/events-frontend
## Storybook
/frontend/.storybook/ @H4ad
/frontend/src/storybook/ @H4ad
/.claude/skills/signoz-page-story/ @H4ad
/.claude/skills/storybook-visual-diff/ @H4ad

View File

@@ -47,6 +47,7 @@ jobs:
- dashboard
- ingestionkeys
- inframonitoring
- llmpricingrules
- logspipelines
- passwordauthn
- preference

View File

@@ -3454,29 +3454,6 @@ components:
required:
- customValue
type: object
DashboardtypesDashboard:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
data:
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
id:
type: string
locked:
type: boolean
org_id:
type: string
source:
$ref: '#/components/schemas/DashboardtypesSource'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
type: object
DashboardtypesDashboardPanelRef:
properties:
dashboardId:
@@ -3656,13 +3633,6 @@ components:
timeRangeEnabled:
type: boolean
type: object
DashboardtypesGettablePublicDashboardData:
properties:
dashboard:
$ref: '#/components/schemas/DashboardtypesDashboard'
publicDashboard:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
type: object
DashboardtypesGettablePublicDashboardDataV2:
properties:
dashboard:
@@ -4447,9 +4417,6 @@ components:
- normal
- percent
type: string
DashboardtypesStorableDashboardData:
additionalProperties: {}
type: object
DashboardtypesTableFormatting:
properties:
columnUnits:
@@ -7225,22 +7192,6 @@ components:
- attributes
- totalKeys
type: object
MetricsexplorertypesMetricDashboard:
properties:
dashboardId:
type: string
dashboardName:
type: string
widgetId:
type: string
widgetName:
type: string
required:
- dashboardName
- dashboardId
- widgetId
- widgetName
type: object
MetricsexplorertypesMetricDashboardPanelsResponse:
properties:
dashboards:
@@ -7251,16 +7202,6 @@ components:
required:
- dashboards
type: object
MetricsexplorertypesMetricDashboardsResponse:
properties:
dashboards:
items:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
nullable: true
type: array
required:
- dashboards
type: object
MetricsexplorertypesMetricHighlightsResponse:
properties:
activeTimeSeries:
@@ -12831,9 +12772,8 @@ paths:
put:
deprecated: false
description: Single write endpoint used by both the user and the Zeus sync job.
Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true)
are fully preserved when the request does not provide isOverride; only synced_at
is stamped.
Rules without isOverride are matched by sourceId and override rows (is_override=true)
are skipped. Rules with isOverride are matched by id and inserted when new.
operationId: CreateOrUpdateLLMPricingRules
requestBody:
content:
@@ -13303,112 +13243,6 @@ paths:
summary: Update org preference
tags:
- preferences
/api/v1/public/dashboards/{id}:
get:
deprecated: false
description: This endpoint returns the sanitized dashboard data for public access
operationId: GetPublicDashboardData
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardData'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- anonymous:
- public-dashboard:read
summary: Get public dashboard data
tags:
- dashboard
/api/v1/public/dashboards/{id}/widgets/{idx}/query_range:
get:
deprecated: false
description: This endpoint return query range results for a widget of public
dashboard
operationId: GetPublicDashboardWidgetQueryRange
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: idx
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- anonymous:
- public-dashboard:read
summary: Get query range result
tags:
- dashboard
/api/v1/roles:
get:
deprecated: false
@@ -19660,74 +19494,6 @@ paths:
summary: Get metric attributes
tags:
- metrics
/api/v2/metrics/dashboards:
get:
deprecated: false
description: This endpoint returns associated dashboards for a specified metric
operationId: GetMetricDashboards
parameters:
- description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
in: query
name: metricName
required: true
schema:
description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboardsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get metric dashboards
tags:
- metrics
/api/v2/metrics/highlights:
get:
deprecated: false

View File

@@ -52,7 +52,7 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
dashboard, err := module.GetV2(ctx, orgID, publicDashboard.DashboardID)
if err != nil {
return err
}
@@ -90,15 +90,6 @@ func (module *module) GetPublic(ctx context.Context, orgID valuer.UUID, dashboar
return dashboardtypes.NewPublicDashboardFromStorablePublicDashboard(storablePublicDashboard), nil
}
func (module *module) GetDashboardByPublicID(ctx context.Context, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
return nil, err
}
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
}
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
orgIDs := make([]string, len(orgs))
for idx, org := range orgs {
@@ -116,24 +107,6 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
}, storableDashboard.OrgID, nil
}
func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.UUID, widgetIdx, startTime, endTime uint64) (*querybuildertypesv5.QueryRangeResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "dashboard",
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRange",
})
dashboard, err := module.GetDashboardByPublicID(ctx, id)
if err != nil {
return nil, err
}
query, err := dashboard.GetWidgetQuery(startTime, endTime, widgetIdx, module.settings.Logger())
if err != nil {
return nil, err
}
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
@@ -189,7 +162,7 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
dashboard, err := module.GetV2(ctx, orgID, publicDashboard.DashboardID)
if err != nil {
return err
}
@@ -200,34 +173,13 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
}
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
dashboard, err := module.Get(ctx, orgID, id)
if err != nil {
return err
}
if err := dashboard.ErrIfNotDeletable(); err != nil {
return err
}
if dashboard.Locked {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
}
return module.delete(ctx, orgID, id)
}
func (module *module) DeleteUnsafe(ctx context.Context, orgID, id valuer.UUID) error {
return module.delete(ctx, orgID, id)
}
func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, dashboardID)
dashboard, err := module.GetV2(ctx, orgID, dashboardID)
if err != nil {
return err
}
@@ -260,10 +212,6 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return stats, nil
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, source, data)
}
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, source, postable)
}
@@ -346,30 +294,10 @@ func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valu
return module.pkgDashboardModule.DeleteView(ctx, orgID, id)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
return module.pkgDashboardModule.GetByMetricNames(ctx, orgID, metricNames)
}
func (module *module) GetByMetricNamesV2(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
return module.pkgDashboardModule.GetByMetricNamesV2(ctx, orgID, metricNames)
}
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.List(ctx, orgID)
}
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Update(ctx, orgID, id, updatedBy, data, diff)
}
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) ReconcileSystemDashboards(ctx context.Context, orgID valuer.UUID) error {
return module.pkgDashboardModule.ReconcileSystemDashboards(ctx, orgID)
}
@@ -377,12 +305,3 @@ func (module *module) ReconcileSystemDashboards(ctx context.Context, orgID value
func (module *module) GetSystemDashboard(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetSystemDashboard(ctx, orgID, name)
}
func (module *module) delete(ctx context.Context, orgID, id valuer.UUID) error {
return module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.DeletePublic(ctx, id.String()); err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
return module.store.Delete(ctx, orgID, id)
})
}

View File

@@ -462,7 +462,7 @@ func (m *module) relatedAssetImpact(ctx context.Context, orgID valuer.UUID, metr
droppedSet[label] = struct{}{}
}
if dashboards, err := m.dashboard.GetByMetricNames(ctx, orgID, []string{metricName}); err != nil {
if dashboards, err := m.dashboard.GetByMetricNamesV2(ctx, orgID, []string{metricName}); err != nil {
m.logger.WarnContext(ctx, "failed to fetch related dashboards for reduction preview", slog.String("metric_name", metricName), errors.Attr(err))
} else {
for _, item := range dashboards[metricName] {

View File

@@ -80,15 +80,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
aiObservability := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
Active: aiObservability,
Usage: 0,
UsageLimit: -1,
Route: "",
})
metricsReduction := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableMetricsReduction.String()),

View File

@@ -0,0 +1,127 @@
---
name: scaffold-feature
description: Scaffold the co-located feature structure in frontend/src. Use when creating a new page, feature, view (tab), or component folder, when a feature needs a shell with tabs, or when moving existing code out of src/container into src/pages. Generates the full folder tree (components/hooks/store/types/utils/constants/__tests__/README) and registers the page's routes with one command.
---
# Scaffold a feature
The frontend is moving to a co-located layout (Bulletproof React / FSD): everything a
feature owns lives in the feature's folder. Read `references/layout.md` for the full
target structure and the rules about what may live where.
**Never hand-create these folders.** Run the generator so every feature comes out
identical, then fill it in.
## Command
```bash
pnpm scaffold page <Name> [options] # a page/feature under src/pages
pnpm scaffold component <Name> [options] # a component folder
```
| Option | Applies to | Effect |
| --- | --- | --- |
| `--views A,B,C` | `page` | Makes the page a shell with tab switching and generates one view folder per name. |
| `--parent <path>` | `component` | Parent, relative to `src` (default `components`). A feature path like `pages/Traces/Explorer` nests the component under that feature's `components/`. |
| `--full` | `component` | Also adds `components/`, `hooks/`, `store/`, `types.ts`, `utils.ts`, `constants.ts`, `README.md` for a component that owns children. |
| `--no-tests` | both | Skips `__tests__/`. |
| `--dry-run` | both | Prints what would be written, writes nothing. |
| `--force` | both | Overwrites files that already exist (off by default; existing entries are reported as skipped). |
Folder names keep the casing you type, with the first letter forced up, so
`LLMObservability` stays `LLMObservability` rather than being re-cased. Separated names
collapse to PascalCase: `api-monitoring` and `api monitoring` both give
`pages/ApiMonitoring`. Test ids, headings, tab paths and constants are all derived from
that folder name — `TracesFunnels` gives `traces-funnels-page`, `Traces Funnels` and
`TRACES_FUNNELS_TABS`.
## What you get
```
pages/ApiMonitoring/
index.tsx # the page component
ApiMonitoring.module.scss
components/ hooks/ store/ # empty, ready for the first file
types.ts utils.ts constants.ts
__tests__/ApiMonitoring.test.tsx
README.md
```
With `--views`, the root becomes a `RouteTab` shell and each view gets the tree above. The
shell mirrors the Logs and Traces root pages: `constants.tsx` exports one `TabRoutes` per
view (icon from `@signozhq/icons`, label, `ROUTES` key, view component), `index.tsx` composes
them into the tab bar, the SCSS module carries the tab-bar overrides, and the test asserts one
tab per view plus the active view. Tab icons come from a small name map in `scaffold.mjs`
(`Explorer`, `Funnels`, `Pipelines`, `Views`, `SavedViews`); other names get a neutral icon
to replace.
## Examples
```bash
pnpm scaffold page ApiMonitoring # leaf page, no shell
pnpm scaffold page Traces --views Explorer,Funnels,Views # shell + 3 views
pnpm scaffold page Traces/Explorer # one more view under an existing shell
pnpm scaffold component DataTable # global, src/components/DataTable
pnpm scaffold component QueryBar --parent pages/Traces/Explorer # feature-local component
```
## Route registration
`page` also registers the routes, so the page is reachable as soon as it is generated:
| File | What is added |
| --- | --- |
| `src/constants/routes.ts` | One key per path: `API_MONITORING: '/api-monitoring'` for a leaf page; `TRACES_BASE` plus `TRACES_EXPLORER`, `TRACES_FUNNELS`, … for a shell. |
| `src/utils/permission/index.ts` | A `routePermission` entry per new key, open to `ADMIN`, `EDITOR` and `VIEWER`. Tighten it if the page is admin-only. |
| `src/AppRoutes/pageComponents.ts` | A `Loadable` export named `<Page>Page` pointing at `pages/<Page>`. |
| `src/AppRoutes/routes.ts` | The import plus one private, exact route per path. For a shell the base path and every tab path render the shell; the shell redirects the base path to its first tab and `RouteTab` picks the tab otherwise. |
| `src/container/TopNav/DateTimeSelectionV2/constants.ts` | Every new path in `routesToSkip`, so the global time-range picker stays hidden until the page opts in. |
Existing keys, exports and entries are left alone, so re-running is safe. An existing key or
export that points somewhere else is a naming collision and the run stops before writing
anything. `--dry-run` lists
the edits without making them. `page Traces/Explorer` registers `TRACES_EXPLORER` pointing
at the `Traces` shell; wiring the new tab into the shell's `constants.tsx` and `index.tsx`
is still by hand. The generator never adds a SideNav item; do that in
`src/container/SideNav/menuItems.tsx` when the page needs one.
## After generating
1. **Review the route registration** (pages only) and add the SideNav entry if the page
needs one. For a view added under an existing shell, add its `TabRoutes` export to the
shell's `constants.tsx` and include it in the `routes` array in the shell's `index.tsx`.
2. **Delete the placeholders you don't need** — empty `types.ts` / `utils.ts` /
`constants.ts`, and any of `components/`, `hooks/`, `store/` the feature won't use.
Those three folders are created empty; git only picks them up once they hold a file.
3. **Fill the README** — the generated file has the prompts; a feature folder without a
filled-in README is not done.
4. **Follow the repo rules while filling it in**: `@signozhq/ui` + `@signozhq/icons` only,
CSS Modules (`docs/css-modules-guide.md`), React Query for server state (prefer
`api/generated` hooks), nuqs for URL state, Zustand for client state, `data-testid` on
every interactive element.
5. **Verify** before reporting done:
```bash
pnpm tsgo --noEmit
pnpm oxlint src/pages/<Feature>
pnpm jest src/pages/<Feature>
```
`pnpm tsgo --noEmit` is the authority. A running dev server can show errors such as
`Property 'X_BASE' does not exist` or `has no exported member 'XPage'` right after
generation. Its type-checker notices new files but, on some machines, not in-place edits
to existing ones, and the generator edits the shared files in place. If tsgo is clean,
restart `pnpm dev`.
## Editing the templates
Templates live in `templates/` — `feature/`, `shell/`, `component/` and
`component-extras/` (the `--full` additions). Every template file ends in `.tmpl`, which
keeps TypeScript, lint and your editor from reading them as source; the generator strips
that suffix on the way out, so `index.tsx.tmpl` becomes `index.tsx`. Tokens are
substituted in both file names and contents: `__Pascal__`, `__kebab__`, `__camel__`,
`__CONST__`, `__Title__`. The shell templates additionally take tokens the generator builds
from `--views`: `__ICON_IMPORTS__`, `__VIEW_IMPORTS__`, `__TAB_EXPORTS__`, `__TAB_NAMES__`,
`__BASE_ROUTE__`, `__FIRST_TAB__`, `__FIRST_VIEW_TESTID__` and `__TAB_ASSERTIONS__`. Tab icons come from
`TAB_ICONS` and the empty folders from `FEATURE_DIRS`, both in `scaffold.mjs`. Name and
route derivations live in `lib.mjs`; run `node --test .claude/skills/scaffold-feature/scaffold.test.mjs`
after changing them. Change these, not the generated
output, when the team's conventions move.

View File

@@ -0,0 +1,77 @@
const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
// Folder names keep the casing the author typed — only the first letter is forced
// up — so acronyms like `LLMObservability` survive. Separated names
// (`api-monitoring`, `api monitoring`) collapse to PascalCase.
export function toDirName(value) {
const name = value.trim().replace(/[^a-zA-Z0-9\-_ ]/g, '');
if (!name) {
throw new Error(`"${value}" has no usable name characters`);
}
return /[-_\s]/.test(name)
? name
.split(/[-_\s]+/)
.filter(Boolean)
.map(capitalize)
.join('')
: capitalize(name);
}
const splitHumps = (name, separator) =>
name
.replace(/([a-z0-9])([A-Z])/g, `$1${separator}$2`)
.replace(/([A-Z]+)([A-Z][a-z])/g, `$1${separator}$2`);
export const toKebab = (value) => splitHumps(toDirName(value), '-').toLowerCase();
export const toTitle = (value) => splitHumps(toDirName(value), ' ');
export const toConst = (value) => toKebab(value).replace(/-/g, '_').toUpperCase();
export const toCamel = (value) => {
const dir = toDirName(value);
return dir.charAt(0).toLowerCase() + dir.slice(1);
};
export function tokensFor(name) {
return {
__Pascal__: toDirName(name),
__kebab__: toKebab(name),
__camel__: toCamel(name),
__CONST__: toConst(name),
__Title__: toTitle(name),
};
}
export function substitute(text, tokens) {
return Object.entries(tokens).reduce(
(acc, [token, value]) => acc.split(token).join(value),
text,
);
}
export const routeKey = (segments, view) =>
[...segments, ...(view ? [view] : [])].map(toConst).join('_');
export const routePath = (segments, view) =>
`/${[...segments, ...(view ? [view] : [])].map(toKebab).join('/')}`;
// Every path under a shell renders the shell itself (RouteTab picks the tab, the base path
// redirects to the first tab), so the page component is always the first segment.
export function routeSpec(segments, views) {
const shell = segments[0];
const component = {
name: `${shell}Page`,
importPath: `pages/${shell}`,
chunk: `${toTitle(shell)} Page`,
};
if (views.length) {
const tabs = views.map((view) => ({
key: routeKey(segments, view),
path: routePath(segments, view),
}));
const keys = [
{ key: `${routeKey(segments)}_BASE`, path: routePath(segments) },
...tabs,
];
return { component, keys, routed: keys.map(({ key }) => key) };
}
const key = routeKey(segments);
return { component, keys: [{ key, path: routePath(segments) }], routed: [key] };
}

View File

@@ -0,0 +1,102 @@
# Frontend layout
Target structure for `frontend/src`. Inspired by Bulletproof React and Feature-Sliced
Design: a feature owns its components, hooks, state, types and tests, and nothing outside
the feature folder reaches into it.
```
src/
app/ # bootstrap: routing, global styles/theme
pages/
Traces/ # has a shell
index.tsx # shell — tab switching only
constants.tsx # tab definitions
Explorer/ # a view
index.tsx # view entry — composition, no business logic
components/
QueryBar/ # same shape as a global component, nests further as needed
QueryBar.tsx
QueryBar.module.scss
components/
hooks/
__tests__/
hooks/ # feature hooks + React Query wrappers over api/generated
store/ # Zustand stores for feature-local client state
types.ts
utils.ts
constants.ts
__tests__/
README.md
Funnels/
Views/
ApiMonitoring/ # no shell — same shape, one level up
index.tsx
components/
hooks/
store/
types.ts
utils.ts
constants.ts
__tests__/
README.md
components/ # cross-feature components, same internal shape as above
DataTable/
DataTable.tsx
DataTable.module.scss
components/
hooks/
store/
types.ts
utils.ts
constants.ts
__tests__/
README.md
lib/
utils/
types/
constants/
store/ # app-wide client state only
i18n/
api/
generated/ # Orval output — never edited by hand
client/ # axios instances, interceptors, error handlers
index.tsx
```
## Rules
- **Folder names are PascalCase**, spelled the way the feature is spelled in the product
(`ApiMonitoring`, `LLMObservability`). This holds for shells, views and components alike.
- **A page folder is the unit of ownership.** Anything used by exactly one feature lives
inside it, however deeply nested. Promote to `src/components` / `src/utils` / `src/hooks`
only when a second feature needs it.
- **`index.tsx` is the entry**, and it composes. Business logic goes to `hooks/`, data
shaping to `utils.ts`, state to `store/`.
- **Nested components repeat the same shape.** A component folder may hold its own
`components/`, `hooks/`, `store/`, `types.ts`, `utils.ts`, `constants.ts`, `__tests__/`.
Nest as deep as ownership actually goes; don't flatten a component that owns children.
- **Shell vs no shell.** A page with tabs gets a shell `index.tsx` whose only job is tab
switching, plus one folder per view. A page without tabs is just the feature folder.
- **Tests.** Feature-root tests in `__tests__/`; a component's tests next to the component
(its own `__tests__/`). Never reach across features in a test.
- **No barrel files.** A page's `index.tsx` is the route entry (a component), not a
re-export hub. Import components by their own path.
- **File size.** Split past ~300 LOC: extract components, and behaviour into
`use<Component>Callbacks`-style hooks. More than ~3 type declarations in a file means a
`types.ts`, and more than ~3 in `types.ts` means a `types/` folder.
- **Styling.** CSS Modules (`<Name>.module.scss`) next to the component — see
`docs/css-modules-guide.md`. Semantic tokens only.
- **State.** Server → React Query (prefer `api/generated` hooks); URL → nuqs; client →
Zustand, one store per file, always with a selector. No Redux or Context for new code.
## Migrating existing code
Most feature code still lives in `src/container` and `src/modules`, with a thin wrapper in
`src/pages`. When touching one of those features:
1. Scaffold the target with `pnpm scaffold page <Name>` (see `../SKILL.md`).
2. Move files in, one concern per commit — components, then hooks, then state.
3. Update importers; keep `src/container/<Feature>` deleted, not re-exported. A shim
directory is how the old layout survives.
4. Do the dead-code pass first: unused props, exports, imports and debug logs go before the
move, in their own commit.

View File

@@ -0,0 +1,606 @@
#!/usr/bin/env node
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
routeKey,
routeSpec,
substitute,
toCamel,
toDirName,
toKebab,
toTitle,
tokensFor,
} from './lib.mjs';
const SKILL_DIR = dirname(fileURLToPath(import.meta.url));
const TEMPLATES = join(SKILL_DIR, 'templates');
const FRONTEND = resolve(SKILL_DIR, '..', '..', '..');
const SRC = join(FRONTEND, 'src');
const ROUTE_FILES = {
routes: join(SRC, 'constants', 'routes.ts'),
permission: join(SRC, 'utils', 'permission', 'index.ts'),
pageComponents: join(SRC, 'AppRoutes', 'pageComponents.ts'),
appRoutes: join(SRC, 'AppRoutes', 'routes.ts'),
topNav: join(SRC, 'container', 'TopNav', 'DateTimeSelectionV2', 'constants.ts'),
};
const ROUTE_ROLES = "['ADMIN', 'EDITOR', 'VIEWER']";
// Port is fixed in vite.config.ts; the base path comes from VITE_BASE_PATH like vite does.
const DEV_SERVER_ORIGIN = 'http://localhost:3301';
function devServerUrl(path) {
const base = process.env.VITE_BASE_PATH ?? envFileValue('VITE_BASE_PATH') ?? '/';
return `${DEV_SERVER_ORIGIN}${base.replace(/\/+$/, '')}${path}`;
}
function envFileValue(name) {
const envFile = join(FRONTEND, '.env');
if (!existsSync(envFile)) {
return undefined;
}
const match = readFileSync(envFile, 'utf8').match(
new RegExp(`^\\s*${name}\\s*=\\s*["']?([^"'\\n#]*)`, 'm'),
);
return match?.[1].trim() || undefined;
}
// Created empty, so the folder exists before it has a file to justify it.
const FEATURE_DIRS = ['components', 'hooks', 'store'];
const USAGE = `usage:
pnpm scaffold page <Name> [--views A,B,C] [--no-tests] [--dry-run] [--force]
pnpm scaffold component <Name> [--parent <path>] [--full] [--no-tests] [--dry-run] [--force]
examples:
pnpm scaffold page ApiMonitoring
pnpm scaffold page Traces --views Explorer,Funnels,Views
pnpm scaffold page Traces/Explorer
pnpm scaffold component DataTable
pnpm scaffold component QueryBar --parent pages/Traces/Explorer`;
function fail(message) {
process.stderr.write(`error: ${message}\n\n${USAGE}\n`);
process.exit(1);
}
function expandEquals(argv) {
return argv.flatMap((arg) =>
arg.startsWith('--') && arg.includes('=')
? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)]
: [arg],
);
}
function parseArgs(argv) {
const flags = {
views: [],
parent: 'components',
full: false,
tests: true,
dryRun: false,
force: false,
};
const positional = [];
const provided = new Set();
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
provided.add(arg);
if (arg === '--views' || arg === '--parent') {
const value = argv[i + 1];
if (!value || value.startsWith('--')) {
fail(`${arg} needs a value`);
}
if (arg === '--views') {
flags.views = value
.split(',')
.map((view) => view.trim())
.filter(Boolean);
if (!flags.views.length) {
fail('--views needs at least one name');
}
} else {
flags.parent = value;
}
i += 1;
} else if (arg === '--full') {
flags.full = true;
} else if (arg === '--no-tests') {
flags.tests = false;
} else if (arg === '--dry-run') {
flags.dryRun = true;
} else if (arg === '--force') {
flags.force = true;
} else if (arg === '-h' || arg === '--help') {
process.stdout.write(`${USAGE}\n`);
process.exit(0);
} else if (arg.startsWith('-')) {
fail(`unknown option: ${arg}`);
} else {
positional.push(arg);
}
}
return { positional, flags, provided };
}
const created = [];
const skipped = [];
let targetExisted = false;
let pagePath = '';
function writeFile(target, contents, flags) {
const rel = relative(FRONTEND, target);
if (existsSync(target) && !flags.force) {
skipped.push(rel);
return;
}
if (!flags.dryRun) {
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, contents);
}
created.push(rel);
}
function createDirs(targetDir, dirs, flags) {
for (const dir of dirs) {
const target = join(targetDir, dir);
const rel = `${relative(FRONTEND, target)}/`;
if (existsSync(target)) {
skipped.push(rel);
continue;
}
if (!flags.dryRun) {
mkdirSync(target, { recursive: true });
}
created.push(rel);
}
}
// Template files carry a `.tmpl` suffix so no TypeScript, lint or editor tooling
// treats them as source; the suffix is dropped on the way out.
function renderTree(templateDir, targetDir, tokens, flags) {
for (const entry of readdirSync(templateDir).sort()) {
const from = join(templateDir, entry);
const name = substitute(entry.replace(/\.tmpl$/, ''), tokens);
if (statSync(from).isDirectory()) {
if (!flags.tests && name === '__tests__') {
continue;
}
renderTree(from, join(targetDir, name), tokens, flags);
} else {
writeFile(
join(targetDir, name),
substitute(readFileSync(from, 'utf8'), tokens),
flags,
);
}
}
}
// Icons for tab names the product already uses; anything else gets a neutral one.
const TAB_ICONS = {
Explorer: 'Compass',
Funnels: 'Cone',
Pipelines: 'Workflow',
SavedViews: 'TowerControl',
Views: 'TowerControl',
};
const DEFAULT_TAB_ICON = 'LayoutPanelTop';
const tabIcon = (view) => TAB_ICONS[toDirName(view)] ?? DEFAULT_TAB_ICON;
const tabName = (view) => `${toCamel(view)}Tab`;
function shellTokens(segments, views) {
const icons = [...new Set(views.map(tabIcon))].sort((a, b) => a.localeCompare(b));
const viewImports = views
.map((view) => `import ${toDirName(view)} from './${toDirName(view)}';`)
.join('\n');
const tabExports = views
.map((view) => {
const route = `ROUTES.${routeKey(segments, view)}`;
return [
`export const ${tabName(view)}: TabRoutes = {`,
`\tComponent: ${toDirName(view)},`,
'\tname: (',
'\t\t<div className={styles.tabItem}>',
`\t\t\t<${tabIcon(view)} size={16} /> ${toTitle(view)}`,
'\t\t</div>',
'\t),',
`\troute: ${route},`,
`\tkey: ${route},`,
'};',
].join('\n');
})
.join('\n\n');
const tabAssertions = views
.map(
(view) =>
`\t\texpect(screen.getByRole('tab', { name: '${toTitle(view)}' })).toBeInTheDocument();\n`,
)
.join('');
return {
__ICON_IMPORTS__: `import { ${icons.join(', ')} } from '@signozhq/icons';`,
__VIEW_IMPORTS__: viewImports,
__TAB_EXPORTS__: `${tabExports}\n`,
__TAB_NAMES__: views.map(tabName).join(', '),
__BASE_ROUTE__: `ROUTES.${routeKey(segments)}_BASE`,
__FIRST_TAB__: tabName(views[0]),
__FIRST_VIEW_TESTID__: `${toKebab(views[0])}-page`,
__TAB_ASSERTIONS__: tabAssertions,
};
}
const edited = [];
function insertBefore(source, anchor, text, rel, from = 0) {
const index = source.indexOf(anchor, from);
if (index === -1) {
fail(`could not find \`${anchor.trim()}\` in ${rel}`);
}
return source.slice(0, index) + text + source.slice(index);
}
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// An existing key or export is only reused when it already means what the generator
// would have written; anything else is a naming collision and stops the run before
// any shared file is touched.
function assertSame(rel, what, existing, expected) {
if (existing !== expected) {
fail(
`${what} already exists in ${rel} as ${existing}, expected ${expected}` +
'pick another name',
);
}
}
function planRoutes({ component, keys, routed }) {
return [
{
file: ROUTE_FILES.routes,
transform: (source, rel) => {
const added = keys.filter(({ key, path }) => {
const match = source.match(new RegExp(`\\n\\t${key}: '([^']*)',`));
if (match) {
assertSame(rel, `ROUTES.${key}`, `'${match[1]}'`, `'${path}'`);
}
return !match;
});
const text = added.map(({ key, path }) => `\n\t${key}: '${path}',`).join('');
return {
source: insertBefore(source, '\n} as const;', text, rel),
added: added.map(({ key }) => key),
};
},
},
{
file: ROUTE_FILES.permission,
transform: (source, rel) => {
const start = source.indexOf('export const routePermission');
if (start === -1) {
fail(`could not find \`routePermission\` in ${rel}`);
}
const added = keys
.map(({ key }) => key)
.filter((key) => !source.includes(`\n\t${key}: `));
const text = added.map((key) => `\n\t${key}: ${ROUTE_ROLES},`).join('');
return { source: insertBefore(source, '\n};', text, rel, start), added };
},
},
{
file: ROUTE_FILES.pageComponents,
transform: (source, rel) => {
const existing = source.match(
new RegExp(`export const ${component.name} = Loadable\\([\\s\\S]*?'([^']+)'`),
);
if (existing) {
assertSame(rel, component.name, `'${existing[1]}'`, `'${component.importPath}'`);
return { source, added: [] };
}
const text =
`\nexport const ${component.name} = Loadable(\n` +
`\t() => import(/* webpackChunkName: "${component.chunk}" */ '${component.importPath}'),\n);\n`;
return {
source: source.replace(/\n*$/, '\n') + text,
added: [component.name],
};
},
},
{
file: ROUTE_FILES.appRoutes,
transform: (source, rel) => {
const added = [];
let next = source;
const importEnd = next.indexOf("} from './pageComponents';");
const importStart = next.lastIndexOf('import {', importEnd);
if (importEnd === -1 || importStart === -1) {
fail(`could not find the pageComponents import in ${rel}`);
}
const names = next
.slice(importStart + 'import {'.length, importEnd)
.split(',')
.map((name) => name.trim())
.filter(Boolean);
if (!names.includes(component.name)) {
const lower = component.name.toLowerCase();
const at = names.findIndex((name) => name.toLowerCase() > lower);
names.splice(at === -1 ? names.length : at, 0, component.name);
next =
next.slice(0, importStart) +
`import {\n\t${names.join(',\n\t')},\n` +
next.slice(importEnd);
added.push(`import ${component.name}`);
}
const arrayStart = next.indexOf('const routes: AppRoutes[] = [');
if (arrayStart === -1) {
fail(`could not find \`const routes: AppRoutes[]\` in ${rel}`);
}
const missing = routed.filter((key) => {
const match = next.match(
new RegExp(`component: (\\w+),\\n\\t\\tkey: '${escapeRegExp(key)}',`),
);
if (match) {
assertSame(rel, `route ${key}`, match[1], component.name);
}
return !match;
});
const entries = missing
.map((key) =>
[
'\n\t{',
`\t\tpath: ROUTES.${key},`,
'\t\texact: true,',
`\t\tcomponent: ${component.name},`,
`\t\tkey: '${key}',`,
'\t\tisPrivate: true,',
'\t},',
].join('\n'),
)
.join('');
next = insertBefore(next, '\n];', entries, rel, arrayStart);
added.push(...missing);
return { source: next, added };
},
},
{
file: ROUTE_FILES.topNav,
transform: (source, rel) => {
const start = source.indexOf('export const routesToSkip = [');
if (start === -1) {
fail(`could not find \`routesToSkip\` in ${rel}`);
}
const end = source.indexOf('\n];', start);
const block = source.slice(start, end);
const added = routed.filter((key) => !block.includes(`ROUTES.${key},`));
const text = added.map((key) => `\n\tROUTES.${key},`).join('');
return { source: insertBefore(source, '\n];', text, rel, start), added };
},
},
];
}
// Every shared file is read and validated before any is written, so a failed anchor or
// a naming collision leaves the tree untouched.
function planRouteEdits(spec) {
return planRoutes(spec).map(({ file, transform }) => {
const rel = relative(FRONTEND, file);
if (!existsSync(file)) {
fail(`shared file not found: ${rel}`);
}
const { source, added } = transform(readFileSync(file, 'utf8'), rel);
return { file, rel, source, added };
});
}
function commitRouteEdits(pending, flags) {
for (const { file, rel, source, added } of pending) {
if (!added.length) {
continue;
}
if (!flags.dryRun) {
writeFileSync(file, source);
}
edited.push({ rel, added });
}
}
function scaffoldFeature(targetDir, name, flags) {
renderTree(join(TEMPLATES, 'feature'), targetDir, tokensFor(name), flags);
createDirs(targetDir, FEATURE_DIRS, flags);
}
function scaffoldPage(name, flags) {
const segments = name.split('/').filter(Boolean).map(toDirName);
if (!segments.length) {
fail('page needs a name');
}
const viewDirs = flags.views.map(toDirName);
const duplicate = viewDirs.find((dir, index) => viewDirs.indexOf(dir) !== index);
if (duplicate) {
fail(`duplicate view: ${duplicate}`);
}
const targetDir = join(SRC, 'pages', ...segments);
const leaf = segments[segments.length - 1];
targetExisted = existsSync(targetDir);
// Shared files land before the page folder so a watching type-checker never sees a
// page that references ROUTES keys that do not exist yet.
const spec = routeSpec(segments, flags.views);
commitRouteEdits(planRouteEdits(spec), flags);
pagePath = spec.keys[0].path;
if (flags.views.length) {
renderTree(
join(TEMPLATES, 'shell'),
targetDir,
{ ...tokensFor(leaf), ...shellTokens(segments, flags.views) },
flags,
);
for (const view of flags.views) {
scaffoldFeature(join(targetDir, toDirName(view)), view, flags);
}
} else {
scaffoldFeature(targetDir, leaf, flags);
}
return targetDir;
}
function resolveParent(parent) {
const segments = parent
.replace(/^src\//, '')
.replace(/\/components\/?$/, '')
.split('/')
.filter(Boolean);
if (segments[0] === 'pages') {
return ['pages', ...segments.slice(1).map(toDirName)];
}
return segments;
}
function scaffoldComponent(name, flags) {
const tokens = tokensFor(name);
const parent = resolveParent(flags.parent);
const isGlobal = parent.length === 1 && parent[0] === 'components';
const componentsDir = isGlobal
? join(SRC, 'components')
: join(SRC, ...parent, 'components');
if (relative(SRC, componentsDir).startsWith('..')) {
fail(`--parent must stay inside src: ${flags.parent}`);
}
if (parent[0] === 'pages' && parent.length < 2) {
fail('a component under pages/ needs a feature: --parent pages/<Feature>');
}
if (!isGlobal && !existsSync(join(SRC, ...parent))) {
fail(`parent does not exist: src/${parent.join('/')}`);
}
const targetDir = join(componentsDir, tokens.__Pascal__);
targetExisted = existsSync(targetDir);
renderTree(join(TEMPLATES, 'component'), targetDir, tokens, flags);
if (flags.full) {
renderTree(join(TEMPLATES, 'component-extras'), targetDir, tokens, flags);
createDirs(targetDir, FEATURE_DIRS, flags);
}
return targetDir;
}
function report(kind, targetDir, flags) {
const rel = relative(FRONTEND, targetDir);
const verb = flags.dryRun ? 'would create' : 'created';
const segments = rel.split('/').slice(2);
const isNestedView = kind === 'page' && segments.length > 1 && !flags.views.length;
const leafName = segments[segments.length - 1];
if (targetExisted) {
process.stdout.write(
`\nwarning: ${rel} already existed — only missing entries were added\n`,
);
}
process.stdout.write(`\n${verb} ${created.length} entr(ies) in ${rel}\n`);
for (const entry of created) {
process.stdout.write(` + ${entry}\n`);
}
if (skipped.length) {
process.stdout.write(
`\nskipped ${skipped.length} existing entr(ies) — pass --force to overwrite files\n`,
);
for (const entry of skipped) {
process.stdout.write(` = ${entry}\n`);
}
}
if (edited.length) {
const editVerb = flags.dryRun ? 'would edit' : 'edited';
process.stdout.write(`\n${editVerb} ${edited.length} shared file(s)\n`);
for (const { rel, added } of edited) {
const additions = added.map((entry) => `+${entry}`).join(', ');
process.stdout.write(` ~ ${rel}: ${additions}\n`);
}
}
const steps =
kind === 'page'
? [
'review the route registration (constants/routes.ts, utils/permission, AppRoutes/pageComponents.ts, AppRoutes/routes.ts, TopNav routesToSkip) and add a SideNav entry in container/SideNav/menuItems.tsx if the page needs one',
...(isNestedView
? [
`add a tab export for ${leafName} in the shell's constants.tsx and include it in the routes array in the shell's index.tsx`,
]
: []),
'delete the placeholders you do not need (empty types/utils/constants, unused folders)',
'fill in README.md',
`verify: pnpm tsgo --noEmit && pnpm oxlint ${rel} && pnpm jest ${rel}`,
]
: [
'delete the placeholders you do not need (empty types/utils/constants, unused folders)',
`verify: pnpm tsgo --noEmit && pnpm oxlint ${rel} && pnpm jest ${rel}`,
];
if (pagePath) {
process.stdout.write(`\nopen: ${devServerUrl(pagePath)}\n`);
}
process.stdout.write('\nnext:\n');
steps.forEach((step, index) => {
process.stdout.write(` ${index + 1}. ${step}\n`);
});
process.stdout.write(
'\nnote: git does not track empty folders — components/, hooks/ and store/ only\n' +
'show up in a commit once they hold a file.\n',
);
}
const { positional, flags, provided } = parseArgs(expandEquals(process.argv.slice(2)));
const [kind, name] = positional;
if (!kind || !name) {
fail('a command and a name are required');
}
if (positional.length > 2) {
fail(`unexpected argument: ${positional[2]}`);
}
function rejectFlags(unsupported) {
for (const flag of unsupported) {
if (provided.has(flag)) {
fail(`${flag} does not apply to \`${kind}\``);
}
}
}
let targetDir;
try {
if (kind === 'page') {
rejectFlags(['--parent', '--full']);
targetDir = scaffoldPage(name, flags);
} else if (kind === 'component') {
rejectFlags(['--views']);
targetDir = scaffoldComponent(name, flags);
} else {
fail(`unknown command: ${kind}`);
}
} catch (error) {
fail(error.message);
}
report(kind, targetDir, flags);

View File

@@ -0,0 +1,95 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
routeKey,
routePath,
routeSpec,
substitute,
toCamel,
toConst,
toDirName,
toKebab,
toTitle,
tokensFor,
} from './lib.mjs';
describe('names', () => {
it('keeps typed casing and forces the first letter up', () => {
assert.equal(toDirName('LLMObservability'), 'LLMObservability');
assert.equal(toDirName('apiMonitoring'), 'ApiMonitoring');
});
it('collapses separated names to PascalCase', () => {
assert.equal(toDirName('api-monitoring'), 'ApiMonitoring');
assert.equal(toDirName('api monitoring'), 'ApiMonitoring');
assert.equal(toDirName('saved_views'), 'SavedViews');
});
it('derives kebab, title, const and camel forms, splitting acronyms', () => {
assert.deepEqual(tokensFor('LLMObservability'), {
__Pascal__: 'LLMObservability',
__kebab__: 'llm-observability',
__camel__: 'lLMObservability',
__CONST__: 'LLM_OBSERVABILITY',
__Title__: 'LLM Observability',
});
assert.equal(toKebab('SavedViews'), 'saved-views');
assert.equal(toTitle('SavedViews'), 'Saved Views');
assert.equal(toConst('SavedViews'), 'SAVED_VIEWS');
assert.equal(toCamel('SavedViews'), 'savedViews');
});
it('rejects names with no usable characters', () => {
assert.throws(() => toDirName('***'), /no usable name characters/);
});
});
describe('substitute', () => {
it('replaces every occurrence of every token, in file names and contents', () => {
const tokens = tokensFor('ApiMonitoring');
assert.equal(substitute('__Pascal__.module.scss', tokens), 'ApiMonitoring.module.scss');
assert.equal(
substitute('__kebab__-page / __kebab__-shell / __Title__', tokens),
'api-monitoring-page / api-monitoring-shell / Api Monitoring',
);
});
});
describe('routes', () => {
it('builds keys and paths from every segment plus the view', () => {
assert.equal(routeKey(['Traces'], 'SavedViews'), 'TRACES_SAVED_VIEWS');
assert.equal(routePath(['Traces'], 'SavedViews'), '/traces/saved-views');
assert.equal(routeKey(['Traces', 'Explorer']), 'TRACES_EXPLORER');
assert.equal(routePath(['Traces', 'Explorer']), '/traces/explorer');
});
it('routes a leaf page under a single key', () => {
assert.deepEqual(routeSpec(['ApiMonitoring'], []), {
component: {
name: 'ApiMonitoringPage',
importPath: 'pages/ApiMonitoring',
chunk: 'Api Monitoring Page',
},
keys: [{ key: 'API_MONITORING', path: '/api-monitoring' }],
routed: ['API_MONITORING'],
});
});
it('routes a shell under a base key plus one key per view, all to the shell', () => {
const spec = routeSpec(['Traces'], ['Explorer', 'Funnels']);
assert.equal(spec.component.name, 'TracesPage');
assert.deepEqual(spec.keys, [
{ key: 'TRACES_BASE', path: '/traces' },
{ key: 'TRACES_EXPLORER', path: '/traces/explorer' },
{ key: 'TRACES_FUNNELS', path: '/traces/funnels' },
]);
assert.deepEqual(spec.routed, ['TRACES_BASE', 'TRACES_EXPLORER', 'TRACES_FUNNELS']);
});
it('points a view added under an existing shell at the shell component', () => {
const spec = routeSpec(['Traces', 'Explorer'], []);
assert.equal(spec.component.importPath, 'pages/Traces');
assert.deepEqual(spec.keys, [{ key: 'TRACES_EXPLORER', path: '/traces/explorer' }]);
});
});

View File

@@ -0,0 +1,21 @@
# __Pascal__
<!-- What this component renders, and the features that use it. -->
## API
<!-- Props, and the behaviour each one controls. -->
## Structure
| Path | Purpose |
| --- | --- |
| `__Pascal__.tsx` | The component. |
| `__Pascal__.module.scss` | Styles. |
| `components/` | Child components this one owns. |
| `hooks/` | Behaviour extracted out of the component. |
| `store/` | Zustand stores this component owns. |
| `types.ts` | Types shared inside this folder. |
| `utils.ts` | Pure helpers. |
| `constants.ts` | Constants. |
| `__tests__/` | Tests. |

View File

@@ -0,0 +1,4 @@
.__camel__ {
display: flex;
color: var(--l1-foreground);
}

View File

@@ -0,0 +1,7 @@
import styles from './__Pascal__.module.scss';
function __Pascal__(): JSX.Element {
return <div className={styles.__camel__} data-testid="__kebab__" />;
}
export default __Pascal__;

View File

@@ -0,0 +1,11 @@
import { render, screen } from 'tests/test-utils';
import __Pascal__ from '../__Pascal__';
describe('__Pascal__', () => {
it('renders', () => {
render(<__Pascal__ />);
expect(screen.getByTestId('__kebab__')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,28 @@
# __Title__
<!-- One paragraph: what this feature does, who uses it, and where it is reachable from. -->
## Structure
| Path | Purpose |
| --- | --- |
| `index.tsx` | Feature entry. Composition only — no business logic. |
| `components/` | Feature-local components, nested as `components/<Name>/`. |
| `hooks/` | Feature hooks, including React Query wrappers over `api/generated`. |
| `store/` | Zustand stores for feature-local client state. |
| `types.ts` | Shared feature types. Split into `types/` past ~3 declarations. |
| `utils.ts` | Pure helpers. |
| `constants.ts` | Feature constants. |
| `__tests__/` | Feature-root tests. Component tests live with the component. |
## Data
<!-- Endpoints this feature reads/writes, and the hooks that wrap them. -->
## State
<!-- What lives in the URL (nuqs), what lives in React Query, what lives in store/. -->
## Routing
<!-- Route key in constants/routes.ts, lazy import in AppRoutes/pageComponents.ts, entry in AppRoutes/routes.ts. -->

View File

@@ -0,0 +1,12 @@
.container {
display: flex;
flex-direction: column;
gap: var(--spacing-3);
padding: var(--spacing-4);
background: var(--l1-background);
}
.title {
color: var(--l1-foreground);
font-size: var(--font-size-lg);
}

View File

@@ -0,0 +1,11 @@
import { render, screen } from 'tests/test-utils';
import __Pascal__ from '../index';
describe('__Pascal__', () => {
it('renders the page', () => {
render(<__Pascal__ />);
expect(screen.getByTestId('__kebab__-page')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,11 @@
import styles from './__Pascal__.module.scss';
function __Pascal__(): JSX.Element {
return (
<section className={styles.container} data-testid="__kebab__-page">
<h1 className={styles.title}>__Title__</h1>
</section>
);
}
export default __Pascal__;

View File

@@ -0,0 +1,20 @@
# __Title__
<!-- One paragraph: what this section of the product is, and what each tab is for. -->
## Structure
| Path | Purpose |
| --- | --- |
| `index.tsx` | Shell. Tab switching only — no feature logic. |
| `constants.tsx` | One `TabRoutes` export per tab: icon, label, route and the view it renders. |
| `<View>/` | One folder per tab, each a self-contained feature. |
## Routing
Every path is registered in `src/constants/routes.ts`, `src/utils/permission/index.ts`,
`src/AppRoutes/routes.ts` and the `routesToSkip` list in
`src/container/TopNav/DateTimeSelectionV2/constants.ts`, all rendering this shell through the
lazy import in `src/AppRoutes/pageComponents.ts`. The base path redirects to the first tab;
`RouteTab` picks the tab from the current path. Adding a tab means a new `ROUTES` key, a
route entry, a permission entry, a `routesToSkip` entry and a `TabRoutes` export here.

View File

@@ -0,0 +1,21 @@
.shell {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
:global(.ant-tabs-nav) {
padding: 0 var(--spacing-8);
margin-bottom: 0;
&::before {
border-bottom: 1px solid var(--l1-border) !important;
}
}
}
.tabItem {
display: flex;
align-items: center;
gap: var(--spacing-4);
}

View File

@@ -0,0 +1,25 @@
import { render, screen } from 'tests/test-utils';
import ROUTES from 'constants/routes';
import { __FIRST_TAB__ } from '../constants';
import __Pascal__ from '../index';
describe('__Pascal__', () => {
it('renders one tab per view', () => {
render(<__Pascal__ />, undefined, { initialRoute: __FIRST_TAB__.route });
expect(screen.getByTestId('__kebab__-shell')).toBeInTheDocument();
__TAB_ASSERTIONS__ });
it('renders the view for the active tab', () => {
render(<__Pascal__ />, undefined, { initialRoute: __FIRST_TAB__.route });
expect(screen.getByTestId('__FIRST_VIEW_TESTID__')).toBeInTheDocument();
});
it('redirects the base path to the first tab', () => {
render(<__Pascal__ />, undefined, { initialRoute: __BASE_ROUTE__ });
expect(screen.getByTestId('__FIRST_VIEW_TESTID__')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,9 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
__ICON_IMPORTS__
__VIEW_IMPORTS__
import styles from './__Pascal__.module.scss';
__TAB_EXPORTS__

View File

@@ -0,0 +1,32 @@
import { matchPath, Redirect, useLocation } from 'react-router-dom';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { __TAB_NAMES__ } from './constants';
import styles from './__Pascal__.module.scss';
function __Pascal__(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [__TAB_NAMES__];
if (matchPath(pathname, { path: __BASE_ROUTE__, exact: true })) {
return <Redirect to={routes[0].route} />;
}
return (
<div className={styles.shell} data-testid="__kebab__-shell">
<RouteTab
routes={routes}
activeKey={pathname}
history={history}
showRightSection={false}
/>
</div>
);
}
export default __Pascal__;

View File

@@ -295,6 +295,8 @@
// Prevents bracket access on CSS modules (styles['kebab-case']) which fails with camelCaseOnly config
"signoz/no-dashboard-fetch-outside-root": "error",
// Forces useDashboardFetchRequired() outside the root V2 pages (allowlisted in overrides below)
"signoz/no-msw-in-story-file": "error",
// Bans msw imports in *.stories.tsx; handlers/mock data belong in the sibling .stories.mocks.tsx
"no-restricted-globals": [
"error",
{

View File

@@ -27,12 +27,22 @@ const mockAliases = [
find: /^(?:src\/)?api\/common\/logEvent$/,
replacement: `${srcPath}/storybook/mocks/logEvent.mock.ts`,
},
{
// jest: not replaced, the suite mounts a mock store per test.
find: /^(?:src\/)?store$/,
replacement: `${srcPath}/storybook/mocks/store.mock.ts`,
},
{
// jest: __mocks__/env.ts, which leaves `baseURL` empty because jsdom already
// resolves a relative `/api/...` against `http://localhost`.
find: /^(?:src\/)?constants\/env$/,
replacement: `${srcPath}/storybook/mocks/env.mock.ts`,
},
{
// jest: not replaced, a test opens the one tooltip it is about.
find: /^@signozhq\/ui\/tooltip$/,
replacement: `${srcPath}/storybook/mocks/tooltip.mock.tsx`,
},
];
/**
@@ -55,12 +65,12 @@ const isExcluded = (plugin: PluginOption): boolean =>
const config: StorybookConfig = {
framework: '@storybook/react-vite',
stories: ['../src/**/*.stories.@(ts|tsx)'],
stories: ['../src/storybook/docs/**/*.mdx', '../src/**/*.stories.@(ts|tsx)'],
// `../public` carries the fonts, icons and i18n bundles the app expects at
// the root; `./public` carries the msw worker, which must not ship in a
// production build.
staticDirs: ['../public', './public'],
addons: ['@storybook/addon-a11y'],
addons: ['@storybook/addon-a11y', '@storybook/addon-docs'],
core: { disableTelemetry: true },
viteFinal: async (viteConfig) => {
const plugins = (viteConfig.plugins ?? [])
@@ -77,6 +87,14 @@ const config: StorybookConfig = {
return {
...viteConfig,
build: {
...viteConfig.build,
// `vite.config.ts` sets this for the app; Storybook's builder replaces
// `build` wholesale, which leaves rolldown-vite on its default
// lightningcss. That one rejects `:global()` in a plain stylesheet, which
// the app has, and the static build dies in CSS minification.
cssMinify: 'esbuild',
},
plugins,
resolve: {
...viteConfig.resolve,

View File

@@ -6,6 +6,17 @@
-->
<link rel="stylesheet" href="storybook-fonts.css" />
<!--
Third-party frames are the one thing msw cannot answer: a cross-origin iframe
navigates outside the service worker's scope, so the YouTube embeds and the
docs pane in onboarding reach the real network. Same intent as the boot data
below, enforced by the browser instead.
-->
<meta
http-equiv="Content-Security-Policy"
content="frame-src 'self' blob: data:"
/>
<link rel="stylesheet" href="css/uPlot.min.css" />
<script>
@@ -24,3 +35,38 @@
},
};
</script>
<script>
// The wall clock every story reads. Chart windows, `4 mins ago` labels and
// trial countdowns all derive from `now`, and Chromatic does not freeze the
// clock, so a live one redraws every chart axis between two builds of the
// same code. `performance.now` and the timers keep running, so anything
// waiting on a timeout still resolves. `?storyClock=live`, or an ISO
// instant, overrides it.
//
// `new Date()` is the frozen instant, which is what the app renders from.
// `Date.now()` runs on from it instead, because it is also what code measures
// elapsed time with: `lodash.debounce` compares two `Date.now()` readings to
// decide its trailing call is due, so a frozen one re-arms its timer forever
// and every debounced input in the app (the onboarding catalogue search, the
// pipelines search, the log filter) silently stops filtering.
(() => {
const asked = new URLSearchParams(window.location.search).get('storyClock');
if (asked === 'live') return;
const frozen = Date.parse(asked || '2026-06-15T12:00:00.000Z');
if (Number.isNaN(frozen)) return;
const RealDate = Date;
const started = performance.now();
class FrozenDate extends RealDate {
constructor(...args) {
super(...(args.length ? args : [frozen]));
}
static now() {
return frozen + (performance.now() - started);
}
}
Object.defineProperty(window, 'Date', { value: FrozenDate, writable: true });
})();
</script>

View File

@@ -1,8 +1,11 @@
import type { Preview } from '@storybook/react-vite';
import type { SetupWorker } from 'msw';
import { setupWorker } from 'msw';
import { configure } from 'storybook/test';
import { settleForCapture } from '../src/storybook/visual/settleForCapture';
import PageDocs from '../src/storybook/docs/PageDocs';
import ThemedDocsContainer from '../src/storybook/docs/ThemedDocsContainer';
import { withProviders } from '../src/storybook/decorators/withProviders';
import { globalMocks } from '../src/storybook/globals';
import { resetStoryHistory } from '../src/storybook/navigation/containment';
@@ -13,7 +16,12 @@ import {
} from '../src/storybook/runtime/resolveStory';
import { allModes } from './modes';
import '../src/ReactI18';
import i18n from '../src/ReactI18';
// `src/index.tsx` does this at boot: without it `@monaco-editor/react` falls back
// to its loader default and pulls Monaco from cdn.jsdelivr.net, which msw does
// not report because the requests look like static assets.
import '../src/lib/monaco/setup';
import '../src/styles.scss';
@@ -63,10 +71,131 @@ const { worker, ready } = (holder.__signozStorybookWorker ??=
};
})());
/**
* `t()` answers with the key until the namespace's JSON has landed, and a `play`
* that clicks as soon as the story renders is quick enough to catch it: the
* channel form's "Channel name is mandatory" arrives as `channel_name_required`.
* Every namespace under `public/locales/en` is loaded once, ahead of the first
* story.
*/
const translationsReady = i18n.loadNamespaces(
Object.keys(import.meta.glob('../public/locales/en/*.json')).map((path) =>
path.slice(path.lastIndexOf('/') + 1, -'.json'.length),
),
);
// testing-library's 1s default for `findBy*` and `waitFor` is shorter than a
// popup or a query takes to land on a loaded CI runner.
configure({ asyncUtilTimeout: 10_000 });
const preview: Preview = {
parameters: {
layout: 'fullscreen',
controls: { expanded: true },
// The sidebar order, mirroring the app's own side nav
// (`container/SideNav/menuItems.tsx`), so a page sits where someone would
// click it in the product. Storybook's default is the order the story files
// happen to be globbed in, which puts `src/modules` first. Anything missing
// from a level lands after the entries listed for it, in file order, so a new
// story shows up at the end of its area rather than disappearing. Stories
// inside a file are never listed, so they keep the order they are declared
// in, `Default` first. Storybook parses this out of the file, so it has to
// stay an inline literal.
options: {
storySort: {
order: [
'Docs',
'Pages',
[
'Home',
'Alerts',
[
'Rules',
'Triggered',
'Overview',
'History',
'Create',
'Edit',
'Planned Downtime',
'Routing Policies',
'Channels',
['List', 'New', 'Edit'],
],
'Dashboards',
['List', 'Detail', 'Panel Editor', 'Public'],
'Services',
['List', 'Detail', 'Top Level Operations', 'Service Map'],
'Logs',
['Explorer', 'Live Tail', 'Saved Views', 'Pipelines', 'Settings'],
'Traces',
['Explorer', 'Trace Details', 'Funnel Details'],
'Metrics',
['Explorer'],
'Infrastructure',
[
'Overview',
'Kubernetes',
[
'Clusters',
'Nodes',
'Namespaces',
'Pods',
'Deployments',
'DaemonSets',
'StatefulSets',
'Jobs',
'Volumes',
],
],
'Integrations',
['List', 'Details', 'Cloud Account'],
'Exceptions',
['List', 'Detail'],
'External APIs',
'AI Observability',
['Overview', 'Explorer', 'Model Pricing', 'Attribute Mapping'],
'Noz',
'Metering',
['Cost Meter', 'Usage Explorer'],
'Messaging Queues',
['Overview', 'Kafka', 'Kafka Detail', 'Celery'],
'Onboarding',
['Questionnaire', 'Add Data Source'],
'Settings',
[
'Workspace',
'Account',
'Billing',
['Overview', 'Authz'],
'MCP Server',
'Roles',
'Role Details',
'Role Editor',
'Members',
'Service Accounts',
'Ingestion',
'Single Sign-on',
'Keyboard Shortcuts',
],
'Auth',
['Login', 'Sign Up', 'Forgot Password', 'Reset Password'],
'System',
[
'Status',
'Support',
'License',
'Not Found',
'Unauthorized',
'Error Fallback',
'Workspace Locked',
'Workspace Suspended',
'Workspace Access Restricted',
],
],
],
},
},
docs: { page: PageDocs, container: ThemedDocsContainer },
// One cloud snapshot per theme, for every story. A mode carries Storybook
// globals, so `theme` here is the same toolbar global the app reads out of
// localStorage. Widths are Chromatic's only real dimension, as they are
@@ -74,6 +203,9 @@ const preview: Preview = {
// one it is given.
chromatic: { modes: allModes },
},
// Every page story gets a docs page: the descriptions on the meta and on each
// story are the page's documentation, and without this they render nowhere.
tags: ['autodocs'],
globalTypes: {
theme: {
description: 'SigNoz color scheme',
@@ -119,12 +251,21 @@ const preview: Preview = {
world.apply();
world.install(worker);
await ready;
await Promise.all([ready, translationsReady]);
},
],
beforeEach: () => {
clearBlockedNavigations();
resetStoryHistory();
// The runner clears its console/network buffer before it navigates, so
// anything the outgoing story still has in flight would be reported
// against this one. Stamping the moment this story starts gives the runner
// a line to discard those by. `Date.now()` is faked for the stories, so
// this reads the one clock the runner's own timestamps share.
document.body.dataset.signozStoryStartedAt = String(
performance.timeOrigin + performance.now(),
);
},
// After `play`, which is the moment both capture stacks shoot at.
afterEach: settleForCapture,

View File

@@ -88,10 +88,16 @@ self.addEventListener('fetch', function (event) {
const { request } = event
const accept = request.headers.get('accept') || ''
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return
}
// msw bypasses server-sent events here, because it answers a request in one
// piece and has no stream to hand back. A story is not a live connection
// either: it wants the backlog a page renders, and one response carries that
// fine. Left bypassed, `/api/v3/logs/livetail` reaches the real network and
// the live tail story is a spinner over ERR_CONNECTION_REFUSED. Restore the
// bypass and re-check `Pages/Logs/Live Tail` if msw regenerates this file.
//
// if (accept.includes('text/event-stream')) {
// return
// }
// Bypass navigation requests.
if (request.mode === 'navigate') {

View File

@@ -25,7 +25,25 @@ const IGNORED_MESSAGES = [
/violates the following Content Security Policy directive/,
];
const messagesByPage = new WeakMap<Page, string[]>();
interface CapturedMessage {
at: number;
text: string;
}
const PREPARE_TIMEOUT_MS = 180_000;
const messagesByPage = new WeakMap<Page, CapturedMessage[]>();
/**
* When the story under test started rendering, stamped by the preview's
* `beforeEach`. Messages captured before it belong to the previous story: the
* runner clears this buffer ahead of the navigation, so whatever that story
* still had in flight lands here.
*/
const storyStartedAt = (page: Page): Promise<number> =>
page
.evaluate(() => Number(document.body.dataset.signozStoryStartedAt ?? 0))
.catch(() => 0);
/**
* Only `console.error` fails a story. `console.warn` is dev-time advice from
@@ -36,6 +54,15 @@ 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',
// The runner's default prepare, minus Playwright's 30s navigation timeout: on
// a cold dev server the first load compiles the whole preview, which outlasts
// it on a shared CI runner.
async prepare({ page }): Promise<void> {
await page.goto(new URL('iframe.html', process.env.TARGET_URL).toString(), {
waitUntil: 'load',
timeout: PREPARE_TIMEOUT_MS,
});
},
async preVisit(page): Promise<void> {
const existing = messagesByPage.get(page);
if (existing) {
@@ -43,14 +70,14 @@ const config: TestRunnerConfig = {
return;
}
const messages: string[] = [];
const messages: CapturedMessage[] = [];
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()}`);
messages.push({ at: Date.now(), text: `[error] ${message.text()}` });
}
});
// The console message alone ("Failed to load resource") doesn't name the
@@ -58,12 +85,23 @@ const config: TestRunnerConfig = {
// actionable instead of just a status code.
page.on('response', (response) => {
if (response.status() >= 400) {
messages.push(`[response] ${response.status()} ${response.url()}`);
messages.push({
at: Date.now(),
text: `[response] ${response.status()} ${response.url()}`,
});
}
});
},
async postVisit(page, context): Promise<void> {
const messages = messagesByPage.get(page) ?? [];
const captured = messagesByPage.get(page) ?? [];
if (captured.length === 0) {
return;
}
const startedAt = await storyStartedAt(page);
const messages = captured
.filter((message) => message.at >= startedAt)
.map((message) => message.text);
if (messages.length === 0) {
return;
}

View File

@@ -10,6 +10,7 @@
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build -o storybook-static",
"test:storybook": "bash scripts/test-storybook.sh",
"scaffold": "node .claude/skills/scaffold-feature/scaffold.mjs",
"build": "vite build",
"preview": "vite preview",
"prettify": "oxfmt",
@@ -162,6 +163,7 @@
"@jest/globals": "30.4.1",
"@jest/types": "30.2.0",
"@storybook/addon-a11y": "10.5.9",
"@storybook/addon-docs": "10.5.9",
"@storybook/react-vite": "10.5.9",
"@storybook/test-runner": "0.24.5",
"@testing-library/dom": "8.20.0",

View File

@@ -0,0 +1,41 @@
/**
* Rule: no-msw-in-story-file
*
* A `.stories.tsx` file is the human-facing surface: it must not carry msw
* handlers or response payloads. Those belong in the sibling
* `<Page>.stories.mocks.tsx` module (and its `__story_mockdata__` builders).
*
* This rule flags any import from `msw` inside a `*.stories.tsx` file. It
* does not match `*.stories.mocks.tsx`, which is where msw imports belong.
*/
export default {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow importing from msw inside a .stories.tsx file; move handlers/mock data to the sibling .stories.mocks.tsx module',
category: 'Storybook',
},
schema: [],
messages: {
noMsw:
'Do not import from msw in a .stories.tsx file. Move the handler and its mock data to the sibling <Page>.stories.mocks.tsx module (and __story_mockdata__ for builders).',
},
},
create(context) {
const filename = context.filename || '';
if (!filename.endsWith('.stories.tsx')) {
return {};
}
return {
ImportDeclaration(node) {
if (node.source.value === 'msw') {
context.report({ node, messageId: 'noMsw' });
}
},
};
},
};

View File

@@ -15,6 +15,7 @@ import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root.mjs';
import noConditionalTextNodesWithSiblings from './rules/no-conditional-text-nodes-with-siblings.mjs';
import noReturnTextNodes from './rules/no-return-text-nodes.mjs';
import noMswInStoryFile from './rules/no-msw-in-story-file.mjs';
export default {
meta: {
@@ -31,5 +32,6 @@ export default {
'no-dashboard-fetch-outside-root': noDashboardFetchOutsideRoot,
'no-conditional-text-nodes-with-siblings': noConditionalTextNodesWithSiblings,
'no-return-text-nodes': noReturnTextNodes,
'no-msw-in-story-file': noMswInStoryFile,
},
};

View File

@@ -363,6 +363,9 @@ importers:
'@storybook/addon-a11y':
specifier: 10.5.9
version: 10.5.9(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
'@storybook/addon-docs':
specifier: 10.5.9
version: 10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
'@storybook/react-vite':
specifier: 10.5.9
version: 10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(esbuild@0.28.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))(typescript@5.9.3)
@@ -2184,6 +2187,12 @@ packages:
'@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
'@mdx-js/react@3.1.1':
resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
peerDependencies:
'@types/react': '>=16'
react: '>=16'
'@monaco-editor/loader@1.7.0':
resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==}
@@ -3766,6 +3775,15 @@ packages:
peerDependencies:
storybook: ^10.5.9
'@storybook/addon-docs@10.5.9':
resolution: {integrity: sha512-8sFsMkZYrrdqCLdV+hnwTwDF7RaBsBPRwl4wfc8ve9Q/7Yhi5REe/Xjvd8x1yn6fBPPw9tnID9dx6Agdnr81fw==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
storybook: ^10.5.9
peerDependenciesMeta:
'@types/react':
optional: true
'@storybook/builder-vite@10.5.9':
resolution: {integrity: sha512-Zg4JbGQiHFPGlFJ9HM+XPgzKmU/RFPCymhohVRJhBBYfmgaQgz0flWWzscseCDpl638MNd8/r/H+nwuoBgSYDg==}
peerDependencies:
@@ -4189,6 +4207,9 @@ packages:
'@types/mdast@4.0.3':
resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==}
'@types/mdx@2.0.14':
resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==}
'@types/ms@0.7.31':
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
@@ -12199,6 +12220,12 @@ snapshots:
'@marijn/find-cluster-break@1.0.2': {}
'@mdx-js/react@3.1.1(@types/react@18.0.26)(react@18.2.0)':
dependencies:
'@types/mdx': 2.0.14
'@types/react': 18.0.26
react: 18.2.0
'@monaco-editor/loader@1.7.0':
dependencies:
state-local: 1.0.7
@@ -13619,6 +13646,25 @@ snapshots:
axe-core: 4.13.0
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
'@storybook/addon-docs@10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
dependencies:
'@mdx-js/react': 3.1.1(@types/react@18.0.26)(react@18.2.0)
'@storybook/csf-plugin': 10.5.9(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
'@storybook/icons': 2.1.0(react@18.2.0)
'@storybook/react-dom-shim': 10.5.9(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
storybook: 10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0)
ts-dedent: 2.3.0
optionalDependencies:
'@types/react': 18.0.26
transitivePeerDependencies:
- '@types/react-dom'
- esbuild
- rollup
- vite
- webpack
'@storybook/builder-vite@10.5.9(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))':
dependencies:
'@storybook/csf-plugin': 10.5.9(esbuild@0.28.1)(rolldown-vite@7.3.1(@types/node@16.18.25)(esbuild@0.28.1)(jiti@2.6.1)(less@4.9.0)(sass@1.97.3)(stylus@0.62.0)(terser@5.46.2)(yaml@2.9.1))(storybook@10.5.9(@types/react@18.0.26)(prettier@3.8.3)(react@18.2.0))
@@ -14064,6 +14110,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.2
'@types/mdx@2.0.14': {}
'@types/ms@0.7.31': {}
'@types/node@16.18.25': {}

View File

@@ -61,6 +61,7 @@
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_EXPLORER": "SigNoz | AI Observability Explorer",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
}

View File

@@ -42,7 +42,6 @@
"NOT_FOUND": "SigNoz | Page Not Found",
"LOGS": "SigNoz | Logs",
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
"OLD_LOGS_EXPLORER": "SigNoz | Old Logs Explorer",
"LIVE_LOGS": "SigNoz | Live Logs",
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
"HOME_PAGE": "Open source Observability Platform | SigNoz",
@@ -86,6 +85,7 @@
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_EXPLORER": "SigNoz | AI Observability Explorer",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
}

View File

@@ -28,4 +28,4 @@ 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 "$@"
pnpm exec test-storybook --ci --maxWorkers=2 --testTimeout 30000 "$@"

View File

@@ -8,7 +8,6 @@ import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useIsAIObservabilityEnabled } from 'hooks/useIsAIObservabilityEnabled';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
@@ -44,7 +43,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const isAdmin = user.role === USER_ROLES.ADMIN;
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isAIObservabilityEnabled = useIsAIObservabilityEnabled();
const mapRoutes = useMemo(
() =>
new Map(
@@ -135,14 +133,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
return <Redirect to={ROUTES.HOME} />;
}
if (
(pathname.startsWith(`${ROUTES.AI_OBSERVABILITY_BASE}/`) ||
pathname === ROUTES.AI_OBSERVABILITY_BASE) &&
!isAIObservabilityEnabled
) {
return <Redirect to={ROUTES.HOME} />;
}
// Check for workspace access restriction (cloud only)
const isCloudPlatform = activeLicense?.platform === LicensePlatform.CLOUD;

View File

@@ -1597,10 +1597,6 @@ describe('PrivateRoute', () => {
LOGS: { path: ROUTES.LOGS, deniedRoles: DENIED_ROLES },
LOGS_EXPLORER: { path: ROUTES.LOGS_EXPLORER, deniedRoles: DENIED_ROLES },
LIVE_LOGS: { path: ROUTES.LIVE_LOGS, deniedRoles: DENIED_ROLES },
OLD_LOGS_EXPLORER: {
path: ROUTES.OLD_LOGS_EXPLORER,
deniedRoles: DENIED_ROLES,
},
METRICS_EXPLORER: {
path: ROUTES.METRICS_EXPLORER,
deniedRoles: DENIED_ROLES,

View File

@@ -154,14 +154,6 @@ export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
);
export const LogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsModulePage'),
);
export const OldLogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/Logs'),
);
export const LiveLogs = Loadable(
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
);

View File

@@ -26,13 +26,11 @@ import {
LiveLogs,
Login,
Logs,
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
OldLogsExplorer,
OnboardingV2,
OrgOnboarding,
PasswordReset,
@@ -284,20 +282,6 @@ const routes: AppRoutes[] = [
key: 'LOGS',
isPrivate: true,
},
{
path: ROUTES.LOGS_EXPLORER,
exact: true,
component: LogsExplorer,
key: 'LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.OLD_LOGS_EXPLORER,
exact: true,
component: OldLogsExplorer,
key: 'OLD_LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.LIVE_LOGS,
exact: true,

View File

@@ -36,16 +36,12 @@ import type {
GetDashboardV2200,
GetDashboardV2PathParameters,
GetPublicDashboard200,
GetPublicDashboardData200,
GetPublicDashboardDataPathParameters,
GetPublicDashboardDataV2200,
GetPublicDashboardDataV2PathParameters,
GetPublicDashboardPanelQueryRangeV2200,
GetPublicDashboardPanelQueryRangeV2Params,
GetPublicDashboardPanelQueryRangeV2PathParameters,
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
GetSystemDashboard200,
GetSystemDashboardPathParameters,
ListDashboardViews200,
@@ -474,217 +470,6 @@ export const useUpdatePublicDashboard = <
> => {
return useMutation(getUpdatePublicDashboardMutationOptions(options));
};
/**
* This endpoint returns the sanitized dashboard data for public access
* @summary Get public dashboard data
*/
export const getPublicDashboardData = (
{ id }: GetPublicDashboardDataPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardData200>({
url: `/api/v1/public/dashboards/${id}`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardDataQueryKey = ({
id,
}: GetPublicDashboardDataPathParameters) => {
return [`/api/v1/public/dashboards/${id}`] as const;
};
export const getGetPublicDashboardDataQueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardData>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetPublicDashboardDataQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardData>>
> = ({ signal }) => getPublicDashboardData({ id }, signal);
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardDataQueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardData>>
>;
export type GetPublicDashboardDataQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get public dashboard data
*/
export function useGetPublicDashboardData<
TData = Awaited<ReturnType<typeof getPublicDashboardData>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardDataQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get public dashboard data
*/
export const invalidateGetPublicDashboardData = async (
queryClient: QueryClient,
{ id }: GetPublicDashboardDataPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardDataQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint return query range results for a widget of public dashboard
* @summary Get query range result
*/
export const getPublicDashboardWidgetQueryRange = (
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardWidgetQueryRange200>({
url: `/api/v1/public/dashboards/${id}/widgets/${idx}/query_range`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardWidgetQueryRangeQueryKey = ({
id,
idx,
}: GetPublicDashboardWidgetQueryRangePathParameters) => {
return [`/api/v1/public/dashboards/${id}/widgets/${idx}/query_range`] as const;
};
export const getGetPublicDashboardWidgetQueryRangeQueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetPublicDashboardWidgetQueryRangeQueryKey({ id, idx });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>
> = ({ signal }) => getPublicDashboardWidgetQueryRange({ id, idx }, signal);
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined && idx !== null && idx !== undefined,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardWidgetQueryRangeQueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>
>;
export type GetPublicDashboardWidgetQueryRangeQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get query range result
*/
export function useGetPublicDashboardWidgetQueryRange<
TData = Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardWidgetQueryRangeQueryOptions(
{ id, idx },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get query range result
*/
export const invalidateGetPublicDashboardWidgetQueryRange = async (
queryClient: QueryClient,
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardWidgetQueryRangeQueryKey({ id, idx }) },
options,
);
return queryClient;
};
/**
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
* @summary List dashboard saved views

View File

@@ -150,7 +150,7 @@ export const invalidateListLLMPricingRules = async (
};
/**
* Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.
* Single write endpoint used by both the user and the Zeus sync job. Rules without isOverride are matched by sourceId and override rows (is_override=true) are skipped. Rules with isOverride are matched by id and inserted when new.
* @summary Create or update pricing rules
*/
export const createOrUpdateLLMPricingRules = (

View File

@@ -24,8 +24,6 @@ import type {
GetMetricAlertsParams,
GetMetricAttributes200,
GetMetricAttributesParams,
GetMetricDashboards200,
GetMetricDashboardsParams,
GetMetricDashboardsV2200,
GetMetricDashboardsV2Params,
GetMetricHighlights200,
@@ -1096,104 +1094,6 @@ export const invalidateGetMetricAttributes = async (
return queryClient;
};
/**
* This endpoint returns associated dashboards for a specified metric
* @summary Get metric dashboards
*/
export const getMetricDashboards = (
params: GetMetricDashboardsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricDashboards200>({
url: `/api/v2/metrics/dashboards`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricDashboardsQueryKey = (
params?: GetMetricDashboardsParams,
) => {
return [`/api/v2/metrics/dashboards`, ...(params ? [params] : [])] as const;
};
export const getGetMetricDashboardsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricDashboards>>
> = ({ signal }) => getMetricDashboards(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricDashboardsQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricDashboards>>
>;
export type GetMetricDashboardsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get metric dashboards
*/
export function useGetMetricDashboards<
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricDashboardsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get metric dashboards
*/
export const invalidateGetMetricDashboards = async (
queryClient: QueryClient,
params: GetMetricDashboardsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricDashboardsQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric
* @summary Get metric highlights

View File

@@ -4886,50 +4886,6 @@ export interface DashboardtypesCustomVariableSpecDTO {
customValue: string;
}
export interface DashboardtypesStorableDashboardDataDTO {
[key: string]: unknown;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
integration = 'integration',
}
export interface DashboardtypesDashboardDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
data?: DashboardtypesStorableDashboardDataDTO;
/**
* @type string
*/
id?: string;
/**
* @type boolean
*/
locked?: boolean;
/**
* @type string
*/
org_id?: string;
source?: DashboardtypesSourceDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface DashboardtypesDashboardPanelRefDTO {
/**
* @type string
@@ -5868,6 +5824,11 @@ export interface DashboardtypesDashboardViewDTO {
updatedAt?: string;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
integration = 'integration',
}
export interface TagtypesGettableTagDTO {
/**
* @type string
@@ -5945,11 +5906,6 @@ export interface DashboardtypesGettablePublicDasbhboardDTO {
timeRangeEnabled?: boolean;
}
export interface DashboardtypesGettablePublicDashboardDataDTO {
dashboard?: DashboardtypesDashboardDTO;
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export interface DashboardtypesGettablePublicDashboardDataV2DTO {
dashboard?: DashboardtypesGettableDashboardV2DTO;
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
@@ -9022,25 +8978,6 @@ export interface MetricsexplorertypesMetricAttributesResponseDTO {
totalKeys: number;
}
export interface MetricsexplorertypesMetricDashboardDTO {
/**
* @type string
*/
dashboardId: string;
/**
* @type string
*/
dashboardName: string;
/**
* @type string
*/
widgetId: string;
/**
* @type string
*/
widgetName: string;
}
export interface MetricsexplorertypesMetricDashboardPanelsResponseDTO {
/**
* @type array,null
@@ -9048,13 +8985,6 @@ export interface MetricsexplorertypesMetricDashboardPanelsResponseDTO {
dashboards: DashboardtypesDashboardPanelRefDTO[] | null;
}
export interface MetricsexplorertypesMetricDashboardsResponseDTO {
/**
* @type array,null
*/
dashboards: MetricsexplorertypesMetricDashboardDTO[] | null;
}
export interface MetricsexplorertypesMetricHighlightsResponseDTO {
/**
* @type integer
@@ -12488,29 +12418,6 @@ export type GetOrgPreference200 = {
export type UpdateOrgPreferencePathParameters = {
name: string;
};
export type GetPublicDashboardDataPathParameters = {
id: string;
};
export type GetPublicDashboardData200 = {
data: DashboardtypesGettablePublicDashboardDataDTO;
/**
* @type string
*/
status: string;
};
export type GetPublicDashboardWidgetQueryRangePathParameters = {
id: string;
idx: string;
};
export type GetPublicDashboardWidgetQueryRange200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status: string;
};
export type ListRoles200 = {
/**
* @type array
@@ -13468,22 +13375,6 @@ export type GetMetricAttributes200 = {
status: string;
};
export type GetMetricDashboardsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricDashboards200 = {
data: MetricsexplorertypesMetricDashboardsResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricHighlightsParams = {
/**
* @type string

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
const addToSelectedFields = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.post(`/logs/fields`, props);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return Promise.reject(ErrorResponseHandler(error as AxiosError));
}
};
export default addToSelectedFields;

View File

@@ -1,26 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/getLogs';
const GetLogs = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.get(`/logs`, {
params: props,
});
return {
statusCode: 200,
error: null,
message: '',
payload: data.data.results,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetLogs;

View File

@@ -1,26 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/getLogsAggregate';
const GetLogsAggregate = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.get(`/logs/aggregate`, {
params: props,
});
return {
statusCode: 200,
error: null,
message: '',
payload: data.data.items,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetLogsAggregate;

View File

@@ -1,24 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/logs/getSearchFields';
const GetSearchFields = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const data = await axios.get(`/logs/fields`);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetSearchFields;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
const removeSelectedField = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.post(`/logs/fields`, props);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return Promise.reject(ErrorResponseHandler(error as AxiosError));
}
};
export default removeSelectedField;

View File

@@ -1,22 +0,0 @@
import apiV1 from 'api/apiV1';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { withBasePath } from 'utils/basePath';
// 10 min in ms
const TIMEOUT_IN_MS = 10 * 60 * 1000;
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
new EventSourcePolyfill(
ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
{
headers: {
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
},
heartbeatTimeout: TIMEOUT_IN_MS,
},
);

View File

@@ -1,6 +1,6 @@
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 92.2 65" style="enable-background:new 0 0 92.2 65;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st0{fill:#7A8291;}
</style>
<metadata>
<sfw xmlns="ns_sfw;">

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 714 B

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 16 15" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0777 13.984C14.945 14.6345 16.2458 14.2008 15.0533 13.0084C11.476 9.53949 12.2349 0 7.79033 0C3.34579 0 4.10461 9.53949 0.527295 13.0084C-0.773543 14.3092 0.635692 14.6345 1.50293 13.984C4.86344 11.7076 4.64663 7.69664 7.79033 7.69664C10.934 7.69664 10.7172 11.7076 14.0777 13.984Z" fill="#4285F4" />
</svg>

After

Width:  |  Height:  |  Size: 394 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><g fill="none" fill-rule="evenodd"><path fill="#c925d1" d="M0 0h64v64H0z"/><path fill="#fff" d="M36.213 16.011h-2.016v-2.005h2.016V12h2.017v2.006h2.016v2.005H38.23v2.006h-2.017zm9.074 8.023h-2.016v-2.006h2.016v-2.005h2.016v2.005h2.017v2.006h-2.017v2.006h-2.016v-2.006m-3.434 22.64c-1.495-3.292-4.482-6.263-7.792-7.75 3.31-1.487 6.297-4.459 7.792-7.75 1.494 3.291 4.482 6.263 7.791 7.75-3.31 1.487-6.297 4.458-7.79 7.75m12.139-8.753c-5.202 0-11.13-5.898-11.13-11.071 0-.555-.452-1.003-1.009-1.003s-1.008.448-1.008 1.003c0 5.173-5.93 11.071-11.13 11.071-.558 0-1.009.448-1.009 1.003s.45 1.003 1.008 1.003c5.202 0 11.13 5.898 11.13 11.07 0 .554.452 1.003 1.01 1.003.556 0 1.007-.45 1.007-1.003 0-5.172 5.93-11.07 11.13-11.07.558 0 1.009-.45 1.009-1.003 0-.555-.45-1.003-1.008-1.003m-31.39-19.904c6.85 0 10.587 1.988 10.587 3.008s-3.737 3.009-10.586 3.009-10.587-1.988-10.587-3.009 3.737-3.008 10.587-3.008m-.35 15.15c-5.012 0-8.62-1.063-10.236-2.19v-7.113c2.367 1.433 6.487 2.176 10.587 2.176s8.22-.743 10.586-2.176v6.484c-1.016 1.406-5.247 2.819-10.936 2.819M33.19 45.975c0 1.435-4.126 3.52-10.59 3.52-6.46 0-10.583-2.085-10.583-3.52v-4.407c2.258 1.354 6.014 2.192 10.276 2.192 2.942 0 5.786-.413 8.01-1.166l-.651-1.898c-2.019.682-4.633 1.058-7.359 1.058-5.39 0-9.252-1.402-10.276-2.76v-5.707c2.411 1.176 6.113 1.885 10.237 1.885 3.92 0 8.34-.725 10.936-2.269v2.162h2.016v-14.04c0-3.292-6.34-5.014-12.602-5.014S10 17.733 10 21.025v24.95c0 3.59 6.49 5.526 12.598 5.526 6.112 0 12.607-1.937 12.607-5.526v-2.887h-2.016z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,6 +1,6 @@
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 50.6 50.6" style="enable-background:new 0 0 50.6 50.6;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st0{fill:#7A8291;}
</style>
<metadata>
<sfw xmlns="ns_sfw;">

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m23.365 13.556-1.442-2.895V8.994c0-2.764-2.218-5.002-4.954-5.002h-2.464c.178-.367.276-.779.276-1.213A2.77 2.77 0 0 0 12.018 0a2.77 2.77 0 0 0-2.763 2.779c0 .434.098.846.276 1.213H7.067c-2.736 0-4.954 2.238-4.954 5.002v1.667L.64 13.549c-.149.29-.149.636 0 .927l1.472 2.855v1.667C2.113 21.762 4.33 24 7.067 24h9.902c2.736 0 4.954-2.238 4.954-5.002V17.33l1.44-2.865c.143-.286.143-.622.002-.91m-12.854 2.36a2.27 2.27 0 0 1-2.261 2.273 2.27 2.27 0 0 1-2.261-2.273v-4.042A2.27 2.27 0 0 1 8.249 9.6a2.267 2.267 0 0 1 2.262 2.274zm7.285 0a2.27 2.27 0 0 1-2.26 2.273 2.27 2.27 0 0 1-2.262-2.273v-4.042A2.267 2.267 0 0 1 15.535 9.6a2.267 2.267 0 0 1 2.261 2.274z" fill="#7A8291" />
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@@ -1,6 +1,6 @@
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 43 43" style="enable-background:new 0 0 43 43;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#0074A2;}
</style>
<metadata>
<sfw xmlns="ns_sfw;">

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23" fill="#7A8291" />
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@@ -3,7 +3,7 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 800.5 907.77" style="enable-background:new 0 0 800.5 907.77;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st0{fill:#8B5CC7;}
</style>
<path class="st0" d="M303.36,238.61c31.36-21.37,71.76-12.97,65-6.53c-12.89,12.28,4.26,8.65,6.11,31.31
c1.36,16.69-4.09,25.88-8.78,31.11c-9.79,1.28-21.69,3.67-36.02,8.33c-8.48,2.76-15.85,5.82-22.31,8.9

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,10 @@
<svg role="img" viewBox="0 0 275 287" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5584 193.736H114.275V227.925H14.5584V193.736Z" fill="#7A8291" />
<path d="M148.464 74.076H262.426V108.265H148.464V74.076Z" fill="#7A8291" />
<path d="M88.6338 84.6127L173.246 0L197.422 24.175L112.809 108.788L88.6338 84.6127Z" fill="#7A8291" />
<path d="M89.157 170.084L24.175 105.102L0 129.277L64.9819 194.259L89.157 170.084Z" fill="#7A8291" />
<path d="M174.629 217.911L106.133 286.407L81.9577 262.232L150.454 193.736L174.629 217.911Z" fill="#7A8291" />
<path d="M174.106 132.44L250.66 208.994L274.835 184.819L198.281 108.265L174.106 132.44Z" fill="#7A8291" />
<path d="M88.6338 48.434V131.057H54.4451L54.4451 48.434H88.6338Z" fill="#7A8291" />
<path d="M208.294 168.094V270.66H174.106V168.094H208.294Z" fill="#7A8291" />
</svg>

After

Width:  |  Height:  |  Size: 825 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Deno</title><path d="M1.105 18.02A11.9 11.9 0 0 1 0 12.985q0-.698.078-1.376a12 12 0 0 1 .231-1.34A12 12 0 0 1 4.025 4.02a12 12 0 0 1 5.46-2.771 12 12 0 0 1 3.428-.23c1.452.112 2.825.477 4.077 1.05a12 12 0 0 1 2.78 1.774 12.02 12.02 0 0 1 4.053 7.078A12 12 0 0 1 24 12.985q0 .454-.036.914a12 12 0 0 1-.728 3.305 12 12 0 0 1-2.38 3.875c-1.33 1.357-3.02 1.962-4.43 1.936a4.4 4.4 0 0 1-2.724-1.024c-.99-.853-1.391-1.83-1.53-2.919a5 5 0 0 1 .128-1.518c.105-.38.37-1.116.76-1.437-.455-.197-1.04-.624-1.226-.829-.045-.05-.04-.13 0-.183a.155.155 0 0 1 .177-.053c.392.134.869.267 1.372.35.66.111 1.484.25 2.317.292 2.03.1 4.153-.813 4.812-2.627s.403-3.609-1.96-4.685-3.454-2.356-5.363-3.128c-1.247-.505-2.636-.205-4.06.582-3.838 2.121-7.277 8.822-5.69 15.032a.191.191 0 0 1-.315.19 12 12 0 0 1-1.25-1.634 12 12 0 0 1-.769-1.404M11.57 6.087c.649-.051 1.214.501 1.31 1.236.13.979-.228 1.99-1.41 2.013-1.01.02-1.315-.997-1.248-1.614.066-.616.574-1.575 1.35-1.635"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="#7A8291" viewBox="0 0 24 24"><title>Deno</title><path d="M1.105 18.02A11.9 11.9 0 0 1 0 12.985q0-.698.078-1.376a12 12 0 0 1 .231-1.34A12 12 0 0 1 4.025 4.02a12 12 0 0 1 5.46-2.771 12 12 0 0 1 3.428-.23c1.452.112 2.825.477 4.077 1.05a12 12 0 0 1 2.78 1.774 12.02 12.02 0 0 1 4.053 7.078A12 12 0 0 1 24 12.985q0 .454-.036.914a12 12 0 0 1-.728 3.305 12 12 0 0 1-2.38 3.875c-1.33 1.357-3.02 1.962-4.43 1.936a4.4 4.4 0 0 1-2.724-1.024c-.99-.853-1.391-1.83-1.53-2.919a5 5 0 0 1 .128-1.518c.105-.38.37-1.116.76-1.437-.455-.197-1.04-.624-1.226-.829-.045-.05-.04-.13 0-.183a.155.155 0 0 1 .177-.053c.392.134.869.267 1.372.35.66.111 1.484.25 2.317.292 2.03.1 4.153-.813 4.812-2.627s.403-3.609-1.96-4.685-3.454-2.356-5.363-3.128c-1.247-.505-2.636-.205-4.06.582-3.838 2.121-7.277 8.822-5.69 15.032a.191.191 0 0 1-.315.19 12 12 0 0 1-1.25-1.634 12 12 0 0 1-.769-1.404M11.57 6.087c.649-.051 1.214.501 1.31 1.236.13.979-.228 1.99-1.41 2.013-1.01.02-1.315-.997-1.248-1.614.066-.616.574-1.575 1.35-1.635"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="#FFF" viewBox="0 0 50 50"><path d="M7 2v46h36V14.594l-.281-.313-12-12L30.406 2Zm2 2h20v12h12v30H9Zm22 1.438L39.563 14H31ZM15 22v2h20v-2Zm0 6v2h16v-2Zm0 6v2h20v-2Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="#7A8291" viewBox="0 0 50 50"><path d="M7 2v46h36V14.594l-.281-.313-12-12L30.406 2Zm2 2h20v12h12v30H9Zm22 1.438L39.563 14H31ZM15 22v2h20v-2Zm0 6v2h16v-2Zm0 6v2h20v-2Z"/></svg>

Before

Width:  |  Height:  |  Size: 242 B

After

Width:  |  Height:  |  Size: 245 B

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M9 6h10v12H9a4 4 0 0 1-4-4v-4a4 4 0 0 1 4-4zm1.2 4.6h8.8v.9h-8.8zm0 3.6h8.8v.9h-8.8z" fill="#7A8291" fill-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@@ -0,0 +1,5 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 5.5h13a1.5 1.5 0 0 1 0 3h-13a1.5 1.5 0 0 1 0-3z" fill="#00B173" />
<path d="M4 10.5h10a1.5 1.5 0 0 1 0 3H4a1.5 1.5 0 0 1 0-3z" fill="#00B173" />
<path d="M6.5 15.5h13a1.5 1.5 0 0 1 0 3h-13a1.5 1.5 0 0 1 0-3z" fill="#00B173" />
</svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 115.28 122.88"><path d="M25.38 57h64.88V37.34H69.59c-2.17 0-5.19-1.17-6.62-2.6s-2.3-4.01-2.3-6.17V7.64H8.15c-.18 0-.32.09-.41.18-.15.1-.19.23-.19.42v106.45c0 .14.09.32.18.41.09.14.28.18.41.18h81.51c.18 0 .17-.09.27-.18.14-.09.33-.28.33-.41v-11.16H25.38c-4.14 0-7.56-3.4-7.56-7.56V64.55c0-4.15 3.4-7.55 7.56-7.55m4.11 11.38h7.43v18.15h11.63v5.92H29.49zm20.4 12.05c0-3.93 1.09-6.99 3.28-9.17 2.19-2.19 5.24-3.28 9.15-3.28 4.01 0 7.09 1.08 9.26 3.22 2.17 2.15 3.25 5.16 3.25 9.04 0 2.81-.47 5.11-1.42 6.91q-1.425 2.7-4.11 4.2t-6.69 1.5c-2.71 0-4.96-.43-6.74-1.29-1.78-.87-3.22-2.23-4.32-4.11-1.11-1.87-1.66-4.21-1.66-7.02m7.42.01c0 2.43.45 4.17 1.36 5.23s2.14 1.59 3.7 1.59c1.6 0 2.84-.52 3.71-1.56.88-1.04 1.32-2.9 1.32-5.6 0-2.26-.46-3.92-1.37-4.96-.92-1.05-2.16-1.57-3.73-1.57-1.5 0-2.71.53-3.62 1.59-.91 1.08-1.37 2.83-1.37 5.28m33.11 3.3v-5.01h11.49v10.23c-2.2 1.5-4.15 2.53-5.83 3.07-1.69.54-3.7.81-6.02.81-2.86 0-5.19-.49-6.99-1.46s-3.19-2.42-4.18-4.35c-.99-1.92-1.48-4.13-1.48-6.63 0-2.63.54-4.91 1.62-6.85s2.67-3.41 4.76-4.42c1.63-.78 3.83-1.17 6.58-1.17 2.66 0 4.64.24 5.96.72s2.41 1.23 3.28 2.24 1.52 2.3 1.96 3.85l-7.16 1.29c-.3-.91-.8-1.61-1.5-2.09-.71-.49-1.6-.73-2.7-.73-1.62 0-2.92.57-3.89 1.7s-1.45 2.92-1.45 5.37c0 2.6.49 4.46 1.47 5.57.97 1.11 2.34 1.68 4.09 1.68q1.245 0 2.37-.36c.75-.24 1.61-.65 2.59-1.22v-2.25h-4.97zM97.79 57h9.93c4.16 0 7.56 3.41 7.56 7.56v31.42c0 4.15-3.41 7.56-7.56 7.56h-9.93v13.55c0 1.61-.65 3.04-1.7 4.1a5.74 5.74 0 0 1-4.1 1.7H5.81a5.74 5.74 0 0 1-4.1-1.7 5.74 5.74 0 0 1-1.7-4.1V5.85c0-1.61.65-3.04 1.7-4.1a5.8 5.8 0 0 1 4.1-1.7h58.72c.13-.05.27-.05.41-.05.64 0 1.29.28 1.75.69h.09c.09.05.14.09.23.18L97 31.23c.51.51.88 1.2.88 1.98 0 .23-.05.41-.09.65zM67.52 27.97V8.94l21.43 21.7H70.19c-.74 0-1.38-.32-1.89-.78-.46-.46-.78-1.15-.78-1.89" style="fill:#D1D5DB;fill-rule:evenodd;clip-rule:evenodd"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 115.28 122.88"><path d="M25.38 57h64.88V37.34H69.59c-2.17 0-5.19-1.17-6.62-2.6s-2.3-4.01-2.3-6.17V7.64H8.15c-.18 0-.32.09-.41.18-.15.1-.19.23-.19.42v106.45c0 .14.09.32.18.41.09.14.28.18.41.18h81.51c.18 0 .17-.09.27-.18.14-.09.33-.28.33-.41v-11.16H25.38c-4.14 0-7.56-3.4-7.56-7.56V64.55c0-4.15 3.4-7.55 7.56-7.55m4.11 11.38h7.43v18.15h11.63v5.92H29.49zm20.4 12.05c0-3.93 1.09-6.99 3.28-9.17 2.19-2.19 5.24-3.28 9.15-3.28 4.01 0 7.09 1.08 9.26 3.22 2.17 2.15 3.25 5.16 3.25 9.04 0 2.81-.47 5.11-1.42 6.91q-1.425 2.7-4.11 4.2t-6.69 1.5c-2.71 0-4.96-.43-6.74-1.29-1.78-.87-3.22-2.23-4.32-4.11-1.11-1.87-1.66-4.21-1.66-7.02m7.42.01c0 2.43.45 4.17 1.36 5.23s2.14 1.59 3.7 1.59c1.6 0 2.84-.52 3.71-1.56.88-1.04 1.32-2.9 1.32-5.6 0-2.26-.46-3.92-1.37-4.96-.92-1.05-2.16-1.57-3.73-1.57-1.5 0-2.71.53-3.62 1.59-.91 1.08-1.37 2.83-1.37 5.28m33.11 3.3v-5.01h11.49v10.23c-2.2 1.5-4.15 2.53-5.83 3.07-1.69.54-3.7.81-6.02.81-2.86 0-5.19-.49-6.99-1.46s-3.19-2.42-4.18-4.35c-.99-1.92-1.48-4.13-1.48-6.63 0-2.63.54-4.91 1.62-6.85s2.67-3.41 4.76-4.42c1.63-.78 3.83-1.17 6.58-1.17 2.66 0 4.64.24 5.96.72s2.41 1.23 3.28 2.24 1.52 2.3 1.96 3.85l-7.16 1.29c-.3-.91-.8-1.61-1.5-2.09-.71-.49-1.6-.73-2.7-.73-1.62 0-2.92.57-3.89 1.7s-1.45 2.92-1.45 5.37c0 2.6.49 4.46 1.47 5.57.97 1.11 2.34 1.68 4.09 1.68q1.245 0 2.37-.36c.75-.24 1.61-.65 2.59-1.22v-2.25h-4.97zM97.79 57h9.93c4.16 0 7.56 3.41 7.56 7.56v31.42c0 4.15-3.41 7.56-7.56 7.56h-9.93v13.55c0 1.61-.65 3.04-1.7 4.1a5.74 5.74 0 0 1-4.1 1.7H5.81a5.74 5.74 0 0 1-4.1-1.7 5.74 5.74 0 0 1-1.7-4.1V5.85c0-1.61.65-3.04 1.7-4.1a5.8 5.8 0 0 1 4.1-1.7h58.72c.13-.05.27-.05.41-.05.64 0 1.29.28 1.75.69h.09c.09.05.14.09.23.18L97 31.23c.51.51.88 1.2.88 1.98 0 .23-.05.41-.09.65zM67.52 27.97V8.94l21.43 21.7H70.19c-.74 0-1.38-.32-1.89-.78-.46-.46-.78-1.15-.78-1.89" style="fill:#7A8291;fill-rule:evenodd;clip-rule:evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Haystack</title><path d="M2.008 0C.9 0 0 .9 0 2.008v19.984C0 23.1.9 24 2.008 24h19.984C23.1 24 24 23.1 24 21.992V2.008C24 .9 23.1 0 21.992 0Zm9.963 3.84c3.43 0 6.21 2.763 6.21 6.17v6.488a.27.27 0 0 1-.27.268 2.423 2.423 0 0 1-2.43-2.414V10.01c0-1.927-1.572-3.488-3.51-3.488S8.547 8.085 8.547 10.01v1.608a.263.263 0 0 0 .259.268h1.54a.27.27 0 0 0 .275-.263V9.945c0-.74.604-1.341 1.35-1.341s1.35.6 1.35 1.341V20.03a.275.275 0 0 1-.28.268 2.41 2.41 0 0 1-2.42-2.404v-3.23a.275.275 0 0 0-.276-.269H8.811a.264.264 0 0 0-.264.263v1.08c0 1.333-1.175 2.414-2.517 2.414a.27.27 0 0 1-.27-.268v-7.872c0-3.408 2.78-6.171 6.21-6.171"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="#0E9A89" viewBox="0 0 24 24"><title>Haystack</title><path d="M2.008 0C.9 0 0 .9 0 2.008v19.984C0 23.1.9 24 2.008 24h19.984C23.1 24 24 23.1 24 21.992V2.008C24 .9 23.1 0 21.992 0Zm9.963 3.84c3.43 0 6.21 2.763 6.21 6.17v6.488a.27.27 0 0 1-.27.268 2.423 2.423 0 0 1-2.43-2.414V10.01c0-1.927-1.572-3.488-3.51-3.488S8.547 8.085 8.547 10.01v1.608a.263.263 0 0 0 .259.268h1.54a.27.27 0 0 0 .275-.263V9.945c0-.74.604-1.341 1.35-1.341s1.35.6 1.35 1.341V20.03a.275.275 0 0 1-.28.268 2.41 2.41 0 0 1-2.42-2.404v-3.23a.275.275 0 0 0-.276-.269H8.811a.264.264 0 0 0-.264.263v1.08c0 1.333-1.175 2.414-2.517 2.414a.27.27 0 0 1-.27-.268v-7.872c0-3.408 2.78-6.171 6.21-6.171"/></svg>

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 710 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="154" height="250" preserveAspectRatio="xMidYMid" viewBox="0 0 256 416"><path d="M201.816 230.216c-16.186 0-30.697 7.171-40.634 18.461l-25.463-18.026c2.703-7.442 4.255-15.433 4.255-23.797 0-8.219-1.498-16.076-4.112-23.408l25.406-17.835c9.936 11.233 24.409 18.365 40.548 18.365 29.875 0 54.184-24.305 54.184-54.184s-24.309-54.184-54.184-54.184-54.184 24.305-54.184 54.184c0 5.348.808 10.505 2.258 15.389l-25.423 17.844c-10.62-13.175-25.911-22.374-43.333-25.182v-30.64c24.544-5.155 43.037-26.962 43.037-53.019C124.171 24.305 99.862 0 69.987 0S15.803 24.305 15.803 54.184c0 25.708 18.014 47.246 42.067 52.769v31.038C25.044 143.753 0 172.401 0 206.854c0 34.621 25.292 63.374 58.355 68.94v32.774c-24.299 5.341-42.552 27.011-42.552 52.894 0 29.879 24.309 54.184 54.184 54.184s54.184-24.305 54.184-54.184c0-25.883-18.253-47.553-42.552-52.894v-32.775a69.97 69.97 0 0 0 42.6-24.776l25.633 18.143c-1.423 4.84-2.22 9.946-2.22 15.24 0 29.879 24.309 54.184 54.184 54.184S256 314.279 256 284.4s-24.309-54.184-54.184-54.184m0-126.695c14.487 0 26.27 11.788 26.27 26.271s-11.783 26.27-26.27 26.27-26.27-11.787-26.27-26.27 11.783-26.271 26.27-26.271m-158.1-49.337c0-14.483 11.784-26.27 26.271-26.27s26.27 11.787 26.27 26.27-11.783 26.27-26.27 26.27-26.271-11.787-26.271-26.27m52.541 307.278c0 14.483-11.783 26.27-26.27 26.27s-26.271-11.787-26.271-26.27 11.784-26.27 26.271-26.27 26.27 11.787 26.27 26.27m-26.272-117.97c-20.205 0-36.642-16.434-36.642-36.638 0-20.205 16.437-36.642 36.642-36.642 20.204 0 36.641 16.437 36.641 36.642 0 20.204-16.437 36.638-36.641 36.638m131.831 67.179c-14.487 0-26.27-11.788-26.27-26.271s11.783-26.27 26.27-26.27 26.27 11.787 26.27 26.27-11.783 26.271-26.27 26.271" style="fill:#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="154" height="250" preserveAspectRatio="xMidYMid" viewBox="0 0 256 416"><path d="M201.816 230.216c-16.186 0-30.697 7.171-40.634 18.461l-25.463-18.026c2.703-7.442 4.255-15.433 4.255-23.797 0-8.219-1.498-16.076-4.112-23.408l25.406-17.835c9.936 11.233 24.409 18.365 40.548 18.365 29.875 0 54.184-24.305 54.184-54.184s-24.309-54.184-54.184-54.184-54.184 24.305-54.184 54.184c0 5.348.808 10.505 2.258 15.389l-25.423 17.844c-10.62-13.175-25.911-22.374-43.333-25.182v-30.64c24.544-5.155 43.037-26.962 43.037-53.019C124.171 24.305 99.862 0 69.987 0S15.803 24.305 15.803 54.184c0 25.708 18.014 47.246 42.067 52.769v31.038C25.044 143.753 0 172.401 0 206.854c0 34.621 25.292 63.374 58.355 68.94v32.774c-24.299 5.341-42.552 27.011-42.552 52.894 0 29.879 24.309 54.184 54.184 54.184s54.184-24.305 54.184-54.184c0-25.883-18.253-47.553-42.552-52.894v-32.775a69.97 69.97 0 0 0 42.6-24.776l25.633 18.143c-1.423 4.84-2.22 9.946-2.22 15.24 0 29.879 24.309 54.184 54.184 54.184S256 314.279 256 284.4s-24.309-54.184-54.184-54.184m0-126.695c14.487 0 26.27 11.788 26.27 26.271s-11.783 26.27-26.27 26.27-26.27-11.787-26.27-26.27 11.783-26.271 26.27-26.271m-158.1-49.337c0-14.483 11.784-26.27 26.271-26.27s26.27 11.787 26.27 26.27-11.783 26.27-26.27 26.27-26.271-11.787-26.271-26.27m52.541 307.278c0 14.483-11.783 26.27-26.27 26.27s-26.271-11.787-26.271-26.27 11.784-26.27 26.271-26.27 26.27 11.787 26.27 26.27m-26.272-117.97c-20.205 0-36.642-16.434-36.642-36.638 0-20.205 16.437-36.642 36.642-36.642 20.204 0 36.641 16.437 36.641 36.642 0 20.204-16.437 36.638-36.641 36.638m131.831 67.179c-14.487 0-26.27-11.788-26.27-26.271s11.783-26.27 26.27-26.27 26.27 11.787 26.27 26.27-11.783 26.271-26.27 26.271" style="fill:#7A8291"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,5 @@
<svg role="img" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(2.1186441,37.661017)">
<path d="m 237.70492,0 c 76.95771,0 139.34426,62.386551 139.34426,139.34426 v 0 l -1.8e-4,197.00774 65.57395,-1.5e-4 c 31.68847,0 57.37705,25.68858 57.37705,57.37705 v 0 V 443 h -7.54607 c -18.22527,0 -33.09496,-14.59276 -33.43754,-32.81482 v 0 -8.22465 l -0.003,-0.40664 c -0.21715,-13.39321 -11.14191,-24.18352 -24.58687,-24.18352 v 0 H 106.55738 C 47.707363,377.37037 0,329.66301 0,270.81299 v 0 -131.46873 C 0,62.386551 62.386551,0 139.34426,0 v 0 z m -8.19672,41.018518 h -81.96722 l -1.76212,0.01428 C 87.741593,41.97378 40.983607,89.314381 40.983607,147.5759 v 0 123.20218 l 0.0088,1.08438 c 0.579068,35.71525 29.711746,64.48939 65.564993,64.48939 v 0 h 163.9344 c 36.2154,0 65.57377,-29.35838 65.57377,-65.57377 v 0 -123.20218 l -0.0143,-1.76213 C 335.11031,87.776505 287.76971,41.018518 229.5082,41.018518 Z m -81.63935,65.629632 c 9.34426,0 16.72131,7.62944 16.72131,17.22778 v 0 49.96055 l 56.80328,-58.57444 c 7.37705,-6.89111 17.45902,-7.38334 24.59017,-0.73834 v 0 0.24611 c 6.88524,6.39889 6.39344,16.98167 -1.22951,23.87278 v 0 l -42.04918,43.31556 48.93442,69.89555 c 5.90164,8.61389 4.18033,19.44278 -3.68852,24.61111 -8.60656,4.92223 -18.44262,2.21501 -24.34426,-6.39888 v 0 l -44.0164,-64.48112 -15,15.75112 v 40.36222 c 0,9.84444 -7.37705,17.22778 -16.72131,17.22778 -9.34426,0 -16.72131,-7.38334 -16.72131,-17.22778 v 0 -137.82222 c 0,-9.59834 7.37705,-17.22778 16.72131,-17.22778 z" fill="#5C62B0" fill-rule="evenodd" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LangChain</title><path d="M8.373 14.502c.013-.06.024-.118.038-.17l.061.145c.115.28.229.557.506.714-.012.254-.334.357-.552.326-.048-.114-.115-.228-.255-.164-.143.056-.3-.01-.266-.185.333-.012.407-.371.468-.666zM18.385 9.245c-.318 0-.616.122-.839.342l-.902.887c-.243.24-.368.572-.343.913l.006.056c.032.262.149.498.337.682.13.128.273.21.447.266a.866.866 0 01-.247.777l-.056.055a2.022 2.022 0 01-1.355-1.555l-.01-.057-.046.037c-.03.024-.06.05-.088.078l-.902.887a1.156 1.156 0 000 1.65c.231.228.535.342.84.342.304 0 .607-.114.838-.341l.902-.888a1.156 1.156 0 00-.436-1.921.953.953 0 01.276-.842 2.062 2.062 0 011.371 1.57l.01.057.047-.037c.03-.024.06-.05.088-.078l.902-.888a1.155 1.155 0 000-1.65 1.188 1.188 0 00-.84-.342z" fill="#1C3C3C"></path><path clip-rule="evenodd" d="M17.901 6H6.1C2.736 6 0 8.692 0 12s2.736 6 6.099 6H17.9C21.264 18 24 15.308 24 12s-2.736-6-6.099-6zm-5.821 9.407c-.195.04-.414.047-.562-.106-.045.1-.136.077-.221.056a.797.797 0 00-.061-.014c-.01.025-.017.048-.026.073-.329.021-.575-.309-.732-.558a4.991 4.991 0 00-.473-.21c-.172-.07-.345-.14-.509-.23a2.218 2.218 0 00-.004.173c-.002.244-.004.503-.227.651-.007.295.236.292.476.29.207-.003.41-.005.447.184a.485.485 0 01-.05.003c-.046 0-.092 0-.127.034-.117.111-.242.063-.372.013-.12-.046-.243-.094-.367-.02a2.318 2.318 0 00-.262.154.97.97 0 01-.548.194c-.024-.036-.014-.059.006-.08a.562.562 0 00.043-.056c.019-.028.035-.057.051-.084.054-.095.103-.18.242-.22-.185-.029-.344.055-.5.137l-.004.002a4.21 4.21 0 01-.065.034c-.097.04-.154.009-.212-.023-.082-.045-.168-.092-.376.04-.04-.032-.02-.061.002-.086.091-.109.21-.125.345-.119-.351-.193-.604-.056-.81.055-.182.098-.327.176-.471-.012-.065.017-.102.063-.138.108-.015.02-.03.038-.047.055-.035-.039-.027-.083-.018-.128l.005-.026a.242.242 0 00.003-.03l-.027-.01c-.053-.022-.105-.044-.09-.124-.117-.04-.2.03-.286.094-.054-.041-.01-.095.032-.145a.279.279 0 00.045-.065c.038-.065.103-.067.166-.069.054-.001.108-.003.145-.042.133-.075.297-.036.462.003.121.028.242.057.354.042.203.025.454-.18.352-.385-.186-.233-.184-.528-.183-.813v-.143c-.016-.108-.172-.233-.328-.358-.12-.095-.24-.191-.298-.28-.16-.177-.285-.382-.409-.585l-.015-.024c-.212-.404-.297-.86-.382-1.315-.103-.546-.205-1.09-.526-1.54-.266.144-.612.075-.841-.118-.12.107-.13.247-.138.396l-.001.014c-.297-.292-.26-.844-.023-1.17.097-.128.213-.233.342-.326.03-.021.04-.042.039-.074.235-1.04 1.836-.839 2.342-.103.167.206.281.442.395.678.137.283.273.566.5.795.22.237.452.463.684.689.359.35.718.699 1.032 1.089.49.587.839 1.276 1.144 1.97.05.092.08.193.11.293.044.15.089.299.2.417.026.035.084.088.149.148.156.143.357.328.289.409.009.019.027.04.05.06.032.028.074.058.116.088.122.087.25.178.16.25zm7.778-3.545l-.902.887c-.24.237-.537.413-.859.51l-.017.005-.006.015A2.021 2.021 0 0117.6 14l-.902.888c-.393.387-.916.6-1.474.6-.557 0-1.08-.213-1.474-.6a2.03 2.03 0 010-2.9l.902-.888c.242-.238.531-.409.859-.508l.016-.004.006-.016c.105-.272.265-.516.475-.724l.902-.887c.393-.387.917-.6 1.474-.6.558 0 1.08.213 1.474.6.394.387.61.902.61 1.45 0 .549-.216 1.064-.61 1.45v.001z" fill="#1C3C3C" fill-rule="evenodd"></path></svg>
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LangChain</title><path d="M8.373 14.502c.013-.06.024-.118.038-.17l.061.145c.115.28.229.557.506.714-.012.254-.334.357-.552.326-.048-.114-.115-.228-.255-.164-.143.056-.3-.01-.266-.185.333-.012.407-.371.468-.666zM18.385 9.245c-.318 0-.616.122-.839.342l-.902.887c-.243.24-.368.572-.343.913l.006.056c.032.262.149.498.337.682.13.128.273.21.447.266a.866.866 0 01-.247.777l-.056.055a2.022 2.022 0 01-1.355-1.555l-.01-.057-.046.037c-.03.024-.06.05-.088.078l-.902.887a1.156 1.156 0 000 1.65c.231.228.535.342.84.342.304 0 .607-.114.838-.341l.902-.888a1.156 1.156 0 00-.436-1.921.953.953 0 01.276-.842 2.062 2.062 0 011.371 1.57l.01.057.047-.037c.03-.024.06-.05.088-.078l.902-.888a1.155 1.155 0 000-1.65 1.188 1.188 0 00-.84-.342z" fill="#3B8686"></path><path clip-rule="evenodd" d="M17.901 6H6.1C2.736 6 0 8.692 0 12s2.736 6 6.099 6H17.9C21.264 18 24 15.308 24 12s-2.736-6-6.099-6zm-5.821 9.407c-.195.04-.414.047-.562-.106-.045.1-.136.077-.221.056a.797.797 0 00-.061-.014c-.01.025-.017.048-.026.073-.329.021-.575-.309-.732-.558a4.991 4.991 0 00-.473-.21c-.172-.07-.345-.14-.509-.23a2.218 2.218 0 00-.004.173c-.002.244-.004.503-.227.651-.007.295.236.292.476.29.207-.003.41-.005.447.184a.485.485 0 01-.05.003c-.046 0-.092 0-.127.034-.117.111-.242.063-.372.013-.12-.046-.243-.094-.367-.02a2.318 2.318 0 00-.262.154.97.97 0 01-.548.194c-.024-.036-.014-.059.006-.08a.562.562 0 00.043-.056c.019-.028.035-.057.051-.084.054-.095.103-.18.242-.22-.185-.029-.344.055-.5.137l-.004.002a4.21 4.21 0 01-.065.034c-.097.04-.154.009-.212-.023-.082-.045-.168-.092-.376.04-.04-.032-.02-.061.002-.086.091-.109.21-.125.345-.119-.351-.193-.604-.056-.81.055-.182.098-.327.176-.471-.012-.065.017-.102.063-.138.108-.015.02-.03.038-.047.055-.035-.039-.027-.083-.018-.128l.005-.026a.242.242 0 00.003-.03l-.027-.01c-.053-.022-.105-.044-.09-.124-.117-.04-.2.03-.286.094-.054-.041-.01-.095.032-.145a.279.279 0 00.045-.065c.038-.065.103-.067.166-.069.054-.001.108-.003.145-.042.133-.075.297-.036.462.003.121.028.242.057.354.042.203.025.454-.18.352-.385-.186-.233-.184-.528-.183-.813v-.143c-.016-.108-.172-.233-.328-.358-.12-.095-.24-.191-.298-.28-.16-.177-.285-.382-.409-.585l-.015-.024c-.212-.404-.297-.86-.382-1.315-.103-.546-.205-1.09-.526-1.54-.266.144-.612.075-.841-.118-.12.107-.13.247-.138.396l-.001.014c-.297-.292-.26-.844-.023-1.17.097-.128.213-.233.342-.326.03-.021.04-.042.039-.074.235-1.04 1.836-.839 2.342-.103.167.206.281.442.395.678.137.283.273.566.5.795.22.237.452.463.684.689.359.35.718.699 1.032 1.089.49.587.839 1.276 1.144 1.97.05.092.08.193.11.293.044.15.089.299.2.417.026.035.084.088.149.148.156.143.357.328.289.409.009.019.027.04.05.06.032.028.074.058.116.088.122.087.25.178.16.25zm7.778-3.545l-.902.887c-.24.237-.537.413-.859.51l-.017.005-.006.015A2.021 2.021 0 0117.6 14l-.902.888c-.393.387-.916.6-1.474.6-.557 0-1.08-.213-1.474-.6a2.03 2.03 0 010-2.9l.902-.888c.242-.238.531-.409.859-.508l.016-.004.006-.016c.105-.272.265-.516.475-.724l.902-.887c.393-.387.917-.6 1.474-.6.558 0 1.08.213 1.474.6.394.387.61.902.61 1.45 0 .549-.216 1.064-.61 1.45v.001z" fill="#3B8686" fill-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z" fill="#0467DF" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M4.89 5.57 0 14.002l2.521 4.4h5.05l4.396-7.718 4.512 7.709 4.996.037L24 14.057l-4.857-8.452-5.073-.015-2.076 3.598L9.94 5.57Zm.837.729h3.787l1.845 3.252H7.572Zm9.189.021 3.803.012 4.228 7.355-3.736-.027zm-9.82.346L6.94 9.914l-4.209 7.389-1.892-3.3Zm9.187.014 4.297 7.343-1.892 3.282-4.3-7.344zm-6.713 3.6h3.79l-4.212 7.394H3.361Zm11.64 4.109 3.74.027-1.893 3.281-3.74-.027z" fill="#3E9C2A" />
</svg>

After

Width:  |  Height:  |  Size: 482 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Ollama</title><path d="M16.361 10.26a.9.9 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.75.75 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.1 1.1 0 0 0-.466 0m-9.203.005c-.305.096-.533.32-.65.639a1.2 1.2 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.9.9 0 0 0-.565-.472 1 1 0 0 0-.474.007m4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.4.4 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.4.4 0 0 0-.193.032m.39-2.195c-.373.036-.475.05-.654.086a4.5 4.5 0 0 0-.951.328c-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a7 7 0 0 0-.855-.031zm.645.937a3.3 3.3 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3 3 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042q-.856-.16-1.35-.68c-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002m-5.503-11a1.65 1.65 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208l-.187.152a5.3 5.3 0 0 0-.949 1.02 5.5 5.5 0 0 0-.94 2.339 6.6 6.6 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.4 7.4 0 0 0-.548 1.873 5 5 0 0 0-.071.991c0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.9.9 0 0 0-.194-.25 1.7 1.7 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.8.8 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.14 3.14 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04s.398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2 2 0 0 1-.224.258.9.9 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a1 1 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516a9 9 0 0 0 0-1.258c-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141a1.4 1.4 0 0 0 .108-.152c.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6 6 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.6 6.6 0 0 0-.024-1.358 5.5 5.5 0 0 0-.939-2.339 5.3 5.3 0 0 0-.95-1.02l-.186-.152a.7.7 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a7 7 0 0 0-.097.726c0 .036-.007.066-.015.066a1 1 0 0 1-.149-.078A4.86 4.86 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a1 1 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a7 7 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2 2 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.4 8.4 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013m9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.7 3.7 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="#7A8291" viewBox="0 0 24 24"><title>Ollama</title><path d="M16.361 10.26a.9.9 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.75.75 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.1 1.1 0 0 0-.466 0m-9.203.005c-.305.096-.533.32-.65.639a1.2 1.2 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.9.9 0 0 0-.565-.472 1 1 0 0 0-.474.007m4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.4.4 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.4.4 0 0 0-.193.032m.39-2.195c-.373.036-.475.05-.654.086a4.5 4.5 0 0 0-.951.328c-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a7 7 0 0 0-.855-.031zm.645.937a3.3 3.3 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3 3 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042q-.856-.16-1.35-.68c-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002m-5.503-11a1.65 1.65 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208l-.187.152a5.3 5.3 0 0 0-.949 1.02 5.5 5.5 0 0 0-.94 2.339 6.6 6.6 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.4 7.4 0 0 0-.548 1.873 5 5 0 0 0-.071.991c0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.9.9 0 0 0-.194-.25 1.7 1.7 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.8.8 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.14 3.14 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04s.398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2 2 0 0 1-.224.258.9.9 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a1 1 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516a9 9 0 0 0 0-1.258c-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141a1.4 1.4 0 0 0 .108-.152c.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6 6 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.6 6.6 0 0 0-.024-1.358 5.5 5.5 0 0 0-.939-2.339 5.3 5.3 0 0 0-.95-1.02l-.186-.152a.7.7 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a7 7 0 0 0-.097.726c0 .036-.007.066-.015.066a1 1 0 0 1-.149-.078A4.86 4.86 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a1 1 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a7 7 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2 2 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.4 8.4 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013m9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.7 3.7 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012"/></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#fff" d="M44.559 19.646a11.957 11.957 0 0 0-1.028-9.822 12.094 12.094 0 0 0-13.026-5.802A11.962 11.962 0 0 0 21.485 0 12.097 12.097 0 0 0 9.95 8.373a11.964 11.964 0 0 0-7.997 5.8A12.097 12.097 0 0 0 3.44 28.356a11.957 11.957 0 0 0 1.028 9.822 12.094 12.094 0 0 0 13.026 5.802 11.953 11.953 0 0 0 9.02 4.02 12.096 12.096 0 0 0 11.54-8.379 11.964 11.964 0 0 0 7.997-5.8 12.099 12.099 0 0 0-1.491-14.177zM26.517 44.863a8.966 8.966 0 0 1-5.759-2.082 6.85 6.85 0 0 0 .284-.16L30.6 37.1c.49-.278.79-.799.786-1.361V22.265l4.04 2.332a.141.141 0 0 1 .078.111v11.16a9.006 9.006 0 0 1-8.987 8.995zM7.191 36.608a8.957 8.957 0 0 1-1.073-6.027c.071.042.195.119.284.17l9.558 5.52a1.556 1.556 0 0 0 1.57 0l11.67-6.738v4.665a.15.15 0 0 1-.057.124l-9.662 5.579a9.006 9.006 0 0 1-12.288-3.293zM4.675 15.744a8.966 8.966 0 0 1 4.682-3.943c0 .082-.005.228-.005.33v11.042a1.555 1.555 0 0 0 .785 1.359l11.669 6.736-4.04 2.333a.143.143 0 0 1-.136.012L7.967 28.03a9.006 9.006 0 0 1-3.293-12.284zm33.19 7.724L26.196 16.73l4.04-2.331a.143.143 0 0 1 .136-.012l9.664 5.579c4.302 2.485 5.776 7.989 3.29 12.29a8.991 8.991 0 0 1-4.68 3.943V24.827a1.553 1.553 0 0 0-.78-1.36zm4.02-6.051c-.07-.044-.195-.119-.283-.17l-9.558-5.52a1.556 1.556 0 0 0-1.57 0l-11.67 6.738V13.8a.15.15 0 0 1 .057-.124l9.662-5.574a8.995 8.995 0 0 1 13.36 9.315zm-25.277 8.315-4.04-2.333a.141.141 0 0 1-.079-.11v-11.16a8.997 8.997 0 0 1 14.753-6.91c-.073.04-.2.11-.283.161L17.4 10.9a1.552 1.552 0 0 0-.786 1.36l-.006 13.469zM18.803 21l5.198-3.002 5.197 3V27l-5.197 3-5.198-3z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#7A8291" d="M44.559 19.646a11.957 11.957 0 0 0-1.028-9.822 12.094 12.094 0 0 0-13.026-5.802A11.962 11.962 0 0 0 21.485 0 12.097 12.097 0 0 0 9.95 8.373a11.964 11.964 0 0 0-7.997 5.8A12.097 12.097 0 0 0 3.44 28.356a11.957 11.957 0 0 0 1.028 9.822 12.094 12.094 0 0 0 13.026 5.802 11.953 11.953 0 0 0 9.02 4.02 12.096 12.096 0 0 0 11.54-8.379 11.964 11.964 0 0 0 7.997-5.8 12.099 12.099 0 0 0-1.491-14.177zM26.517 44.863a8.966 8.966 0 0 1-5.759-2.082 6.85 6.85 0 0 0 .284-.16L30.6 37.1c.49-.278.79-.799.786-1.361V22.265l4.04 2.332a.141.141 0 0 1 .078.111v11.16a9.006 9.006 0 0 1-8.987 8.995zM7.191 36.608a8.957 8.957 0 0 1-1.073-6.027c.071.042.195.119.284.17l9.558 5.52a1.556 1.556 0 0 0 1.57 0l11.67-6.738v4.665a.15.15 0 0 1-.057.124l-9.662 5.579a9.006 9.006 0 0 1-12.288-3.293zM4.675 15.744a8.966 8.966 0 0 1 4.682-3.943c0 .082-.005.228-.005.33v11.042a1.555 1.555 0 0 0 .785 1.359l11.669 6.736-4.04 2.333a.143.143 0 0 1-.136.012L7.967 28.03a9.006 9.006 0 0 1-3.293-12.284zm33.19 7.724L26.196 16.73l4.04-2.331a.143.143 0 0 1 .136-.012l9.664 5.579c4.302 2.485 5.776 7.989 3.29 12.29a8.991 8.991 0 0 1-4.68 3.943V24.827a1.553 1.553 0 0 0-.78-1.36zm4.02-6.051c-.07-.044-.195-.119-.283-.17l-9.558-5.52a1.556 1.556 0 0 0-1.57 0l-11.67 6.738V13.8a.15.15 0 0 1 .057-.124l9.662-5.574a8.995 8.995 0 0 1 13.36 9.315zm-25.277 8.315-4.04-2.333a.141.141 0 0 1-.079-.11v-11.16a8.997 8.997 0 0 1 14.753-6.91c-.073.04-.2.11-.283.161L17.4 10.9a1.552 1.552 0 0 0-.786 1.36l-.006 13.469zM18.803 21l5.198-3.002 5.197 3V27l-5.197 3-5.198-3z"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>OpenRouter</title><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="#7A8291" viewBox="0 0 24 24"><title>OpenRouter</title><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 688 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,10 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="#A5300F" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.4 7.7c0 4.6-4.7 4.9-4.7 8.6" />
<path d="M8 10.7c3.3 0 4.2 2.4 4.2 5.6" />
<circle cx="17.4" cy="4.6" r="3.1" fill="#FADDCD" />
<circle cx="4.9" cy="10.7" r="3.1" fill="#FADDCD" />
<rect x="7.9" y="16.3" width="9.6" height="6.6" rx="1.6" fill="#FADDCD" />
<path d="M11.3 18.6 9.9 19.9l1.4 1.3M14.1 18.6l1.4 1.3-1.4 1.3M13.1 18.3l-1.2 3.3" stroke-width="1.1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1155 1000"><path fill="#fff" d="m577.3 0 577.4 1000H0z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1155 1000"><path fill="#7A8291" d="m577.3 0 577.4 1000H0z"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

After

Width:  |  Height:  |  Size: 131 B

View File

@@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m23.6 0-8.721 4.59L9.829 24h7.41z" fill="#30A2FF" />
<path d="M9.83 24V5.142H.4z" fill="#FDB515" />
</svg>

After

Width:  |  Height:  |  Size: 190 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" fill="#fff" stroke="#fff" viewBox="0 0 24 24"><path d="M20.98 11.802a1 1 0 0 0-.738-.771l-6.86-1.716 2.537-5.921a1 1 0 0 0-.317-1.192.996.996 0 0 0-1.234.024l-11 9a1 1 0 0 0 .39 1.744l6.719 1.681-3.345 5.854A1 1 0 0 0 8 22a1 1 0 0 0 .6-.2l12-9a1 1 0 0 0 .38-.998z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" fill="#7A8291" stroke="#7A8291" viewBox="0 0 24 24"><path d="M20.98 11.802a1 1 0 0 0-.738-.771l-6.86-1.716 2.537-5.921a1 1 0 0 0-.317-1.192.996.996 0 0 0-1.234.024l-11 9a1 1 0 0 0 .39 1.744l6.719 1.681-3.345 5.854A1 1 0 0 0 8 22a1 1 0 0 0 .6-.2l12-9a1 1 0 0 0 .38-.998z"/></svg>

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 343 B

View File

@@ -0,0 +1,69 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import dayjs from 'dayjs';
import { screen, userEvent } from 'storybook/test';
import { withCanvas } from '@/storybook/decorators/withCanvas';
import CustomTimePicker from '../CustomTimePicker';
const minTime = dayjs('2025-01-15T11:00:00Z').valueOf() * 1_000_000;
const maxTime = dayjs('2025-01-15T12:00:00Z').valueOf() * 1_000_000;
function TimePickerFixture(): JSX.Element {
const [open, setOpen] = useState(false);
const [selectedTime, setSelectedTime] = useState('1h');
return (
<CustomTimePicker
isModalTimeSelection
items={[
{ label: 'Last 15 minutes', value: '15m' },
{ label: 'Last 1 hour', value: '1h' },
{ label: 'Last 6 hours', value: '6h' },
{ label: 'Custom', value: 'custom' },
]}
maxTime={maxTime}
minTime={minTime}
newPopover
open={open}
onCustomDateHandler={(): void => undefined}
onError={(): void => undefined}
onSelect={(value): void => setSelectedTime(value)}
onValidCustomDateChange={(): void => undefined}
selectedTime={selectedTime}
selectedValue="15 Jan 2025 11:00:00 - 15 Jan 2025 12:00:00"
setOpen={setOpen}
/>
);
}
const meta = {
title: 'Components/Custom Time Picker',
component: TimePickerFixture,
tags: ['play'],
decorators: [withCanvas({ maxWidth: 400 })],
} satisfies Meta<typeof TimePickerFixture>;
export default meta;
type Story = StoryObj<typeof meta>;
/** Interaction: the time-range menu is open with its relative-range choices. */
export const TimeRangeMenuOpen: Story = {
play: async (): Promise<void> => {
await userEvent.click(await screen.findByRole('textbox'));
await screen.findByText('RELATIVE TIMES');
},
};
/** Interaction: the timezone menu is reached through the real time-range footer. */
export const TimezoneMenuOpen: Story = {
play: async (): Promise<void> => {
await userEvent.click(await screen.findByRole('textbox'));
await userEvent.click(
await screen.findByRole('button', { name: 'Change Timezone' }),
);
await screen.findByPlaceholderText('Search timezones...');
},
};

View File

@@ -0,0 +1,28 @@
/**
* AI-owned. Generated and maintained by the `signoz-page-story` skill.
* Do not hand-edit: regenerate instead.
*/
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { rest } from 'msw';
import { fieldKeysResponse } from '@/storybook/msw/__story_mockdata__/fields';
export const fieldSuggestionsHandlers = [
rest.get('http://localhost/api/v1/fields/keys', (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json(
fieldKeysResponse(['service.name', 'body'], {
signal: TelemetrytypesSignalDTO.logs,
}),
),
),
),
];
export const noFieldSuggestionsHandlers = [
rest.get('http://localhost/api/v1/fields/keys', (_req, res, ctx) =>
res(ctx.status(200), ctx.json(fieldKeysResponse([]))),
),
];

View File

@@ -0,0 +1,102 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { screen, userEvent } from 'storybook/test';
import { DataSource } from 'types/common/queryBuilder';
import FieldsSelector from '../FieldsSelector';
import {
fieldSuggestionsHandlers,
noFieldSuggestionsHandlers,
} from './FieldsSelector.stories.mocks';
const meta = {
title: 'Components/Fields Selector',
component: FieldsSelector,
tags: ['play'],
args: {
allowCustomFields: true,
defaultPosition: { x: 40, y: 40 },
fields: [
{
fieldContext: 'log',
fieldDataType: 'string',
name: 'timestamp',
signal: 'logs',
},
],
height: 560,
isOpen: true,
onClose: (): void => undefined,
onFieldsChange: (): void => undefined,
signal: DataSource.LOGS,
title: 'Edit log columns',
width: 420,
},
parameters: {
msw: {
handlers: fieldSuggestionsHandlers,
},
},
} satisfies Meta<typeof FieldsSelector>;
export default meta;
type Story = StoryObj<typeof meta>;
/** Open: the draggable field editor shows its selected and available columns. */
export const Open: Story = {};
/** Mutation: adding a suggested field exposes the real unsaved-change footer. */
export const UnsavedChanges: Story = {
play: async (): Promise<void> => {
// One Add per suggested field, so the first row's is the one clicked.
const [addField] = await screen.findAllByRole('button', { name: 'Add' });
await userEvent.click(addField);
await screen.findByRole('button', { name: 'Save changes' });
},
};
/** Empty: the suggestion request succeeds with no columns to add. */
export const NoResults: Story = {
parameters: {
msw: {
handlers: noFieldSuggestionsHandlers,
},
},
};
/** Limit: available columns cannot be added once the configured maximum is reached. */
export const MaximumFields: Story = {
args: {
fields: [
{
fieldContext: 'log',
fieldDataType: 'string',
name: 'timestamp',
signal: 'logs',
},
{
fieldContext: 'log',
fieldDataType: 'string',
name: 'severity_text',
signal: 'logs',
},
],
maxFields: 2,
},
};
/** Required: mandatory fields remain present without removal controls. */
export const RequiredFields: Story = {
args: {
fields: [
{
fieldContext: 'resource',
fieldDataType: 'string',
name: 'service.name',
signal: 'logs',
},
],
requiredFields: ['resource:service.name:string'],
},
};

View File

@@ -1,12 +0,0 @@
import { ReactNode } from 'react';
import { CategoryHeadingText } from './styles';
interface ICategoryHeadingProps {
children: ReactNode;
}
function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element {
return <CategoryHeadingText color="muted">{children}</CategoryHeadingText>;
}
export default CategoryHeading;

View File

@@ -1,6 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const CategoryHeadingText = styled(Typography.Text)`
font-size: 0.8rem;
`;

View File

@@ -1,33 +0,0 @@
import { CSSProperties } from 'react';
import { Color } from '@signozhq/design-tokens';
import { TableProps } from 'antd';
export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
return {
paddingTop: 4,
paddingBottom: 6,
paddingRight: 8,
paddingLeft: 8,
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400,
fontSize: '14px',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '18px',
letterSpacing: '-0.07px',
marginBottom: '0px',
minWidth: '10rem',
width: 'auto',
};
}
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
};
export const defaultListViewPanelStyle: CSSProperties = {
maxWidth: '40rem',
};
export const tableScroll: TableProps<Record<string, unknown>>['scroll'] = {
x: true,
};

View File

@@ -1,24 +0,0 @@
import { Table } from 'antd';
// config
import { tableScroll } from './config';
import { LogsTableViewProps } from './types';
import { useTableView } from './useTableView';
function LogsTableView(props: LogsTableViewProps): JSX.Element {
const { dataSource, columns } = useTableView(props);
return (
<Table
size="small"
columns={columns}
dataSource={dataSource}
pagination={false}
rowKey="id"
bordered
scroll={tableScroll}
/>
);
}
export default LogsTableView;

View File

@@ -1,32 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
interface TableBodyContentProps {
linesPerRow: number;
fontSize: FontSize;
isDarkMode?: boolean;
}
export const TableBodyContent = styled.div<TableBodyContentProps>`
margin-bottom: 0;
color: ${(props): string =>
props.isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400};
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: ${(props): number => props.linesPerRow};
line-clamp: ${(props): number => props.linesPerRow};
-webkit-box-orient: vertical;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `font-size:11px; line-height:16px;`
: fontSize === FontSize.MEDIUM
? `font-size:13px; line-height:20px;`
: `font-size:14px; line-height:24px;`}
`;

View File

@@ -1,40 +1,5 @@
import {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { TableColumnType as ColumnType } from 'antd';
export type ColumnTypeRender<T = unknown> = ReturnType<
NonNullable<ColumnType<T>['render']>
>;
export type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
fontSize: FontSize;
onClickExpand?: (log: ILog) => void;
};
export type UseTableViewResult = {
columns: ColumnsType<Record<string, unknown>>;
dataSource: Record<string, string>[];
};
export type UseTableViewProps = {
appendTo?: 'center' | 'end';
onOpenLogsContext?: (log: ILog) => void;
onClickExpand?: (log: ILog) => void;
activeLog?: ILog | null;
activeLogIndex?: number;
activeContextLog?: ILog | null;
isListViewPanel?: boolean;
} & LogsTableViewProps;
export type ActionsColumnProps = {
logId: string;
logs: ILog[];
onOpenLogsContext?: (log: ILog) => void;
};

View File

@@ -1,59 +0,0 @@
.text {
color: var(--l2-foreground);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
&.small {
font-size: 11px;
line-height: 16px;
}
&.medium {
font-size: 13px;
line-height: 20px;
}
&.large {
font-size: 14px;
line-height: 24px;
}
}
.state-indicator {
width: 15px;
.log-state-indicator {
padding: 0px;
}
}
.table-timestamp {
display: flex;
align-items: center;
.timestamp-text {
color: var(--l1-foreground);
margin: 0 !important;
}
}
.paragraph {
margin: 0;
padding: 0px !important;
&.small {
font-size: 11px !important;
line-height: 16px !important;
}
&.medium {
font-size: 13px !important;
line-height: 20px !important;
}
&.large {
font-size: 14px !important;
line-height: 24px !important;
}
}

View File

@@ -1,180 +0,0 @@
import { useMemo } from 'react';
import { TableColumnsType as ColumnsType } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import {
defaultListViewPanelStyle,
defaultTableStyle,
getDefaultCellStyle,
} from './config';
import { TableBodyContent } from './styles';
import {
ColumnTypeRender,
UseTableViewProps,
UseTableViewResult,
} from './types';
import './useTableView.styles.scss';
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const {
logs,
fields,
linesPerRow,
fontSize,
appendTo = 'center',
isListViewPanel,
} = props;
const isDarkMode = useIsDarkMode();
const flattenLogData = useMemo(
() => logs.map((log) => FlatLogData(log)),
[logs],
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const bodyColumnStyle = useMemo(
() => ({
...defaultTableStyle,
...(fields.length > 2 ? { width: 'auto' } : {}),
}),
[fields.length],
);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
.map(({ name }) => ({
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: {
...(isListViewPanel
? defaultListViewPanelStyle
: getDefaultCellStyle(isDarkMode)),
display: '-webkit-box',
WebkitLineClamp: linesPerRow,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
wordBreak: 'break-all',
},
},
children: <p className={cx('paragraph', fontSize)}>{field}</p>,
}),
}));
if (isListViewPanel) {
return [...fieldColumns];
}
return [
{
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
key: 'state-indicator',
accessorKey: 'state-indicator',
id: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
<LogStateIndicator
fontSize={fontSize}
severityText={item.severity_text as string}
severityNumber={item.severity_number as number}
/>
</div>
),
}),
},
...(fields.some((field) => field.name === 'timestamp')
? [
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
accessorKey: 'timestamp',
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? formatTimezoneAdjustedTimestamp(
field,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
)
: formatTimezoneAdjustedTimestamp(
field / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
);
return {
children: (
<div className="table-timestamp">
<p className={cx('timestamp-text text', fontSize)}>{date}</p>
</div>
),
};
},
},
]
: []),
...(appendTo === 'center' ? fieldColumns : []),
...(fields.some((field) => field.name === 'body')
? [
{
title: 'body',
dataIndex: 'body',
key: 'body',
accessorKey: 'body',
id: 'body',
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: bodyColumnStyle,
},
children: (
<TableBodyContent
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(field as string, {
shouldEscapeHtml: true,
}),
}}
fontSize={fontSize}
linesPerRow={linesPerRow}
isDarkMode={isDarkMode}
/>
),
}),
},
]
: []),
...(appendTo === 'end' ? fieldColumns : []),
];
}, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
fontSize,
formatTimezoneAdjustedTimestamp,
bodyColumnStyle,
]);
return { columns, dataSource: flattenLogData };
};

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { Button, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { LogViewMode } from 'container/OptionsMenu/types';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import {
Check,

Some files were not shown because too many files have changed in this diff Show More