Compare commits

..

24 Commits

Author SHA1 Message Date
aks07
4c657fcdce feat(trace-details): show span and error counts in the bottom strip
The page passes the values rather than the strip node fetching them, since the
trace query key includes the selected span and a self-fetching node would issue
a request on every span click. Header counts stay as is.
2026-09-24 11:43:05 +05:30
aks07
231f25c989 feat(bottom-strip): add the left slot mechanism
Pages push a node, the strip knows nothing about pages. Only the owner that
set a value may clear it, so a consumer unmounting late cannot wipe the one
that is showing. The node comes from the page and the strip renders on every
route, so it is wrapped in an error boundary that falls back to the version.
2026-09-24 11:43:05 +05:30
aks07
a9e4f16a9d feat(bottom-strip): add the strip typography primitive 2026-09-24 11:43:05 +05:30
aks07
146013fcee feat(bottom-strip): add Support and drop the floating bubbles
One CTA for both. Pylon users get the widget, trial users without a card get
the add credit card modal, and it hides when neither applies..same as today
where only one of the two bubbles ever shows.

Both floating bubbles go with the flag on. Everything else stays, side nav
entries and the "Facing issues?" buttons are untouched.

The Pylon chat window is lifted off the strip in css. Whether their wrapper is
actually bottom anchored needs checking on a pylon enabled tenant before the
flag goes on for anyone.
2026-09-24 11:43:05 +05:30
aks07
cad67dea5a feat(bottom-strip): add the separator primitive
First of the strip primitives kit. Collapses itself at either edge and when
two end up adjacent, so items can keep hiding by rendering null and the call
sites stay free of visibility plumbing.

Also sets the strip buttons to 12px, the component defaults to 11px.
2026-09-24 11:43:05 +05:30
aks07
88a31b9fd6 refactor(chat-support): derive the support gate in one place
Which support affordance a user gets was worked out in three places with
slightly different wording. The bottom strip needs one CTA covering both the
Pylon bubble and the trial-without-card one, so the gate moves here.

Reads the pylon setting from the boot data rather than `window.pylon`, which is
set in an effect..a memo reading that would run first and never recompute.
2026-09-24 11:43:05 +05:30
aks07
e9a415a85d refactor(chat-support): share one Add Credit Card modal
The same modal was written out three times. This pulls two of them into one
component..the /support page has a third with different classes on the submit
button, left alone for now.
2026-09-24 11:43:05 +05:30
aks07
d8be6fe0f2 feat(bottom-strip): add Ask Noz to the strip
Opens the drawer, same as the header entry. Carries that entry's pending badge
so the strip shows when Noz is blocked on the user.

Nothing is hidden. Design wants the header button kept for now, so this is an
extra entry point rather than a replacement.
2026-09-24 11:43:05 +05:30
aks07
7b56ebf134 fix(layout): size pages from the layout instead of the viewport
Pages that hardcoded `100vh` minus a guess at what sits above them came out
taller than the pane they live in, which showed up as scroll that should not
be there. They now take what the layout gives them.

Most of the `100vh` in the app turned out to be harmless, either absorbed by
flex-shrink or by a pane that scrolls anyway, and is left alone. Only the ones
with a real symptom are changed here.

Alert rules and triggered alerts also needed the AlertList tabs chain to hand
height down, since that page uses antd Tabs directly rather than RouteTab.
2026-09-24 11:43:05 +05:30
aks07
ef3e09c0b1 feat(bottom-strip): add the layout shell behind a feature flag
Mounts a 24px strip at the bottom of the app under the same gate as the side
nav, behind the SAVED_VIEW_ENABLED localStorage flag shared with saved views.
Shows the build version on the left for now; the per-page left slot and the
right side actions come in later tickets.

To give the strip a stable box, `.app-content` becomes a column flex and
`LayoutContent` takes the height left over instead of `height: 100%`. Fixed
bottom elements read `--bottom-strip-height`, which only exists while the strip
is mounted, so with the flag off every offset falls back to where it is today.

Hides nothing. Each later ticket hides the piece it replaces.
2026-09-24 06:13:04 +00:00
Aditya Singh
720810d424 fix(quick-filters): filter sidebar scrolls the whole page (#12915)
#### Description

- Quick filters sidebar scrolls the whole page instead of scrolling
itself, so the top nav, module tabs and the table scroll away with it.
Happens on traces, llm observability, api monitoring, exceptions and
meter.. logs is the only page behaving today.
- Cause is antd Tabs.. it never passes height down to the tab pane, and
every module page wrapped `RouteTab` in a plain div, so the page inside
was never bounded. Pages that wanted their own scroll each kept a
private copy of the same `.ant-tabs` override, traces and meter never
had one.
- `RouteTab` now owns the height chain and scrolls each pane's content,
so the tab bar stays put on every tabbed page. Module pages pass their
class to `RouteTab` instead of wrapping it, and the six copied overrides
are gone.
- New `QuickFiltersLayout` gives the explorer pages a bounded two pane
layout.. 280px sidebar that scrolls itself, content that scrolls itself.
Traces, llm, api monitoring, exceptions and meter are on it now.
- Logs and infra are unchanged here, both move to the shared layout in
follow ups.
- Also drops the per page viewport heights on the quick filter settings
drawer.. with the pane bounded, `height: 100%` is enough, and the
save/discard footer stays reachable with the banners on.

#### Issues closed by this PR

Closes https://github.com/SigNoz/engineering-pod/issues/6088
Closes https://github.com/SigNoz/engineering-pod/issues/6104
Part of https://github.com/SigNoz/engineering-pod/issues/5946


#### Screenshots/ recordings



Banner fix [BEFORE]


https://github.com/user-attachments/assets/44bc98f8-7ce9-45a7-9c45-e86648c40f69

Banner fix [AFTER]



https://github.com/user-attachments/assets/c1140a2a-f00e-4685-87e4-4a0f5d72623a



Quick Filter Whole Page scroll Fix
[BEFORE]



https://github.com/user-attachments/assets/07bd42a0-db80-4d14-bf8a-bcea01fb70b1




[AFTER]




https://github.com/user-attachments/assets/7bd14951-2595-43cd-8eab-dcb57cdce011





#### Additional Information

- The `RouteTab` change touches every tabbed page, not just the quick
filter ones. Checked traces, logs, exceptions, api monitoring, meter,
infra hosts, metrics summary, funnels, saved views, pipelines, mq, logs
settings, settings (org + members) and alert details, with the trial
banner on and off. llm observability is feature gated on my instance so
it is not checked in the browser.
- Behaviour change to call out: top nav and tab bar are now fixed on all
tabbed pages. Pages that mount `RouteTab` inside a block wrapper (alert
details, the exceptions inner tabs) are unaffected, the scroller is
inert there.
- Sidebar is 280px everywhere now, was a 260/280 mix.
- Pane content that needs a bounded box should size with `height:
100%`.. `flex: 1` does nothing inside the scroller viewport (documented
on `RouteTab`).
- cc. @H4ad
2026-09-24 06:01:23 +00:00
Ashwin Bhatkal
7424885a14 chore(codeowners): add alert code ownership to pulse-frontend (#12977)
#### Description

Paths that belong to alerts and notification channels added to
CODEOWNERS

- `container/FormAlertChannels/`
- `hooks/notificationChannels/`
- `container/RoutingPolicies/`
- `components/AlertBreadcrumb/`
- `container/EditRules/`
- `components/AlertDetailsFilters/`
- `components/Alerts/`
- `hooks/routingPolicies/`
- `types/api/alerts/`
- `providers/Alert.tsx`
- `constants/alerts.ts`

All go to `@SigNoz/pulse-frontend`, matching the rest of those blocks.
2026-09-24 04:29:31 +00:00
Abhi kumar
2c09fedde1 fix(dashboard): panel UX fixes — bar gap, tooltip date, palette, legend search (#12959)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

Four independent panel-UX fixes from the dashboards epic, one commit
each.

- **Bar gap.** The gap between bars was wider than half a bar. The bar
width factor goes 0.6 → 0.85, leaving just enough to separate them.
Shared by bar and histogram panels.
- **Tooltip date.** A tooltip on a point from today now shows only the
time; the date still appears for any other day.
- **Red in the palette.** Roughly one series in six was being coloured
red — 18 of 117 entries in `chartcolors`, 15 of 74 in `lightModeColor` —
which spends the one colour that should mean "something is wrong". Each
red entry is rotated onto a free hue with its original lightness
preserved and saturation clamped, and its key renamed to match. Entries
are replaced, never removed: `generateColor` indexes by `hash %
Object.keys(...).length`, so changing the count would recolour every
existing chart. Pinks and magentas are left alone.
- **Bottom legend search.** A bottom legend that overflows the rows the
panel reserves for it now gets a search box and a "Showing N of M
series" readout, on a single row above the series. Whether that row
exists is the chart layout's call, since it is the layout that reserves
the height — a row nobody reserved would eat a row of series. One
`LegendToolbar` serves both placements, with position driving the layout
only, so the right column is unchanged. The readout counts the rows the
search left listed, against the whole series set.

#### Issues closed by this PR

Closes https://github.com/SigNoz/engineering-pod/issues/3959
Closes https://github.com/SigNoz/engineering-pod/issues/3967
Closes https://github.com/SigNoz/engineering-pod/issues/3976
Closes https://github.com/SigNoz/engineering-pod/issues/3977

#### Additional Information

- The palette replacements are computed (even spread across the non-red
arc), not hand-designed — worth a visual pass across light and dark
before merging.
- Two tests asserted a palette hex for a given series label and are
updated to the new value.
- The search row's 24px height and 4px gap are pinned as
`LEGEND_TOOLBAR_HEIGHT` / `LEGEND_TOOLBAR_GAP` beside the existing row
constants; they must match the stylesheet or the reserved rectangle
clips a row.

<img width="1611" height="634" alt="image"
src="https://github.com/user-attachments/assets/2c21a458-e142-4e4f-baff-5bbcbb072140"
/>
2026-09-23 18:58:55 +00:00
Nityananda Gohain
5a1be60745 fix(ai-o11y): scope overview dashboard to gen_ai spans and move message attributes (#12967)
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
<!--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
- Top span names and Cost by service use `builder_ai_query`, so non-AI
spans no longer show up.
- LLM cost/token panels filter on `gen_ai.request.model EXISTS`; with
variables on "All" they scanned every span.
- Default span mappers now move (not copy) vendor message keys into
`gen_ai.input.messages` / `gen_ai.output.messages`, as documented.
- Bumped versions: dashboard to 3, `gen_ai.llm` mapper to 3,
`gen_ai.agent` mapper to 2.

<!--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 14:56:04 +00:00
Gaurav Tewari
cad93a8063 chore: remove beta tag class (#12965)
<!--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

Removed beta tag class from AI o11y . 
since it remove margin and breaks alignment. don't see any use of it .
it also shifts the pin icon a bit

before - 
<img width="149" height="160" alt="image"
src="https://github.com/user-attachments/assets/26770802-b5b0-452b-b66a-fecda0d49c38"
/>
 
 
<img width="331" height="152" alt="image"
src="https://github.com/user-attachments/assets/fba73ebe-2655-4de5-8488-eb6bb0bfebca"
/>


 now - 
 
<img width="158" height="192" alt="image"
src="https://github.com/user-attachments/assets/fb0ca483-e8db-4ff0-bb36-e0186c2e9458"
/>

<img width="272" height="63" alt="image"
src="https://github.com/user-attachments/assets/68486c3f-a704-4077-82e5-089452abf773"
/>

 



it was added for long text .
https://github.com/SigNoz/signoz/pull/5801/changes#r1736497688
for now in side nav we don't have any long text. and maybe we could have
handled this better
 

<!--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

<!--Please delete paragraphs that you did not use before submitting.-->

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-23 11:45:33 +00:00
Abhi kumar
aed096bf27 refactor(query-builder): compose the panel-type field map instead of listing it (#12781)
#### Description

`panelTypeDataSourceFormValuesMap` — the map deciding which builder
fields survive a panel-type switch — spelled out all 21 panel-type ×
data-source combinations as literal field lists, 435 lines of them.

They reduce to seven distinct sets:

- logs and traces carry **identical** fields for every panel type
- metrics adds its two aggregation steps (`timeAggregation`,
`spaceAggregation`)
- every panel type is one of four query shapes: series, scalar table,
single value, raw rows

Much of the apparent variation was ordering noise — a bar chart and a
table on logs have the *same* field set, listed in a different order.

Composed from those rules it's 84 lines, and the policy is legible at a
glance: charts, table and pie share a surface; table and pie differ only
by `reduceTo` on metrics; a single value has nothing to group, limit or
order; raw rows carry no aggregation. Two asymmetries that were buried
in the literals are now called out where they're decided, rather than
silently reproduced.

**No behaviour change.** Adding a panel type becomes one line — "which
shape is it?"

#### Additional Information

- Equivalence was checked cell by cell against the previous literal
table before it was removed; all 21 cells matched as sets. The old table
is in git history at `main:frontend/src/lib/query/panelQuery.ts` if you
want to re-run that comparison.
- The specs pin the **rules**, not the values, so they fail when a rule
changes — the moment to stop and decide — rather than whenever a field
moves. Two are worth reading:
- *"gives bar / histogram / table / pie the same non-metrics fields as a
time series"* states the hazard composing introduces: the aggregating
types share one field list, so an edit meant for charts reaches table
and pie too. A failure there names the reason.
- *"gives every cell its own array instance"* — the `QueryBuilder`
provider does `propsRequired?.push('dataSource')` on the list it reads
from this map, so cells sharing an instance would leak fields into each
other. My first draft shared one array across 10 cells; this test is
what guards it.
- Order is not asserted anywhere: `handleQueryChange` and the provider
both assign each field independently via `set()`, so sequence carries no
meaning.
- Three consumers, all exercised: `handleQueryChange` (dashboards v2's
kind switcher and V1's `PanelTypeSelector`) and the shared
`QueryBuilder` provider every explorer uses. Verified with `tsgo`,
`oxlint`, and the `lib` / `providers` / `WidgetCard` / Logs+Traces
explorer / `DashboardPage` suites: 243 suites, 2056 tests.
- Pre-existing and deliberately left alone: that `push` mutates module
state, so the arrays grow by one `'dataSource'` entry on every
query-builder change. Harmless today only because the assignment is
idempotent; `[...propsRequired, 'dataSource']` would fix it, but that's
the provider's bug, not this map's.
2026-09-23 11:24:51 +00:00
Nityananda Gohain
6b66ab64c8 fix: add ai-o11y quick filter migration (#12964)
<!--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
Added migration to update old instances where quick filters for ai-o11y
is not present.

<!--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 11:15:42 +00:00
Naman Verma
362d3a4fdf fix: backfill notification channel tuples (#12960)
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
Release Drafter / update_release_draft (push) Has been cancelled
cacheci / tests (push) Has been cancelled
<!--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
175 changed files with 3581 additions and 1531 deletions

11
.github/CODEOWNERS vendored
View File

@@ -200,6 +200,15 @@ go.mod @therealpandey
/frontend/src/container/ListAlertRules/ @SigNoz/pulse-frontend
/frontend/src/container/TriggeredAlerts/ @SigNoz/pulse-frontend
/frontend/src/container/AnomalyAlertEvaluationView/ @SigNoz/pulse-frontend
/frontend/src/container/RoutingPolicies/ @SigNoz/pulse-frontend
/frontend/src/components/AlertBreadcrumb/ @SigNoz/pulse-frontend
/frontend/src/container/EditRules/ @SigNoz/pulse-frontend
/frontend/src/components/AlertDetailsFilters/ @SigNoz/pulse-frontend
/frontend/src/components/Alerts/ @SigNoz/pulse-frontend
/frontend/src/hooks/routingPolicies/ @SigNoz/pulse-frontend
/frontend/src/types/api/alerts/ @SigNoz/pulse-frontend
/frontend/src/providers/Alert.tsx @SigNoz/pulse-frontend
/frontend/src/constants/alerts.ts @SigNoz/pulse-frontend
## Notification Channels
/frontend/src/pages/ChannelsEdit/ @SigNoz/pulse-frontend
@@ -207,6 +216,8 @@ go.mod @therealpandey
/frontend/src/container/AllAlertChannels/ @SigNoz/pulse-frontend
/frontend/src/container/CreateAlertChannels/ @SigNoz/pulse-frontend
/frontend/src/container/EditAlertChannels/ @SigNoz/pulse-frontend
/frontend/src/container/FormAlertChannels/ @SigNoz/pulse-frontend
/frontend/src/hooks/notificationChannels/ @SigNoz/pulse-frontend
## OpenAPI Schema - Generated
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv

View File

@@ -23,4 +23,3 @@ We **recommend** (almost enforce) reviewing these guides before contributing to
- [SQL](sql.md) - Database and SQL patterns
- [DSL Filtering to SQL](dslfilteringtosql.md) - Compiling the list filter DSL to relational-store WHERE clauses
- [Types](types.md) - Domain types, request/response bodies, and storage rows in `pkg/types/`
sdsdcdsc

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

@@ -1,6 +1,7 @@
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';
@@ -83,6 +84,10 @@ const translationsReady = i18n.loadNamespaces(
),
);
// 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',

View File

@@ -30,6 +30,8 @@ interface CapturedMessage {
text: string;
}
const PREPARE_TIMEOUT_MS = 180_000;
const messagesByPage = new WeakMap<Page, CapturedMessage[]>();
/**
@@ -52,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) {

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

@@ -12,13 +12,14 @@ import { CmdKPalette } from 'components/cmdKPalette/cmdKPalette';
import NotFound from 'components/NotFound';
import { ShiftHoldOverlayController } from 'components/ShiftOverlay/ShiftHoldOverlayController';
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import AppLayout from 'container/AppLayout';
import Hex from 'crypto-js/enc-hex';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { StatusCodes } from 'http-status-codes';
@@ -58,7 +59,6 @@ function App(): JSX.Element {
isFetchingActiveLicense,
activeLicenseFetchError,
userFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
featureFlags,
org,
@@ -66,6 +66,8 @@ function App(): JSX.Element {
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isSavedViewEnabled = useSavedViewEnabled();
const chatSupport = useChatSupport();
const { hostname } = window.location;
const [pathname, setPathname] = useState(history.location.pathname);
@@ -253,7 +255,9 @@ function App(): JSX.Element {
}, [isDarkMode]);
useEffect(() => {
// The bottom strip carries Support, so the floating bubble goes entirely.
if (
isSavedViewEnabled ||
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/') ||
pathname === '/ai-assistant' ||
@@ -263,71 +267,32 @@ function App(): JSX.Element {
} else {
window.Pylon?.('showChatBubble');
}
}, [pathname]);
}, [pathname, isSavedViewEnabled]);
// eslint-disable-next-line sonarjs/cognitive-complexity
// Identity for the Pylon widget. Whether this user gets Pylon at all is
// `useChatSupport`'s call — this only fills in who they are.
useEffect(() => {
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing
// to something went wrong which would ideally need a reload.
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
activeLicense &&
trialInfo
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
isPremiumSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
?.active || false;
}
const showAddCreditCardModal =
!isPremiumSupportEnabled && !trialInfo?.trialConvertedToSubscription;
if (
isLoggedInState &&
isChatSupportEnabled &&
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser) &&
window.signozBootData?.settings?.pylon?.enabled
) {
const email = user.email || '';
const secret = window.signozBootData?.settings?.pylon?.identitySecret || '';
let emailHash = '';
if (email && secret) {
emailHash = HmacSHA256(email, Hex.parse(secret)).toString(Hex);
}
window.pylon = {
chat_settings: {
app_id: window.signozBootData?.settings?.pylon?.appId,
email: user.email,
name: user.displayName || user.email,
email_hash: emailHash,
},
};
}
if (chatSupport !== ChatSupportState.Pylon) {
return;
}
}, [
isLoggedInState,
user,
pathname,
trialInfo?.trialConvertedToSubscription,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
activeLicense,
trialInfo,
isCloudUser,
isEnterpriseSelfHostedUser,
]);
const email = user.email || '';
const secret = window.signozBootData?.settings?.pylon?.identitySecret || '';
let emailHash = '';
if (email && secret) {
emailHash = HmacSHA256(email, Hex.parse(secret)).toString(Hex);
}
window.pylon = {
chat_settings: {
app_id: window.signozBootData?.settings?.pylon?.appId,
email: user.email,
name: user.displayName || user.email,
email_hash: emailHash,
},
};
}, [chatSupport, user]);
useEffect(() => {
if (!isFetchingUser && isCloudUser && user && user.email) {

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,103 @@
import { useMutation } from 'react-query';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { createSubscription } from 'api/generated/services/subscriptions';
import type { CreateSubscription201 } from 'api/generated/services/sigNoz.schemas';
import { useNotifications } from 'hooks/useNotifications';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SubscriptionCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
import { CreditCard, X } from '@signozhq/icons';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
interface AddCreditCardModalProps {
open: boolean;
onClose: () => void;
onAddCreditCard?: () => void;
}
/**
* Shown to trial users who have not added a card, in place of chat support.
* Submitting creates the subscription and opens the returned billing URL.
*/
function AddCreditCardModal({
open,
onClose,
onAddCreditCard,
}: AddCreditCardModalProps): JSX.Element {
const { notifications } = useNotifications();
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
createSubscription,
{ onSuccess: handleBillingOnSuccess, onError: handleBillingOnError },
);
const handleAddCreditCard = (): void => {
onAddCreditCard?.();
updateCreditCard({ url: getBaseUrl() });
};
return (
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={open}
closable
onCancel={onClose}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={onClose}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<AuthZTooltip
key="submit"
checks={[SubscriptionCreatePermission]}
withPortal={false}
>
<Button
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>
</AuthZTooltip>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>.
Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
);
}
AddCreditCardModal.defaultProps = { onAddCreditCard: undefined };
export default AddCreditCardModal;

View File

@@ -1,63 +1,15 @@
import { useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Button } from 'antd';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import logEvent from 'api/common/logEvent';
import { createSubscription } from 'api/generated/services/subscriptions';
import type { CreateSubscription201 } from 'api/generated/services/sigNoz.schemas';
import { useNotifications } from 'hooks/useNotifications';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SubscriptionCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
import { CreditCard, MessageSquareText, X } from '@signozhq/icons';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import { MessageSquareText } from '@signozhq/icons';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
useState(false);
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
createSubscription,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const { pathname } = useLocation();
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `chat support icon`,
page: pathname,
});
updateCreditCard({
url: getBaseUrl(),
});
};
return (
<>
<div className="chat-support-gateway">
@@ -76,47 +28,16 @@ export default function ChatSupportGateway(): JSX.Element {
</Button>
</div>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
<AddCreditCardModal
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<AuthZTooltip
key="submit"
checks={[SubscriptionCreatePermission]}
withPortal={false}
>
<Button
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>
</AuthZTooltip>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
onClose={(): void => setIsAddCreditCardModalOpen(false)}
onAddCreditCard={(): void => {
logEvent('Add Credit card modal: Clicked', {
source: `chat support icon`,
page: pathname,
});
}}
/>
</>
);
}

View File

@@ -1,22 +1,14 @@
import { useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { Button, Modal, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Button, Tooltip } from 'antd';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import logEvent from 'api/common/logEvent';
import { createSubscription } from 'api/generated/services/subscriptions';
import type { CreateSubscription201 } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { SubscriptionCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/subscription.permissions';
import { defaultTo } from 'lodash-es';
import { CircleHelp, CreditCard, X } from '@signozhq/icons';
import { CircleHelp } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import './LaunchChatSupport.styles.scss';
@@ -41,7 +33,6 @@ function LaunchChatSupport({
chatMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null {
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const { notifications } = useNotifications();
const {
trialInfo,
featureFlags,
@@ -119,43 +110,12 @@ function LaunchChatSupport({
}
};
const handleBillingOnSuccess = (data: CreateSubscription201): void => {
if (data?.data?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.data.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (error: APIError): void => {
notifications.error({
message: error.getErrorCode(),
description: error.getErrorMessage(),
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
createSubscription,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `facing issues button`,
page: pathname,
...attributes,
});
updateCreditCard({
url: getBaseUrl(),
});
};
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
@@ -175,47 +135,11 @@ function LaunchChatSupport({
</Button>
</Tooltip>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
<AddCreditCardModal
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<AuthZTooltip
key="submit"
checks={[SubscriptionCreatePermission]}
withPortal={false}
>
<Button
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>
</AuthZTooltip>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
onClose={(): void => setIsAddCreditCardModalOpen(false)}
onAddCreditCard={handleAddCreditCard}
/>
</div>
) : null;
}

View File

@@ -2,6 +2,8 @@
display: flex;
flex-direction: row;
position: relative;
flex: 1;
min-height: 0;
.quick-filters-settings-container {
flex: 0 0 0;

View File

@@ -0,0 +1,33 @@
// The one `overflow: hidden` in the chain. Ancestors (RouteTab, AppLayout)
// only hand height down; each pane below owns its own scroll.
.layout {
display: flex;
flex: 1;
height: 100%;
min-height: 0;
overflow: hidden;
}
// Positioned so overlays (settings drawer) paint above the content pane
// without changing this pane's layout width.
.filters {
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
min-height: 0;
position: relative;
overflow: visible;
z-index: 2;
}
// Bounded box for the OverlayScrollbar inside it (`.overlay-scrollbar` is
// `height: 100%`), which owns the scrolling.
.content {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}

View File

@@ -0,0 +1,54 @@
import { ComponentProps, ReactNode } from 'react';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import QuickFilters from '../QuickFilters';
import styles from './QuickFiltersLayout.module.scss';
// Same optionality as `<QuickFilters />` in JSX (honours its defaultProps).
type QuickFiltersElementProps = JSX.LibraryManagedAttributes<
typeof QuickFilters,
ComponentProps<typeof QuickFilters>
>;
export interface QuickFiltersLayoutProps {
quickFilterProps: QuickFiltersElementProps;
showFilters: boolean;
className?: string;
contentClassName?: string;
testId?: string;
children: ReactNode;
}
function QuickFiltersLayout({
quickFilterProps,
showFilters,
className,
contentClassName,
testId,
children,
}: QuickFiltersLayoutProps): JSX.Element {
return (
<div className={cx(styles.layout, className)} data-testid={testId}>
{showFilters && (
<aside
className={styles.filters}
data-testid="quick-filters-layout-filters"
>
<QuickFilters {...quickFilterProps} />
</aside>
)}
<section
className={cx(styles.content, contentClassName)}
data-testid="quick-filters-layout-content"
>
<OverlayScrollbar>
<div>{children}</div>
</OverlayScrollbar>
</section>
</div>
);
}
export default QuickFiltersLayout;

View File

@@ -0,0 +1,79 @@
import { render, screen } from 'tests/test-utils';
import { QuickFiltersSource } from '../../types';
import QuickFiltersLayout from '../QuickFiltersLayout';
jest.mock('../QuickFiltersLayout.module.scss', () => ({
__esModule: true,
default: {
layout: 'layout',
filters: 'filters',
content: 'content',
},
}));
jest.mock('../../QuickFilters', () => ({
__esModule: true,
default: ({ source }: { source: string }): JSX.Element => (
<div data-testid="quick-filters">{source}</div>
),
}));
const quickFilterProps = {
source: QuickFiltersSource.TRACES_EXPLORER,
handleFilterVisibilityChange: jest.fn(),
};
describe('QuickFiltersLayout', () => {
it('renders QuickFilters with the given props inside the filters pane', () => {
render(
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
<div>content</div>
</QuickFiltersLayout>,
);
const filtersPane = screen.getByTestId('quick-filters-layout-filters');
expect(filtersPane).toContainElement(screen.getByTestId('quick-filters'));
expect(screen.getByTestId('quick-filters')).toHaveTextContent(
QuickFiltersSource.TRACES_EXPLORER,
);
expect(screen.getByTestId('quick-filters-layout-content')).toHaveTextContent(
'content',
);
});
it('does not render the filters pane when showFilters is false', () => {
render(
<QuickFiltersLayout showFilters={false} quickFilterProps={quickFilterProps}>
<div>content</div>
</QuickFiltersLayout>,
);
expect(
screen.queryByTestId('quick-filters-layout-filters'),
).not.toBeInTheDocument();
expect(screen.queryByTestId('quick-filters')).not.toBeInTheDocument();
expect(screen.getByText('content')).toBeInTheDocument();
});
it('merges classNames onto the root and content panes', () => {
render(
<QuickFiltersLayout
showFilters
quickFilterProps={quickFilterProps}
className="page-root"
contentClassName="page-content"
testId="page"
>
<div>content</div>
</QuickFiltersLayout>,
);
const root = screen.getByTestId('page');
expect(root).toHaveClass('layout', 'page-root');
expect(screen.getByTestId('quick-filters-layout-content')).toHaveClass(
'content',
'page-content',
);
});
});

View File

@@ -6,27 +6,12 @@
left: 0;
z-index: 999;
width: 342px;
height: 100%;
background: var(--l1-background);
transition: width 0.05s ease-in-out;
overflow: hidden;
color: var(--l1-foreground);
&.qf-logs-explorer {
height: calc(100vh - 45px);
}
&.qf-exceptions {
height: 100vh;
}
&.qf-api-monitoring {
height: calc(100vh - 45px);
}
&.qf-traces-explorer {
height: calc(100vh - 45px);
}
&.hidden {
width: 0;
}

View File

@@ -0,0 +1,38 @@
// Hands the parent's height down to the active pane and lets the pane scroll
// its own content, so TopNav and the tab bar stay put. Child combinators only
// (nested Tabs must not be caught).
.routeTab {
flex: 1;
min-height: 0;
}
.routeTab > :global(.ant-tabs-content-holder) {
display: flex;
flex-direction: column;
}
.routeTab > :global(.ant-tabs-content-holder) > :global(.ant-tabs-content) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.routeTab
> :global(.ant-tabs-content-holder)
> :global(.ant-tabs-content)
> :global(.ant-tabs-tabpane-active) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.routeTab
> :global(.ant-tabs-content-holder)
> :global(.ant-tabs-content)
> :global(.ant-tabs-tabpane-active)
> :global(.overlay-scrollbar) {
flex: 1;
min-height: 0;
}

View File

@@ -5,6 +5,11 @@ import { fireEvent, render, screen } from 'tests/test-utils';
import RouteTab from './index';
import { RouteTabProps } from './types';
jest.mock('./RouteTab.module.scss', () => ({
__esModule: true,
default: { routeTab: 'routeTab' },
}));
function DummyComponent1(): JSX.Element {
return <div>Dummy Component 1</div>;
}
@@ -74,6 +79,36 @@ describe('RouteTab component', () => {
expect(history.location.pathname).toBe('/tab2');
});
it('applies the layout class alongside a custom className', () => {
const history = createMemoryHistory();
const { container } = render(
<Router history={history}>
<RouteTab
history={history}
routes={testRoutes}
activeKey="Tab1"
className="custom-tabs"
/>
</Router>,
);
expect(container.querySelector('.ant-tabs')).toHaveClass(
'routeTab',
'custom-tabs',
);
});
it('renders the active tab content inside an overlay scrollbar', () => {
const history = createMemoryHistory();
const { container } = render(
<Router history={history}>
<RouteTab history={history} routes={testRoutes} activeKey="Tab1" />
</Router>,
);
expect(
container.querySelector('.ant-tabs-tabpane-active > .overlay-scrollbar'),
).toHaveTextContent('Dummy Component 1');
});
it('calls onChangeHandler on tab change', () => {
const onChangeHandler = jest.fn();
const history = createMemoryHistory();

View File

@@ -5,20 +5,32 @@ import {
useParams,
} from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import cx from 'classnames';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { RouteTabProps } from './types';
import styles from './RouteTab.module.scss';
interface Params {
[key: string]: string;
}
/**
* Each pane scrolls its own content inside an OverlayScrollbar, so the tab bar
* stays put. Mounted as the page root the pane is bounded to the viewport; inside
* a plain block wrapper the scroller is inert and the page scrolls as usual.
* Pane content that needs a bounded box must size itself with `height: 100%`
* (the scroller's viewport is block flow, so `flex: 1` has no effect there).
*/
function RouteTab({
routes,
activeKey,
onChangeHandler,
history,
showRightSection,
className,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
const params = useParams<Params>();
@@ -50,11 +62,16 @@ function RouteTab({
label: name,
key,
tabKey: route,
children: <Component />,
children: (
<OverlayScrollbar>
<Component />
</OverlayScrollbar>
),
}));
return (
<Tabs
className={cx(styles.routeTab, className)}
onChange={onChange}
destroyInactiveTabPane
activeKey={currentRoute?.key || activeKey}

View File

@@ -8,6 +8,5 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
USE_JSON_BODY = 'use_json_body',
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
}

View File

@@ -47,4 +47,5 @@ export enum LOCALSTORAGE {
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
LLM_ATTRIBUTE_MAPPING_TEST_SPAN = 'LLM_ATTRIBUTE_MAPPING_TEST_SPAN',
SAVED_VIEW_ENABLED = 'SAVED_VIEW_ENABLED',
}

View File

@@ -129,6 +129,10 @@ const themeColors = {
salmon2: '#FFAB91',
salmon3: '#E0876A',
},
/* Series palette (dark). Hues in the red band are deliberately absent: red is
reserved for thresholds and error states, so an arbitrary series must never
claim it. generateColor indexes by `hash % Object.keys(...).length`, so
adding or removing an entry recolors every existing chart. */
chartcolors: {
// Blues (3)
dodgerBlue: '#2F80ED',
@@ -152,13 +156,13 @@ const themeColors = {
// Oranges (3)
festivalOrange: '#F2994A',
coralOrange: '#E17055',
amber1: '#E1A155',
pumpkin: '#FF7F50',
// Reds (3)
radicalRed: '#FF1A66',
crimsonRed: '#EB5757',
fireRed: '#E10600',
// Olives / Greens (3)
olive1: '#DFC33A',
olive2: '#D5E55D',
green7: '#81C220',
// Pinks (3)
hotPink: '#E84393',
@@ -191,9 +195,9 @@ const themeColors = {
orange1: '#D35400',
orange2: '#E67E22',
orange3: '#F5B041',
red1: '#C0392B',
red2: '#E74C3C',
red3: '#EC7063',
green8: '#5AC02B',
green9: '#48E043',
green10: '#68E788',
pink1: '#D81B60',
pink2: '#E91E63',
pink3: '#F06292',
@@ -212,9 +216,9 @@ const themeColors = {
coral1: '#E67E22',
coral2: '#F39C12',
coral3: '#F5B041',
crimson1: '#C0392B',
crimson2: '#E74C3C',
crimson3: '#EC7063',
teal7: '#2BC07B',
teal8: '#43E0C5',
teal9: '#68D9E7',
violet1: '#8E44AD',
violet2: '#9B59B6',
violet3: '#BB8FCE',
@@ -224,18 +228,18 @@ const themeColors = {
forest1: '#27AE60',
forest2: '#2ECC71',
forest3: '#58D68D',
blush1: '#FF6F91',
cyan4: '#83C2EB',
blush2: '#FF85A2',
blush3: '#FFA0B3',
lavender1: '#9B59B6',
lavender2: '#AF7AC5',
lavender3: '#C39BD3',
tomato1: '#E74C3C',
tomato2: '#EC7063',
tomato3: '#F1948A',
salmon1: '#FF6B6B',
salmon2: '#FF8787',
salmon3: '#FFA1A1',
blue7: '#4375E0',
blue8: '#686DE7',
indigo1: '#A68EED',
indigo2: '#B980EA',
purple6: '#EE98D9',
olive3: '#F2F0AE',
mustard1: '#F1C40F',
mustard2: '#F7DC6F',
mustard3: '#F9E79F',
@@ -254,9 +258,9 @@ const themeColors = {
blue4: '#2874A6',
blue5: '#2E86C1',
blue6: '#3498DB',
red4: '#C0392B',
red5: '#E74C3C',
red6: '#EC7063',
purple4: '#A52BC0',
purple5: '#E043D0',
magenta4: '#E768B5',
orange4: '#D35400',
orange5: '#E67E22',
orange6: '#EB984E',
@@ -267,18 +271,19 @@ const themeColors = {
gold5: '#F1C40F',
gold6: '#F4D03F',
},
/* Series palette (light). Same red-free constraint as chartcolors above. */
lightModeColor: {
radicalRed: '#D81B60',
magenta1: '#D81B60',
dodgerBlueDark: '#1E5BD9',
steelgrey: '#344B6B',
steelpurple: '#5E548E',
steelindigo: '#8E4A7C',
steelpink: '#B63A6F',
steelcoral: '#E14B5A',
amber1: '#E1A14B',
steelorange: '#E76F2F',
steelgold: '#E09B00',
steelrust: '#C93A50',
olive1: '#C9BD3A',
steelgreen: '#2F7D69',
mediumOrchidDark: '#8E24AA',
@@ -286,17 +291,17 @@ const themeColors = {
seaGreen: '#1E7F5A',
turquoiseBlueDark: '#007EA7',
silverDark: '#5F5F5F',
outrageousOrangeDark: '#E64A19',
roseBudDark: '#D84315',
green1: '#ACDB24',
green2: '#66CC21',
deepSkyBlueDark: '#0277BD',
royalBlue: '#2A4FDB',
avocadoDark: '#6B6B1E',
mintGreenDark: '#2E9E55',
chestnut: '#8B3A3A',
green3: '#3F8B3A',
limaDark: '#5C7F00',
olive: '#6E7F00',
beautyBushDark: '#C93C3C',
green4: '#3CC964',
danube: '#4F6FB3',
oliveDrab: '#4F7F1A',
@@ -304,13 +309,13 @@ const themeColors = {
electricLimeDark: '#6B8F00',
robin: '#2F4FCC',
harleyOrange: '#CC2E12',
teal1: '#1FBF83',
gladeGreen: '#4F7F46',
hemlock: '#5C5C45',
vidaLoca: '#3D6B00',
rust: '#993300',
red: '#C62828',
teal2: '#28C6C1',
blue: '#1A237E',
green: '#1B7F3A',
purple: '#6A1B9A',
@@ -320,7 +325,7 @@ const themeColors = {
brown: '#7A3A1E',
teal: '#006D6F',
limeDark: '#4C8C2B',
maroon: '#6D1B1B',
cyan1: '#1B546D',
navy: '#0D1B5E',
gray: '#616161',
@@ -328,25 +333,25 @@ const themeColors = {
indigo: '#303F9F',
slateGray: '#556B7C',
chocolate: '#9C4A1A',
tomato: '#E53935',
blue1: '#3B74DF',
steelBlue: '#3A6EA5',
peruDark: '#B35E00',
darkOliveGreen: '#445B1F',
indianRed: '#B04040',
blue2: '#4041B0',
mediumSlateBlue: '#5C6BC0',
rosyBrownDark: '#A94444',
indigo1: '#6644A9',
darkSlateGray: '#2E4A4A',
fuchsia: '#C511C5',
salmonDark: '#E64A3C',
darkSalmonDark: '#C85A3A',
indigo2: '#AD42E0',
purple1: '#C83AC5',
paleVioletRedDark: '#C2186A',
mediumPurple: '#7E57C2',
darkOrchid: '#7B1FA2',
mediumSeaGreenDark: '#2E8B57',
lightCoralDark: '#E57373',
purple2: '#E573BC',
gold: '#D4AF37',
sandyBrownDark: '#C76A15',

View File

@@ -1,23 +1,15 @@
.api-monitoring-page {
display: flex;
height: 100%;
.api-monitoring-explorer {
.api-quick-filters-header {
padding: 12px;
border-bottom: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
.api-quick-filter-left-section {
width: 0%;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
.api-quick-filters-header {
padding: 12px;
border-bottom: 1px solid var(--l1-border);
border-right: 1px solid var(--l1-border);
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
line-height: 18px;
}
font-size: 14px;
line-height: 18px;
}
.api-module-right-section {
@@ -161,16 +153,6 @@
}
}
}
&.filter-visible {
.api-quick-filter-left-section {
width: 260px;
}
.api-module-right-section {
width: calc(100% - 260px);
}
}
}
.no-filtered-domains-message-container {

View File

@@ -1,8 +1,7 @@
import { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
@@ -20,20 +19,21 @@ function Explorer(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className={cx('api-monitoring-page', 'filter-visible')}>
<section className="api-quick-filter-left-section">
<QuickFilters
className="qf-api-monitoring"
source={QuickFiltersSource.API_MONITORING}
signal={SignalType.API_MONITORING}
showFilterCollapse={false}
showQueryName={false}
handleFilterVisibilityChange={(): void => {}}
useFieldApis={quickFilterFieldApis}
/>
</section>
<QuickFiltersLayout
className="api-monitoring-explorer"
showFilters
quickFilterProps={{
className: 'qf-api-monitoring',
source: QuickFiltersSource.API_MONITORING,
signal: SignalType.API_MONITORING,
showFilterCollapse: false,
showQueryName: false,
handleFilterVisibilityChange: (): void => {},
useFieldApis: quickFilterFieldApis,
}}
>
<DomainList />
</div>
</QuickFiltersLayout>
</Sentry.ErrorBoundary>
);
}

View File

@@ -53,6 +53,10 @@
z-index: 0;
background: var(--l1-background);
// Column so the bottom strip sits under the scrolling content, not inside it.
display: flex;
flex-direction: column;
&.full-screen-content {
width: 100%;
}
@@ -70,7 +74,9 @@
.chat-support-gateway {
position: fixed;
bottom: 20px;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: calc(20px + var(--bottom-strip-height, 0px));
right: 20px;
z-index: 1000;

View File

@@ -43,6 +43,7 @@ import { USER_PREFERENCES } from 'constants/userPreferences';
import AIAssistantModal from 'container/AIAssistant/AIAssistantModal';
import AIAssistantPanel from 'container/AIAssistant/AIAssistantPanel';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
import BottomStrip from 'container/BottomStrip';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import dayjs from 'dayjs';
@@ -51,6 +52,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useNotifications } from 'hooks/useNotifications';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import useTabVisibility from 'hooks/useTabFocus';
import history from 'lib/history';
import { isNull } from 'lodash-es';
@@ -402,6 +404,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}, [pathname]);
const isToDisplayLayout = isLoggedIn;
const isSavedViewEnabled = useSavedViewEnabled();
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
const pageTitle = t(routeKey);
@@ -868,6 +871,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</OverlayScrollbar>
</LayoutContent>
</Sentry.ErrorBoundary>
{isSavedViewEnabled && isToDisplayLayout && !renderFullScreen && (
<BottomStrip />
)}
</div>
{isLoggedIn && isAIAssistantEnabled && (
@@ -878,7 +885,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
</Flex>
{showAddCreditCardModal && <ChatSupportGateway />}
{showAddCreditCardModal && !isSavedViewEnabled && <ChatSupportGateway />}
{showChangelogModal && changelog && (
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
)}

View File

@@ -12,8 +12,12 @@ export const Layout = styled(LayoutComponent)`
}
`;
// Takes the height left in `.app-content` after the bottom strip.
// `min-height: 0` is not needed right now, overlayscrollbars already sets
// `overflow: auto` here. Kept so this does not break if that goes away.
export const LayoutContent = styled(LayoutComponent.Content)`
height: 100%;
flex: 1;
min-height: 0;
&::-webkit-scrollbar {
width: 0.1rem;
}

View File

@@ -0,0 +1,36 @@
.askNoz {
display: flex;
align-items: center;
flex-shrink: 0;
}
.badge {
flex-shrink: 0;
display: inline-flex;
line-height: 0;
color: var(--accent-primary);
}
.pulseDot {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
animation: askNozDotPulse 1.5s ease-in-out infinite;
transform: scale(0.8);
margin-right: -12px;
}
@keyframes askNozDotPulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.35;
transform: scale(0.82);
}
}

View File

@@ -0,0 +1,76 @@
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { selectPendingUserInputStreamCount } from 'container/AIAssistant/store/pendingInputSelectors';
import {
openAIAssistant,
useAIAssistantStore,
} from 'container/AIAssistant/store/useAIAssistantStore';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import ROUTES from 'constants/routes';
import { Dot } from '@signozhq/icons';
import styles from './AskNoz.module.scss';
/**
* Opens the Noz drawer, matching the header entry it replaces. Cmd+K opens the
* modal instead; this is deliberately the drawer.
*
* Carries the header's pending badge: when Noz is blocked on the user
* (`awaiting_approval` / `awaiting_clarification`) a dot pulses. Without it,
* hiding the header button would remove a notification rather than move it.
*/
function AskNoz(): JSX.Element | null {
const { pathname } = useLocation();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isDrawerOpen = useAIAssistantStore((state) => state.isDrawerOpen);
const isModalOpen = useAIAssistantStore((state) => state.isModalOpen);
const pendingUserInputCount = useAIAssistantStore(
selectPendingUserInputStreamCount,
);
// Noz is already on screen in the modal, so the "needs you" dot would be noise.
const showPendingBadge = pendingUserInputCount > 0 && !isModalOpen;
// The drawer does not render on the Noz full page, so the button would be inert.
const isAIAssistantPage = pathname.startsWith(ROUTES.AI_ASSISTANT_BASE);
if (!isAIAssistantEnabled || isDrawerOpen || isAIAssistantPage) {
return null;
}
return (
<div className={styles.askNoz} data-testid="bottom-strip-ask-noz">
{showPendingBadge && (
<span className={styles.badge} aria-hidden>
<span className={styles.pulseDot}>
<Dot size={36} />
</span>
</span>
)}
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<Button
variant="ghost"
color="secondary"
size="sm"
className="noz-wave"
prefix={<Noz size={16} />}
onClick={(): void => openAIAssistant()}
aria-label={
showPendingBadge
? `Ask Noz, ${pendingUserInputCount} ${
pendingUserInputCount === 1 ? 'action needs' : 'actions need'
} your response`
: 'Ask Noz'
}
>
Ask Noz
</Button>
</TooltipSimple>
</div>
);
}
export default AskNoz;

View File

@@ -0,0 +1,133 @@
import { fireEvent } from '@testing-library/react';
import { openAIAssistant } from 'container/AIAssistant/store/useAIAssistantStore';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { render } from 'tests/test-utils';
import AskNoz from '../AskNoz';
jest.mock('hooks/useIsAIAssistantEnabled');
jest.mock('container/AIAssistant/store/useAIAssistantStore', () => ({
openAIAssistant: jest.fn(),
useAIAssistantStore: jest.fn(),
}));
const mockEnabled = useIsAIAssistantEnabled as jest.MockedFunction<
typeof useIsAIAssistantEnabled
>;
const mockOpen = openAIAssistant as jest.MockedFunction<typeof openAIAssistant>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { useAIAssistantStore } = jest.requireMock(
'container/AIAssistant/store/useAIAssistantStore',
) as { useAIAssistantStore: jest.Mock };
/** The component reads the store through three separate selector calls. */
function mockStore({
isDrawerOpen = false,
isModalOpen = false,
pendingCount = 0,
}: {
isDrawerOpen?: boolean;
isModalOpen?: boolean;
pendingCount?: number;
} = {}): void {
const state = { isDrawerOpen, isModalOpen, streams: {} };
useAIAssistantStore.mockImplementation((selector: (s: unknown) => unknown) => {
const picked = selector(state);
// `selectPendingUserInputStreamCount` walks `streams`, which is empty here,
// so stand in the count we want to assert against.
return typeof picked === 'number' ? pendingCount : picked;
});
}
// `TooltipSimple` replaces its trigger's props, so the testId lives on the
// wrapper and the button itself is reached by role — there is only ever one.
const SLOT = 'bottom-strip-ask-noz';
describe('AskNoz', () => {
beforeEach(() => {
jest.clearAllMocks();
mockEnabled.mockReturnValue(true);
mockStore();
});
describe('visibility', () => {
it('renders when the assistant is enabled', () => {
const { getByRole } = render(<AskNoz />);
expect(getByRole('button')).toBeInTheDocument();
});
it('renders nothing when the assistant is disabled', () => {
mockEnabled.mockReturnValue(false);
const { queryByTestId } = render(<AskNoz />);
expect(queryByTestId(SLOT)).not.toBeInTheDocument();
});
it('renders nothing while the drawer is open', () => {
mockStore({ isDrawerOpen: true });
const { queryByTestId } = render(<AskNoz />);
expect(queryByTestId(SLOT)).not.toBeInTheDocument();
});
it('renders nothing on the Noz full page, where the drawer does not mount', () => {
const { queryByTestId } = render(<AskNoz />, undefined, {
initialRoute: '/ai-assistant/some-conversation-id',
});
expect(queryByTestId(SLOT)).not.toBeInTheDocument();
});
});
describe('what it does', () => {
it('opens the drawer on click', () => {
const { getByRole } = render(<AskNoz />);
fireEvent.click(getByRole('button'));
expect(mockOpen).toHaveBeenCalledTimes(1);
});
});
describe('pending badge', () => {
it('announces the count when Noz is waiting on the user', () => {
mockStore({ pendingCount: 2 });
const { getByRole } = render(<AskNoz />);
expect(getByRole('button')).toHaveAttribute(
'aria-label',
'Ask Noz, 2 actions need your response',
);
});
it('uses the singular for one pending action', () => {
mockStore({ pendingCount: 1 });
const { getByRole } = render(<AskNoz />);
expect(getByRole('button')).toHaveAttribute(
'aria-label',
'Ask Noz, 1 action needs your response',
);
});
it('stays quiet when nothing is pending', () => {
const { getByRole } = render(<AskNoz />);
expect(getByRole('button')).toHaveAttribute('aria-label', 'Ask Noz');
});
it('stays quiet while the modal is open, Noz is already on screen', () => {
mockStore({ isModalOpen: true, pendingCount: 3 });
const { getByRole } = render(<AskNoz />);
expect(getByRole('button')).toHaveAttribute('aria-label', 'Ask Noz');
});
});
});

View File

@@ -0,0 +1,46 @@
.strip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-shrink: 0;
height: var(--bottom-strip-height);
padding: 0 12px;
background: var(--l2-background);
border-top: 1px solid var(--l2-border);
--button-font-size: 12px;
// font styles
font-family: var(--font-family-sf-mono, monospace);
font-size: 13px;
font-weight: var(--font-weight-normal);
line-height: var(--line-height-none);
// Above page content, below the body-portalled overlays that are meant to
// cover the strip.
position: relative;
z-index: 1;
}
.left,
.right {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.left {
cursor: default;
}
// Temporary placeholder for the left slot. Replaced later.
.version {
color: var(--l2-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -0,0 +1,29 @@
import * as Sentry from '@sentry/react';
import { useAppContext } from 'providers/App/App';
import StripTypography from '../components/StripTypography/StripTypography';
import { useBottomStripStore } from '../store/useBottomStripStore';
function LeftSlot(): JSX.Element | null {
const { versionData } = useAppContext();
const left = useBottomStripStore((state) => state.left);
const ownerId = useBottomStripStore((state) => state.ownerId);
const version = versionData?.version?.trim();
const versionNode = version ? (
<StripTypography>{version}</StripTypography>
) : null;
if (!left) {
return versionNode;
}
return (
// Keyed so a page whose node threw does not leave the boundary latched on
// the fallback for every page after it.
<Sentry.ErrorBoundary key={ownerId} fallback={<>{versionNode}</>}>
{left}
</Sentry.ErrorBoundary>
);
}
export default LeftSlot;

View File

@@ -0,0 +1,45 @@
import { useState } from 'react';
import { Button } from '@signozhq/ui/button';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { MessageSquareText } from '@signozhq/icons';
function SupportButton(): JSX.Element | null {
const chatSupport = useChatSupport();
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
useState(false);
if (chatSupport === ChatSupportState.Unavailable) {
return null;
}
const handleClick = (): void => {
if (chatSupport === ChatSupportState.NeedsCard) {
setIsAddCreditCardModalOpen(true);
return;
}
window.Pylon?.('show');
};
return (
<>
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<MessageSquareText size={16} />}
onClick={handleClick}
testId="bottom-strip-support"
>
Support
</Button>
<AddCreditCardModal
open={isAddCreditCardModalOpen}
onClose={(): void => setIsAddCreditCardModalOpen(false)}
/>
</>
);
}
export default SupportButton;

View File

@@ -0,0 +1,81 @@
import { fireEvent } from '@testing-library/react';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { render } from 'tests/test-utils';
import SupportButton from '../SupportButton';
jest.mock('hooks/useChatSupport', () => ({
...jest.requireActual('hooks/useChatSupport'),
useChatSupport: jest.fn(),
}));
const mockChatSupport = useChatSupport as jest.MockedFunction<
typeof useChatSupport
>;
const BUTTON = 'bottom-strip-support';
const MODAL_TITLE = 'Add Credit Card for Chat Support';
describe('SupportButton', () => {
beforeEach(() => {
jest.clearAllMocks();
window.Pylon = jest.fn() as never;
});
describe('when Pylon is available', () => {
beforeEach(() => mockChatSupport.mockReturnValue(ChatSupportState.Pylon));
it('shows the button', () => {
const { getByTestId } = render(<SupportButton />);
expect(getByTestId(BUTTON)).toBeInTheDocument();
});
it('opens the Pylon widget on click', () => {
const { getByTestId } = render(<SupportButton />);
fireEvent.click(getByTestId(BUTTON));
expect(window.Pylon).toHaveBeenCalledWith('show');
});
it('does not open the credit card modal', () => {
const { getByTestId, queryByText } = render(<SupportButton />);
fireEvent.click(getByTestId(BUTTON));
expect(queryByText(MODAL_TITLE)).not.toBeInTheDocument();
});
});
describe('when the user needs a card', () => {
beforeEach(() => mockChatSupport.mockReturnValue(ChatSupportState.NeedsCard));
it('shows the same button', () => {
const { getByTestId } = render(<SupportButton />);
expect(getByTestId(BUTTON)).toBeInTheDocument();
});
it('opens the credit card modal on click, not Pylon', () => {
const { getByTestId, getByText } = render(<SupportButton />);
fireEvent.click(getByTestId(BUTTON));
expect(getByText(MODAL_TITLE)).toBeInTheDocument();
expect(window.Pylon).not.toHaveBeenCalled();
});
});
describe('when support is unavailable', () => {
beforeEach(() =>
mockChatSupport.mockReturnValue(ChatSupportState.Unavailable),
);
it('renders nothing at all', () => {
const { queryByTestId } = render(<SupportButton />);
expect(queryByTestId(BUTTON)).not.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,165 @@
import { type ReactNode, useState } from 'react';
import { fireEvent } from '@testing-library/react';
import { render } from 'tests/test-utils';
import { Info } from 'types/api/v1/version/get';
import BottomStrip, {
BOTTOM_STRIP_HEIGHT,
BOTTOM_STRIP_HEIGHT_VAR,
BOTTOM_STRIP_ON_CLASS,
} from '..';
import { useBottomStripStore } from '../store/useBottomStripStore';
import { useBottomStripLeft } from '../useBottomStripLeft';
/** Stands in for a page that puts something on the left of the strip. */
function Page({ children }: { children: ReactNode }): null {
useBottomStripLeft(children);
return null;
}
/** A page whose value changes without needing `rerender`. */
function ChangingPage(): JSX.Element {
const [count, setCount] = useState(600);
useBottomStripLeft(`${count} traces`);
return (
<button type="button" onClick={(): void => setCount(42)}>
change
</button>
);
}
/** A page node that blows up while the strip renders it. */
function Boom(): JSX.Element {
throw new Error('bad left node');
}
const VERSION = 'v0.134.67';
const versionData: Info = { version: VERSION, ee: 'Y', setupCompleted: true };
const withVersion = { appContextOverrides: { versionData } };
describe('BottomStrip', () => {
// The store is module level, so it outlives each test.
beforeEach(() => {
useBottomStripStore.setState({ left: null, ownerId: null });
});
it('publishes the body class and height property while mounted', () => {
const { unmount } = render(<BottomStrip />);
expect(document.body.classList.contains(BOTTOM_STRIP_ON_CLASS)).toBe(true);
expect(document.body.style.getPropertyValue(BOTTOM_STRIP_HEIGHT_VAR)).toBe(
`${BOTTOM_STRIP_HEIGHT}px`,
);
unmount();
expect(document.body.classList.contains(BOTTOM_STRIP_ON_CLASS)).toBe(false);
expect(document.body.style.getPropertyValue(BOTTOM_STRIP_HEIGHT_VAR)).toBe(
'',
);
});
// The string is whatever the Go build injected, so it is rendered untouched —
// same as SideNav. Release tags carry the "v", local builds do not.
it.each([['v0.134.67'], ['main-64f1c2a']])(
'renders the build version %p exactly as given',
(version) => {
const { getByText } = render(<BottomStrip />, undefined, {
appContextOverrides: {
versionData: { version, ee: 'Y', setupCompleted: true },
},
});
expect(getByText(version)).toBeInTheDocument();
},
);
it('renders the strip without a version when none is available', () => {
const { getByTestId } = render(<BottomStrip />, undefined, {
appContextOverrides: { versionData: null },
});
const strip = getByTestId('bottom-strip');
expect(strip).toBeInTheDocument();
expect(strip).toHaveTextContent('');
});
// `tests/test-utils` builds its wrapper around the first `ui`, so `rerender`
// re-renders the original tree. These drive change through state and through
// separate trees instead, which the module-level store lets them share.
describe('left slot', () => {
it('shows what the page put there instead of the version', () => {
render(<Page>600 traces</Page>);
const { getByText, queryByText } = render(
<BottomStrip />,
undefined,
withVersion,
);
expect(getByText('600 traces')).toBeInTheDocument();
expect(queryByText(VERSION)).not.toBeInTheDocument();
});
it('falls back to the version once the page is gone', () => {
const page = render(<Page>600 traces</Page>);
const { getByText } = render(<BottomStrip />, undefined, withVersion);
page.unmount();
expect(getByText(VERSION)).toBeInTheDocument();
});
it('updates when the page changes what it shows', () => {
render(<ChangingPage />);
const { getByText, getByRole } = render(
<BottomStrip />,
undefined,
withVersion,
);
expect(getByText('600 traces')).toBeInTheDocument();
fireEvent.click(getByRole('button', { name: 'change' }));
expect(getByText('42 traces')).toBeInTheDocument();
});
it('falls back to the version when the page node throws', () => {
// React logs the caught error, which is noise here.
const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
render(
<Page>
<Boom />
</Page>,
);
const { getByTestId, getByText } = render(
<BottomStrip />,
undefined,
withVersion,
);
expect(getByTestId('bottom-strip')).toBeInTheDocument();
expect(getByText(VERSION)).toBeInTheDocument();
consoleError.mockRestore();
});
it('keeps the new page value when the old page unmounts after it', () => {
// Navigation order: the next page mounts before the last one unmounts,
// so without the owner guard the outgoing page wipes the incoming value.
const pageA = render(<Page>page A</Page>);
render(<Page>page B</Page>);
const { getByText } = render(<BottomStrip />, undefined, withVersion);
pageA.unmount();
expect(getByText('page B')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,15 @@
.separator {
flex-shrink: 0;
width: 1px;
height: 14px;
background: var(--l2-border);
}
// Strip items hide themselves by rendering null, which would otherwise leave a
// separator dangling at an edge or two of them side by side. Collapsing them
// here keeps the call sites free of visibility plumbing.
.separator:first-child,
.separator:last-child,
.separator + .separator {
display: none;
}

View File

@@ -0,0 +1,7 @@
import styles from './StripSeparator.module.scss';
function StripSeparator(): JSX.Element {
return <span className={styles.separator} aria-hidden />;
}
export default StripSeparator;

View File

@@ -0,0 +1,10 @@
.stripTypography {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--l2-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -0,0 +1,34 @@
import type { ReactNode } from 'react';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import styles from './StripTypography.module.scss';
interface StripTypographyProps {
children: ReactNode;
/** Leading icon, aligned and spaced for you. Same shape as `Button`'s. */
prefix?: ReactNode;
className?: string;
}
/**
* Everything the strip renders goes through this: the version, a plain count, or
* an icon with a key and value. It owns the strip's type and alignment and
* nothing else — how a consumer colours its own content is theirs.
*/
function StripTypography({
children,
prefix,
className,
}: StripTypographyProps): JSX.Element {
return (
<span className={cx(styles.stripTypography, className)}>
{prefix}
<Typography.Text as="span">{children}</Typography.Text>
</span>
);
}
StripTypography.defaultProps = { prefix: undefined, className: undefined };
export default StripTypography;

View File

@@ -0,0 +1,43 @@
import { useLayoutEffect } from 'react';
import AskNoz from './AskNoz/AskNoz';
import LeftSlot from './LeftSlot/LeftSlot';
import StripSeparator from './components/StripSeparator/StripSeparator';
import SupportButton from './SupportButton/SupportButton';
import styles from './BottomStrip.module.scss';
export const BOTTOM_STRIP_HEIGHT = 24;
export const BOTTOM_STRIP_ON_CLASS = 'bottom-strip-on';
export const BOTTOM_STRIP_HEIGHT_VAR = '--bottom-strip-height';
function BottomStrip(): JSX.Element {
useLayoutEffect(() => {
document.body.classList.add(BOTTOM_STRIP_ON_CLASS);
document.body.style.setProperty(
BOTTOM_STRIP_HEIGHT_VAR,
`${BOTTOM_STRIP_HEIGHT}px`,
);
return (): void => {
document.body.classList.remove(BOTTOM_STRIP_ON_CLASS);
document.body.style.removeProperty(BOTTOM_STRIP_HEIGHT_VAR);
};
}, []);
return (
<div className={styles.strip} data-testid="bottom-strip">
<div className={styles.left}>
<LeftSlot />
</div>
<div className={styles.right}>
<AskNoz />
<StripSeparator />
<SupportButton />
</div>
</div>
);
}
export default BottomStrip;

View File

@@ -0,0 +1,26 @@
import type { ReactNode } from 'react';
import { create } from 'zustand';
interface BottomStripState {
/** What the strip shows on the left, or null to fall back to the version. */
left: ReactNode | null;
/** Which page owns the current value — see `clearLeft`. */
ownerId: string | null;
setLeft: (ownerId: string, left: ReactNode) => void;
clearLeft: (ownerId: string) => void;
}
export const useBottomStripStore = create<BottomStripState>()((set, get) => ({
left: null,
ownerId: null,
setLeft: (ownerId, left): void => set({ left, ownerId }),
// Only the current owner may clear. On a plain route swap React runs the old
// page's cleanup before the new page's effect, so this is moot, but two
// consumers can be mounted at once (a page under a drawer): the one that set
// last owns the slot, and the other unmounting must not wipe it.
clearLeft: (ownerId): void => {
if (get().ownerId === ownerId) {
set({ left: null, ownerId: null });
}
},
}));

View File

@@ -0,0 +1,23 @@
import { type ReactNode, useEffect, useId } from 'react';
import { useBottomStripStore } from './store/useBottomStripStore';
/**
* Puts `node` on the left of the bottom strip for as long as the calling page is
* mounted. Pass null to show nothing and let the version through.
*
* There is no refresh API by design: a page that refetches re-renders, which
* produces a new node, which re-runs this effect. Wrap the node in `useMemo`
* keyed on the values it shows, or the store is written on every render.
*/
export function useBottomStripLeft(node: ReactNode | null): void {
const ownerId = useId();
const setLeft = useBottomStripStore((state) => state.setLeft);
const clearLeft = useBottomStripStore((state) => state.clearLeft);
useEffect(() => {
setLeft(ownerId, node);
return (): void => clearLeft(ownerId);
}, [node, ownerId, setLeft, clearLeft]);
}

View File

@@ -1,6 +1,8 @@
.create-alert-v2-footer {
position: fixed;
bottom: 0;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: var(--bottom-strip-height, 0px);
left: 63px;
right: 0;
background-color: var(--l1-background);

View File

@@ -1,6 +1,8 @@
.explorer-options-container {
position: fixed;
bottom: 0px;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: var(--bottom-strip-height, 0px);
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;

View File

@@ -1,6 +1,8 @@
.explorer-option-droppable-container {
position: fixed;
bottom: 0;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: var(--bottom-strip-height, 0px);
width: -webkit-fill-available;
height: 24px;
display: flex;

View File

@@ -1,7 +1,6 @@
.home-container {
display: flex;
flex-direction: column;
min-height: 100vh;
overflow-y: auto;
height: 100%;
width: 100%;

View File

@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { Tabs } from 'antd';
import logEvent from 'api/common/logEvent';
import { useConfirmableAction } from 'hooks/useConfirmableAction';
import AttributeMappingActions from './components/AttributeMappingActions/AttributeMappingActions';
@@ -20,6 +21,10 @@ function LLMObservabilityAttributeMapping(): JSX.Element {
const groupDrawer = useGroupFormDrawer();
const spanTest = useTestSpanMapper(editor.snapshot, editor.groups);
useEffect(() => {
void logEvent('AI Observability Attribute Mapping: Page visited', {});
}, []);
const { discard } = editor;
// Discarding wipes the whole working copy, so gate it behind a confirm
// prompt rather than firing straight from the button.

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import logEvent from 'api/common/logEvent';
import {
RenderErrorResponseDTO,
SpantypesSpanMapperTestSpanDTO,
@@ -126,6 +127,7 @@ export function useTestSpanMapper(
{ data: body },
{
onSuccess: (response) => {
void logEvent('AI Observability Attribute Mapping: Test run', {});
setTestedAttributes(submittedAttributes);
setTestedResource(submittedResource);
setResult(response.data?.spans ?? []);

View File

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { cloneDeep, isEqual } from 'lodash-es';
import { toast } from '@signozhq/ui/sonner';
import { useQueryClient } from 'react-query';
import logEvent from 'api/common/logEvent';
import {
useCreateSpanMapper,
useCreateSpanMapperGroup,
@@ -262,6 +263,7 @@ export function useAttributeMappingEditor(): AttributeMappingEditor {
setSaveError(null);
try {
await persistDraft(snapshot, draft, mutations);
void logEvent('AI Observability Attribute Mapping: Changes saved', {});
// Refresh the groups list in place — it stays mounted, so this just
// swaps in fresh data without a loading flash. Using the query's own
// refetch keeps it scoped to the groups list; the per-group mapper

View File

@@ -65,8 +65,6 @@
}
.trace-explorer-page {
display: flex;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
@@ -75,32 +73,8 @@
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
.filter {
width: 260px;
height: 100%;
min-height: 100vh;
border-right: 0px;
border: 1px solid var(--l1-border);
background-color: var(--l1-background);
> .ant-card-body {
padding: 0;
width: 258px;
}
}
.trace-explorer {
width: 100%;
background: var(--l1-background);
> .ant-card-body {
padding: 0;
}
border-color: var(--l1-border);
}
.trace-explorer.filters-expanded {
width: calc(100% - 260px);
}
}

View File

@@ -2,12 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import WarningPopover from 'components/WarningPopover/WarningPopover';
@@ -173,7 +171,7 @@ function Explorer(): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current) {
logEvent('Traces Explorer: Page visited', {});
logEvent('AI Observability Explorer: Page visited', {});
logEventCalledRef.current = true;
}
}, []);
@@ -188,26 +186,21 @@ function Explorer(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div
<QuickFiltersLayout
className="trace-explorer-page"
data-testid="llm-observability-explorer"
testId="llm-observability-explorer"
showFilters={isOpen}
quickFilterProps={{
className: 'qf-traces-explorer',
source: QuickFiltersSource.AI_OBSERVABILITY,
signal: SignalType.AI_OBSERVABILITY,
useFieldApis: quickFiltersFieldApis,
handleFilterVisibilityChange: (): void => {
setOpen(!isOpen);
},
}}
>
<Card className="filter" hidden={!isOpen}>
<QuickFilters
className="qf-traces-explorer"
source={QuickFiltersSource.AI_OBSERVABILITY}
signal={SignalType.AI_OBSERVABILITY}
useFieldApis={quickFiltersFieldApis}
handleFilterVisibilityChange={(): void => {
setOpen(!isOpen);
}}
/>
</Card>
<div
className={cx('trace-explorer', {
'filters-expanded': isOpen,
})}
>
<div className="trace-explorer">
<div className="trace-explorer-header">
<Toolbar
showAutoRefresh
@@ -291,7 +284,7 @@ function Explorer(): JSX.Element {
)}
</div>
</div>
</div>
</QuickFiltersLayout>
</Sentry.ErrorBoundary>
);
}

View File

@@ -173,7 +173,7 @@ function ListView({
useEffect(() => {
if (!isLoading && !isFetching && !isError && rows.length !== 0) {
void logEvent('Traces Explorer: Data present', {
void logEvent('AI Observability Explorer: Data present', {
panelType,
});
}

View File

@@ -159,7 +159,7 @@ function TracesView({
useEffect(() => {
if (!isLoading && !isFetching && !isError && rows.length !== 0) {
void logEvent('Traces Explorer: Data present', {
void logEvent('AI Observability Explorer: Data present', {
panelType: 'TRACE',
});
}

View File

@@ -1,4 +1,6 @@
import { useEffect } from 'react';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import Spinner from 'components/Spinner';
import DashboardContainer from 'pages/DashboardPage/DashboardContainer';
@@ -8,6 +10,10 @@ import styles from './Overview.module.scss';
function Overview(): JSX.Element {
const { dashboard, isLoading, isError, error, refetch } = useSystemDashboard();
useEffect(() => {
void logEvent('AI Observability Overview: Page visited', {});
}, []);
const renderContent = (): JSX.Element => {
if (isLoading) {
return <Spinner tip="Loading dashboard..." />;

View File

@@ -1,5 +1,7 @@
import { useEffect } from 'react';
import { Badge } from '@signozhq/ui/badge';
import { Tabs } from 'antd';
import logEvent from 'api/common/logEvent';
import { useListUnmappedLLMModels } from 'api/generated/services/llmpricingrules';
import { parseAsStringEnum, useQueryState } from 'nuqs';
@@ -20,6 +22,10 @@ function LLMObservabilityModelPricing(): JSX.Element {
const { data } = useListUnmappedLLMModels();
const unpricedCount = data?.data?.items?.length ?? 0;
useEffect(() => {
void logEvent('AI Observability Model Pricing: Page visited', {});
}, []);
return (
<div
className={styles.llmObservabilityModelPricing}

View File

@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { useQueryClient } from 'react-query';
import logEvent from 'api/common/logEvent';
import {
getListLLMPricingRulesQueryKey,
getListUnmappedLLMModelsQueryKey,
@@ -94,6 +95,10 @@ export function useModelCostDrawer(): UseModelCostDrawerResult {
await createOrUpdate({
data: { rules: [buildRulePayload(draft)] },
});
void logEvent('AI Observability Model Pricing: Model cost saved', {
mode,
modelName: draft.modelName,
});
await invalidateList();
setIsOpen(false);
setSelectedRuleId(null);

View File

@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { useQueryClient } from 'react-query';
import logEvent from 'api/common/logEvent';
import {
getListLLMPricingRulesQueryKey,
useDeleteLLMPricingRule,
@@ -46,6 +47,9 @@ export function useModelCostDelete(): UseModelCostDeleteResult {
}
try {
await deleteRuleApi({ pathParams: { id: pendingDelete.id } });
void logEvent('AI Observability Model Pricing: Model cost deleted', {
modelName: pendingDelete.modelName,
});
await queryClient.invalidateQueries({
queryKey: getListLLMPricingRulesQueryKey(),
});

View File

@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { useQueryClient } from 'react-query';
import logEvent from 'api/common/logEvent';
import {
getListLLMPricingRulesQueryKey,
getListUnmappedLLMModelsQueryKey,
@@ -45,6 +46,10 @@ export function useUnpricedModelMapping(): UseUnpricedModelMappingResult {
setIsSaving(true);
try {
await createOrUpdate({ data: { rules: [payload] } });
void logEvent('AI Observability Model Pricing: Unpriced model mapped', {
modelName: model.modelName,
billingModelName: rule.modelName,
});
await Promise.all([
queryClient.invalidateQueries({
queryKey: getListUnmappedLLMModelsQueryKey(),

View File

@@ -1,7 +1,4 @@
.licenses-page {
max-height: 100vh;
overflow: hidden;
.licenses-page-header {
border-bottom: 1px solid var(--l1-border);
background: var(--l1-background);
@@ -32,7 +29,6 @@
.licenses-page-content {
flex: 1;
height: calc(100vh - 48px);
background: var(--l1-background);
padding: 10px 8px;
overflow-y: auto;

View File

@@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
gap: 1rem;
height: calc(100vh - 62px);
flex: 1;
min-height: 400px;
}

View File

@@ -1,18 +1,7 @@
.meter-explorer-container {
display: flex;
flex-direction: row;
.meter-explorer-quick-filters-section {
width: 280px;
border-right: 1px solid var(--l1-border);
&.hidden {
display: none;
}
}
.meter-explorer-content-section {
width: 100%;
// Clearance for the fixed ExplorerOptions bar.
padding-bottom: 80px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
@@ -83,14 +72,6 @@
}
}
}
&.quick-filters-open {
.meter-explorer-content-section {
width: calc(100% - 280px);
}
}
padding-bottom: 80px;
}
.dashboards-and-alerts-popover-container {

View File

@@ -3,9 +3,8 @@ import { useQueryClient } from 'react-query';
import * as Sentry from '@sentry/react';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
@@ -121,29 +120,21 @@ function Explorer(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div
className={cx('meter-explorer-container', {
'quick-filters-open': showQuickFilters,
})}
<QuickFiltersLayout
className="meter-explorer-container"
showFilters={showQuickFilters}
quickFilterProps={{
className: 'qf-meter-explorer',
source: QuickFiltersSource.METER_EXPLORER,
signal: SignalType.METER_EXPLORER,
showFilterCollapse: true,
showQueryName: false,
handleFilterVisibilityChange: (): void => {
setShowQuickFilters(!showQuickFilters);
},
useFieldApis: quickFilterFieldApis,
}}
>
<div
className={cx('meter-explorer-quick-filters-section', {
hidden: !showQuickFilters,
})}
>
<QuickFilters
className="qf-meter-explorer"
source={QuickFiltersSource.METER_EXPLORER}
signal={SignalType.METER_EXPLORER}
showFilterCollapse
showQueryName={false}
handleFilterVisibilityChange={(): void => {
setShowQuickFilters(!showQuickFilters);
}}
useFieldApis={quickFilterFieldApis}
/>
</div>
<div className="meter-explorer-content-section">
<div className="meter-explorer-explore-content">
<div className="explore-header">
@@ -196,7 +187,7 @@ function Explorer(): JSX.Element {
splitedQueries={splitedQueries}
/>
</div>
</div>
</QuickFiltersLayout>
</Sentry.ErrorBoundary>
);
}

View File

@@ -60,9 +60,6 @@
.metrics-table-container {
padding-bottom: 48px;
.ant-table {
margin-left: -16px;
margin-right: -16px;
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
@@ -184,7 +181,9 @@
.ant-pagination {
position: fixed;
bottom: 0;
// Lifted above the bottom strip. Don't extend this pattern — new
// fixed-bottom UI belongs in the bounded layout, not in another offset here.
bottom: var(--bottom-strip-height, 0px);
width: calc(100% - 54px);
background: var(--l1-background);
padding: 16px;

View File

@@ -4,11 +4,13 @@ import amazonMskUrl from '@/assets/Logos/amazon-msk.svg';
import androidJavaMonitoringUrl from '@/assets/Logos/android-java-monitoring.svg';
import androidKotlinMonitoringUrl from '@/assets/Logos/android-kotlin-monitoring.svg';
import anthropicApiMonitoringUrl from '@/assets/Logos/anthropic-api-monitoring.svg';
import antigravityUrl from '@/assets/Logos/antigravity.svg';
import apacheDruidUrl from '@/assets/Logos/apache-druid.svg';
import apacheUrl from '@/assets/Logos/apache.svg';
import apiGatewayUrl from '@/assets/Logos/api-gateway.svg';
import argocdUrl from '@/assets/Logos/argocd.svg';
import aspnetUrl from '@/assets/Logos/aspnet.svg';
import auroraUrl from '@/assets/Logos/aurora.svg';
import auth0Url from '@/assets/Logos/auth0.svg';
import autogenUrl from '@/assets/Logos/autogen.svg';
import awsAlbUrl from '@/assets/Logos/aws-alb.svg';
@@ -28,6 +30,7 @@ import celeryUrl from '@/assets/Logos/celery.svg';
import certManagerUrl from '@/assets/Logos/cert-manager.svg';
import claudeCodeUrl from '@/assets/Logos/claude-code.svg';
import clickhouseUrl from '@/assets/Logos/clickhouse.svg';
import clineUrl from '@/assets/Logos/cline.svg';
import cloudflareUrl from '@/assets/Logos/cloudflare.svg';
import cloudnativePgUrl from '@/assets/Logos/cloudnative-pg.svg';
import cloudwatchLogsUrl from '@/assets/Logos/cloudwatch-logs.svg';
@@ -36,8 +39,10 @@ import confluentKafkaUrl from '@/assets/Logos/confluent-kafka.svg';
import convexLogoUrl from '@/assets/Logos/convex-logo.svg';
import cppUrl from '@/assets/Logos/cpp.svg';
import crewAiUrl from '@/assets/Logos/crew-ai.svg';
import cursorUrl from '@/assets/Logos/cursor.svg';
import dashboardsUrl from '@/assets/Logos/dashboards.svg';
import datadogUrl from '@/assets/Logos/datadog.svg';
import daytonaUrl from '@/assets/Logos/daytona.svg';
import dbosUrl from '@/assets/Logos/dbos.svg';
import deepseekUrl from '@/assets/Logos/deepseek.svg';
import denoUrl from '@/assets/Logos/deno.svg';
@@ -47,6 +52,7 @@ import documentLoadUrl from '@/assets/Logos/document-load.svg';
import dotnetUrl from '@/assets/Logos/dotnet.svg';
import dspyUrl from '@/assets/Logos/dspy.svg';
import dynamodbUrl from '@/assets/Logos/dynamodb.svg';
import e2bUrl from '@/assets/Logos/e2b.svg';
import ec2Url from '@/assets/Logos/ec2.svg';
import ecsUrl from '@/assets/Logos/ecs.svg';
import eksUrl from '@/assets/Logos/eks.svg';
@@ -55,6 +61,7 @@ import elasticsearchUrl from '@/assets/Logos/elasticsearch.svg';
import elbUrl from '@/assets/Logos/elb.svg';
import elixirUrl from '@/assets/Logos/elixir.svg';
import elkUrl from '@/assets/Logos/elk.svg';
import emqxUrl from '@/assets/Logos/emqx.svg';
import envoyUrl from '@/assets/Logos/envoy.svg';
import externalApiMonitoringUrl from '@/assets/Logos/external-api-monitoring.svg';
import fluentbitUrl from '@/assets/Logos/fluentbit.svg';
@@ -101,6 +108,7 @@ import javascriptUrl from '@/assets/Logos/javascript.svg';
import jbossUrl from '@/assets/Logos/jboss.svg';
import jenkinsUrl from '@/assets/Logos/jenkins.svg';
import kafkaUrl from '@/assets/Logos/kafka.svg';
import karpenterUrl from '@/assets/Logos/karpenter.svg';
import kedaUrl from '@/assets/Logos/keda.svg';
import kongUrl from '@/assets/Logos/kong.svg';
import kubernetesUrl from '@/assets/Logos/kubernetes.svg';
@@ -116,9 +124,11 @@ import logrusUrl from '@/assets/Logos/logrus.svg';
import logsUrl from '@/assets/Logos/logs.svg';
import logstashUrl from '@/assets/Logos/logstash.svg';
import mastraUrl from '@/assets/Logos/mastra.svg';
import metaUrl from '@/assets/Logos/meta.svg';
import metricsUrl from '@/assets/Logos/metrics.svg';
import microsoftSqlServerUrl from '@/assets/Logos/microsoft-sql-server.svg';
import mistralUrl from '@/assets/Logos/mistral.svg';
import modalUrl from '@/assets/Logos/modal.svg';
import mongoUrl from '@/assets/Logos/mongo.svg';
import n8nUrl from '@/assets/Logos/n8n.svg';
import neonUrl from '@/assets/Logos/neon.svg';
@@ -141,6 +151,7 @@ import phpUrl from '@/assets/Logos/php.svg';
import pinoUrl from '@/assets/Logos/pino.svg';
import pipecatUrl from '@/assets/Logos/pipecat.svg';
import planetscaleUrl from '@/assets/Logos/planetscale.svg';
import podmanUrl from '@/assets/Logos/podman.svg';
import postgresqlUrl from '@/assets/Logos/postgresql.svg';
import prometheusUrl from '@/assets/Logos/prometheus.svg';
import pydanticAiUrl from '@/assets/Logos/pydantic-ai.svg';
@@ -158,6 +169,7 @@ import rubyOnRailsUrl from '@/assets/Logos/ruby-on-rails.svg';
import rustUrl from '@/assets/Logos/rust.svg';
import s3Url from '@/assets/Logos/s3.svg';
import scalaUrl from '@/assets/Logos/scala.svg';
import sglangUrl from '@/assets/Logos/sglang.svg';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
import slogUrl from '@/assets/Logos/slog.svg';
import slurmUrl from '@/assets/Logos/slurm.svg';
@@ -179,6 +191,7 @@ import traceloopUrl from '@/assets/Logos/traceloop.svg';
import tracesUrl from '@/assets/Logos/traces.svg';
import vectorUrl from '@/assets/Logos/vector.svg';
import vercelUrl from '@/assets/Logos/vercel.svg';
import vllmUrl from '@/assets/Logos/vllm.svg';
import vmUrl from '@/assets/Logos/vm.svg';
import vpcUrl from '@/assets/Logos/vpc.svg';
import windowsUrl from '@/assets/Logos/windows.svg';
@@ -1014,35 +1027,64 @@ const onboardingConfigWithLinks = [
'tracing',
],
id: 'golang',
link: '/docs/instrumentation/opentelemetry-golang/',
link: '/docs/instrumentation/golang/overview/',
question: {
desc: 'What is your Environment?',
desc: 'How do you want to instrument your application?',
helpText:
'The SDK needs code changes. Compile-time instrumentation rebuilds the binary with otelc. eBPF attaches to a running binary and needs neither.',
helpLink: '/docs/instrumentation/golang/overview/',
helpLinkText: 'Compare the options →',
type: 'select',
entityID: 'environment',
entityID: 'framework',
options: [
{
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
key: 'golang-sdk',
label: 'OpenTelemetry SDK',
imgUrl: goUrl,
link: '/docs/instrumentation/opentelemetry-golang/',
question: {
desc: 'What is your Environment?',
type: 'select',
entityID: 'environment',
options: [
{
key: 'vm',
label: 'VM',
imgUrl: vmUrl,
link: '/docs/instrumentation/opentelemetry-golang/',
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/opentelemetry-golang/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/opentelemetry-golang/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/opentelemetry-golang/',
},
],
},
},
{
key: 'k8s',
label: 'Kubernetes',
imgUrl: kubernetesUrl,
link: '/docs/instrumentation/opentelemetry-golang/',
key: 'golang-compile-time',
label: 'Compile-Time (otelc)',
imgUrl: goUrl,
link: '/docs/instrumentation/golang/compile-time-instrumentation/',
},
{
key: 'windows',
label: 'Windows',
imgUrl: windowsUrl,
link: '/docs/instrumentation/opentelemetry-golang/',
},
{
key: 'docker',
label: 'Docker',
imgUrl: dockerUrl,
link: '/docs/instrumentation/opentelemetry-golang/',
key: 'golang-ebpf',
label: 'eBPF',
imgUrl: opentelemetryUrl,
link: '/docs/instrumentation/golang/ebpf-instrumentation/',
},
],
},
@@ -2868,22 +2910,28 @@ const onboardingConfigWithLinks = [
imgUrl: lambdaUrl,
link: '/docs/aws-monitoring/lambda/lambda-traces/',
question: {
desc: 'Which runtime does your Lambda function use?',
desc: 'How is your Lambda function packaged?',
helpText:
'Python, Node.js, Java, and Ruby use the OpenTelemetry auto-instrumentation layer. Go has no layer, so you add the SDK to your code.',
'Zip functions use the OpenTelemetry auto-instrumentation layer, except Go, which has no layer and needs the SDK in your code. Layers do not attach to container images, so those bake instrumentation into the image.',
options: [
{
key: 'aws-lambda-traces-auto',
label: 'Python, Node.js, Java, Ruby',
label: 'Zip - Python, Node.js, Java, Ruby',
imgUrl: lambdaUrl,
link: '/docs/aws-monitoring/lambda/lambda-traces/',
},
{
key: 'aws-lambda-traces-golang',
label: 'Go',
label: 'Zip - Go',
imgUrl: goUrl,
link: '/docs/aws-monitoring/lambda/lambda-traces-golang/',
},
{
key: 'aws-lambda-traces-container-image',
label: 'Container Image',
imgUrl: dockerUrl,
link: '/docs/aws-monitoring/lambda/lambda-traces-container-image/',
},
],
},
},
@@ -7489,5 +7537,455 @@ const onboardingConfigWithLinks = [
id: 'mysql-logs',
link: '/docs/integrations/opentelemetry-mysql/',
},
{
dataSource: 'cursor',
label: 'Cursor',
imgUrl: cursorUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'ai coding assistant',
'cursor',
'cursor ai',
'cursor observability',
'cursor traces',
'ide',
'llm',
'llm monitoring',
'monitoring',
'observability',
'token usage',
'traces',
'tracing',
],
id: 'cursor',
link: '/docs/cursor-observability/',
},
{
dataSource: 'antigravity-cli',
label: 'Antigravity CLI',
imgUrl: antigravityUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'ai coding assistant',
'antigravity',
'antigravity cli',
'cli',
'google antigravity',
'llm',
'llm monitoring',
'monitoring',
'observability',
'quota',
'tool calls',
'traces',
'tracing',
],
id: 'antigravity-cli',
link: '/docs/antigravity-cli-monitoring/',
},
{
dataSource: 'qwen-code',
label: 'Qwen Code',
imgUrl: qwenUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'ai coding assistant',
'cli',
'llm',
'llm monitoring',
'monitoring',
'observability',
'qwen',
'qwen code',
'qwen code cli',
'token spend',
'traces',
'tracing',
],
id: 'qwen-code',
link: '/docs/qwen-code-observability/',
},
{
dataSource: 'deepseek-harness',
label: 'DeepSeek Harness',
imgUrl: deepseekUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'ai coding assistant',
'deepseek',
'deepseek harness',
'dsh',
'llm',
'llm monitoring',
'monitoring',
'observability',
'token spend',
'traces',
'tracing',
],
id: 'deepseek-harness',
link: '/docs/deepseek-harness-observability/',
},
{
dataSource: 'muse-spark',
label: 'Meta Muse Spark',
imgUrl: metaUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'llm',
'llm monitoring',
'meta',
'meta model api',
'monitoring',
'muse',
'muse spark',
'observability',
'prompt cache',
'reasoning tokens',
'traces',
'tracing',
],
id: 'muse-spark',
link: '/docs/muse-spark-monitoring/',
},
{
dataSource: 'openai-agents-sdk',
label: 'OpenAI Agents SDK',
imgUrl: openaiUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'agents sdk',
'handoffs',
'llm',
'llm monitoring',
'monitoring',
'observability',
'openai',
'openai agents sdk',
'tool calls',
'traces',
'tracing',
],
id: 'openai-agents-sdk',
link: '/docs/openai-agents-sdk-observability/',
},
{
dataSource: 'e2b',
label: 'E2B Sandbox',
imgUrl: e2bUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'code execution',
'e2b',
'e2b sandbox',
'llm',
'llm monitoring',
'monitoring',
'observability',
'sandbox',
'traces',
'tracing',
],
id: 'e2b',
link: '/docs/e2b-monitoring/',
},
{
dataSource: 'daytona',
label: 'Daytona Sandbox',
imgUrl: daytonaUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'code execution',
'daytona',
'daytona sandbox',
'llm',
'llm monitoring',
'monitoring',
'observability',
'sandbox',
'traces',
'tracing',
],
id: 'daytona',
link: '/docs/daytona-monitoring/',
},
{
dataSource: 'vercel-sandbox',
label: 'Vercel Sandbox',
imgUrl: vercelUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'code execution',
'llm',
'llm monitoring',
'monitoring',
'observability',
'sandbox',
'traces',
'tracing',
'vercel',
'vercel sandbox',
],
id: 'vercel-sandbox',
link: '/docs/vercel-sandbox-observability/',
},
{
dataSource: 'modal',
label: 'Modal',
imgUrl: modalUrl,
tags: ['LLM Monitoring'],
module: 'apm',
relatedSearchKeywords: [
'agent',
'container metrics',
'llm',
'llm monitoring',
'modal',
'modal functions',
'modal sandbox',
'monitoring',
'observability',
'sandbox',
'serverless',
'traces',
'tracing',
],
id: 'modal',
link: '/docs/integrations/modal/',
},
{
dataSource: 'cline',
label: 'Cline',
imgUrl: clineUrl,
tags: ['LLM Monitoring'],
module: 'metrics',
relatedSearchKeywords: [
'ai coding assistant',
'cline',
'cline monitoring',
'cost per model',
'llm',
'llm monitoring',
'metrics',
'monitoring',
'observability',
'token usage',
'vs code',
],
id: 'cline',
link: '/docs/cline-monitoring/',
},
{
dataSource: 'muse-code',
label: 'Meta Muse Code',
imgUrl: metaUrl,
tags: ['LLM Monitoring'],
module: 'metrics',
relatedSearchKeywords: [
'agent',
'ai coding assistant',
'cli',
'llm',
'llm monitoring',
'meta',
'metrics',
'monitoring',
'muse',
'muse code',
'observability',
'prompt cache',
'token usage',
],
id: 'muse-code',
link: '/docs/muse-code-monitoring/',
},
{
dataSource: 'vllm',
label: 'vLLM',
imgUrl: vllmUrl,
tags: ['LLM Monitoring'],
module: 'metrics',
relatedSearchKeywords: [
'inference server',
'kv cache',
'llm',
'llm monitoring',
'metrics',
'monitoring',
'observability',
'time to first token',
'token throughput',
'vllm',
],
id: 'vllm',
link: '/docs/vllm-observability/',
},
{
dataSource: 'sglang',
label: 'SGLang',
imgUrl: sglangUrl,
tags: ['LLM Monitoring'],
module: 'metrics',
relatedSearchKeywords: [
'inference server',
'kv cache',
'llm',
'llm monitoring',
'metrics',
'monitoring',
'observability',
'sglang',
'time to first token',
'token throughput',
],
id: 'sglang',
link: '/docs/sglang-observability/',
},
{
dataSource: 'karpenter',
label: 'Karpenter',
imgUrl: karpenterUrl,
tags: ['infrastructure monitoring', 'metrics'],
module: 'metrics',
relatedSearchKeywords: [
'autoscaling',
'disruption',
'karpenter',
'karpenter metrics',
'kubernetes',
'metrics',
'monitoring',
'node provisioning',
'observability',
'opentelemetry karpenter',
'prometheus',
],
id: 'karpenter',
link: '/docs/metrics-management/opentelemetry-karpenter-metrics/',
},
{
dataSource: 'podman',
label: 'Podman',
imgUrl: podmanUrl,
tags: ['infrastructure monitoring', 'metrics'],
module: 'metrics',
relatedSearchKeywords: [
'container metrics',
'containers',
'metrics',
'monitoring',
'observability',
'opentelemetry podman',
'podman',
'podman metrics',
],
id: 'podman',
link: '/docs/metrics-management/opentelemetry-podman-metrics/',
},
{
dataSource: 'emqx',
label: 'EMQX',
imgUrl: emqxUrl,
tags: ['Messaging Queues', 'metrics'],
module: 'metrics',
relatedSearchKeywords: [
'broker',
'emqx',
'emqx metrics',
'iot',
'messaging queues',
'metrics',
'monitoring',
'mqtt',
'observability',
'opentelemetry emqx',
],
id: 'emqx',
link: '/docs/integrations/opentelemetry-emqx/',
},
{
dataSource: 'aws-rds-aurora',
label: 'AWS RDS Aurora',
imgUrl: auroraUrl,
tags: ['AWS', 'database'],
module: 'metrics',
relatedSearchKeywords: [
'aurora',
'aurora mysql',
'aurora postgresql',
'aws',
'cloudwatch',
'database',
'metrics',
'monitoring',
'observability',
'rds',
'rds aurora',
],
id: 'aws-rds-aurora',
link: '/docs/integrations/aws-rds-aurora/',
},
{
dataSource: 'gcp-cloud-storage',
label: 'GCP Cloud Storage',
imgUrl: gcpCloudStorageUrl,
tags: ['GCP'],
module: 'dashboards',
relatedSearchKeywords: [
'bucket',
'cloud storage',
'gcp',
'gcp cloud storage',
'gcp integration',
'gcs',
'google cloud',
'metrics',
'monitoring',
'object storage',
'observability',
],
id: 'gcp-cloud-storage',
link: '/integrations/gcp?service=cloudstorage',
internalRedirect: true,
},
{
dataSource: 'aws-lambda-microvms',
label: 'AWS Lambda MicroVMs',
imgUrl: lambdaUrl,
tags: ['AWS'],
module: 'metrics',
relatedSearchKeywords: [
'aws',
'lambda',
'lambda microvms',
'logs',
'metrics',
'microvm',
'monitoring',
'observability',
'opentelemetry collector',
'serverless',
'traces',
],
id: 'aws-lambda-microvms',
link: '/docs/aws-monitoring/lambda-microvms/',
},
];
export default onboardingConfigWithLinks;

View File

@@ -114,8 +114,4 @@
}
}
}
.beta-tag {
padding-right: 0;
}
}

View File

@@ -51,7 +51,7 @@ export default function NavItem({
data-testid={dataTestId}
>
{showIcon && <div className="nav-item-active-marker" />}
<div className={cx('nav-item-data', isBeta ? 'beta-tag' : '')}>
<div className="nav-item-data">
{showIcon && (
<div className={cx('nav-item-icon', isEarlyAccess ? 'noz-wave' : '')}>
{icon}

View File

@@ -47,7 +47,6 @@ import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useIsAIObservabilityEnabled } from 'hooks/useIsAIObservabilityEnabled';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isArray } from 'lodash-es';
@@ -255,7 +254,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isAdmin = user.role === USER_ROLES.ADMIN;
const isEditor = user.role === USER_ROLES.EDITOR;
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isAIObservabilityEnabled = useIsAIObservabilityEnabled();
const aiAssistantActiveConversationId = useAIAssistantStore(
(s) => s.activeConversationId,
);
@@ -295,9 +293,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
if (item.key === ROUTES.INTEGRATIONS) {
return shouldShowIntegrationsValue;
}
if (item.key === ROUTES.AI_OBSERVABILITY_OVERVIEW) {
return isAIObservabilityEnabled;
}
return item.isEnabled;
};
@@ -314,7 +309,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
isEnterpriseSelfHostedUser,
isAdmin,
isEditor,
isAIObservabilityEnabled,
]);
// Track if we've done the initial sync (to avoid overwriting user actions during session)

View File

@@ -293,9 +293,7 @@ export const defaultMoreMenuItems: SidebarItem[] = [
label: 'AI Observability',
icon: <Brain size={16} />,
isBeta: true,
// Gated behind the `enable_ai_observability` feature flag in
// SideNav's `computedSecondaryMenuItems`; disabled by default.
isEnabled: false,
isEnabled: true,
itemKey: 'ai-observability',
},
{

View File

@@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
gap: 1rem;
height: calc(100vh - 62px);
flex: 1;
min-height: 400px;
padding-top: var(--spacing-8);
}

View File

@@ -1,7 +1,4 @@
.version-container {
max-height: 100vh;
overflow: hidden;
.version-page-header {
border-bottom: 1px solid var(--l1-border);
background: var(--l1-background);

View File

@@ -0,0 +1,148 @@
import { renderHook } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { useAppContext } from 'providers/App/App';
jest.mock('providers/App/App');
jest.mock('hooks/useGetTenantLicense');
const mockAppContext = useAppContext as jest.MockedFunction<
typeof useAppContext
>;
const mockLicense = useGetTenantLicense as jest.MockedFunction<
typeof useGetTenantLicense
>;
const flag = (name: FeatureKeys, active: boolean): Record<string, unknown> => ({
name,
active,
usage: 0,
usage_limit: -1,
route: '',
});
function setup({
chatSupport = true,
premiumSupport = false,
trialConverted = false,
isCloudUser = true,
isEnterpriseSelfHostedUser = false,
isLoggedIn = true,
pylonEnabled = true,
isFetchingFeatureFlags = false,
activeLicense = {} as unknown,
trialInfo = {} as unknown,
} = {}): void {
window.signozBootData = {
settings: { pylon: { enabled: pylonEnabled } },
} as never;
mockAppContext.mockReturnValue({
featureFlags: [
flag(FeatureKeys.CHAT_SUPPORT, chatSupport),
flag(FeatureKeys.PREMIUM_SUPPORT, premiumSupport),
],
isFetchingFeatureFlags,
featureFlagsFetchError: null,
trialInfo: trialInfo && { trialConvertedToSubscription: trialConverted },
isLoggedIn,
activeLicense,
} as never);
mockLicense.mockReturnValue({
isCloudUser,
isEnterpriseSelfHostedUser,
} as never);
}
const state = (): ChatSupportState =>
renderHook(() => useChatSupport()).result.current;
describe('useChatSupport', () => {
beforeEach(() => jest.clearAllMocks());
describe('pylon', () => {
it('hands off to Pylon for a cloud user past trial', () => {
setup({ trialConverted: true });
expect(state()).toBe(ChatSupportState.Pylon);
});
it('hands off to Pylon for enterprise self-hosted', () => {
setup({
trialConverted: true,
isCloudUser: false,
isEnterpriseSelfHostedUser: true,
});
expect(state()).toBe(ChatSupportState.Pylon);
});
it('hands off to Pylon when premium support is on, card or not', () => {
setup({ premiumSupport: true, trialConverted: false });
expect(state()).toBe(ChatSupportState.Pylon);
});
it('offers nothing when Pylon is not configured server side', () => {
setup({ trialConverted: true, pylonEnabled: false });
expect(state()).toBe(ChatSupportState.Unavailable);
});
});
describe('needsCard', () => {
it('offers the card flow to a cloud user still on trial', () => {
setup({ trialConverted: false, premiumSupport: false });
expect(state()).toBe(ChatSupportState.NeedsCard);
});
it('offers nothing to a non-cloud user needing a card', () => {
setup({
trialConverted: false,
isCloudUser: false,
isEnterpriseSelfHostedUser: true,
});
expect(state()).toBe(ChatSupportState.Unavailable);
});
});
describe('unavailable', () => {
it('offers nothing without the chat support flag', () => {
setup({ chatSupport: false });
expect(state()).toBe(ChatSupportState.Unavailable);
});
it('offers nothing when logged out', () => {
setup({ isLoggedIn: false });
expect(state()).toBe(ChatSupportState.Unavailable);
});
it('offers nothing while the flags are still loading', () => {
setup({ isFetchingFeatureFlags: true });
expect(state()).toBe(ChatSupportState.Unavailable);
});
it('offers nothing before the licence has loaded', () => {
setup({ activeLicense: null });
expect(state()).toBe(ChatSupportState.Unavailable);
});
it('offers nothing on a tenant that is neither cloud nor enterprise', () => {
setup({
trialConverted: true,
isCloudUser: false,
isEnterpriseSelfHostedUser: false,
});
expect(state()).toBe(ChatSupportState.Unavailable);
});
});
});

View File

@@ -0,0 +1,70 @@
import { FeatureKeys } from 'constants/features';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useAppContext } from 'providers/App/App';
import { useMemo } from 'react';
export enum ChatSupportState {
/** Pylon is configured for this user — hand off to the widget. */
Pylon = 'pylon',
/** Trial without a card — offer the Add Credit Card flow instead. */
NeedsCard = 'needsCard',
/** No support entry at all. */
Unavailable = 'unavailable',
}
export function useChatSupport(): ChatSupportState {
const {
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
trialInfo,
isLoggedIn,
activeLicense,
} = useAppContext();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
return useMemo(() => {
const isReady =
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
activeLicense &&
trialInfo;
if (!isReady || !isLoggedIn) {
return ChatSupportState.Unavailable;
}
const flag = (name: FeatureKeys): boolean =>
featureFlags?.find((f) => f.name === name)?.active || false;
if (!flag(FeatureKeys.CHAT_SUPPORT)) {
return ChatSupportState.Unavailable;
}
const needsCard =
!flag(FeatureKeys.PREMIUM_SUPPORT) &&
!trialInfo?.trialConvertedToSubscription;
if (needsCard) {
// The credit card flow is cloud-only
return isCloudUser
? ChatSupportState.NeedsCard
: ChatSupportState.Unavailable;
}
const pylonConfigured = Boolean(
window.signozBootData?.settings?.pylon?.enabled,
);
return (isCloudUser || isEnterpriseSelfHostedUser) && pylonConfigured
? ChatSupportState.Pylon
: ChatSupportState.Unavailable;
}, [
activeLicense,
featureFlags,
featureFlagsFetchError,
isCloudUser,
isEnterpriseSelfHostedUser,
isFetchingFeatureFlags,
isLoggedIn,
trialInfo,
]);
}

View File

@@ -1,11 +0,0 @@
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
export function useIsAIObservabilityEnabled(): boolean {
const { featureFlags } = useAppContext();
return (
featureFlags?.find(
(flag) => flag.name === FeatureKeys.ENABLE_AI_OBSERVABILITY,
)?.active || false
);
}

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