Compare commits

..

2 Commits

Author SHA1 Message Date
srikanthccv
fd15b85dde chore(tests): name the query builders by what they build and make the request type explicit
build_scalar_query built a builder query envelope and did not decide the
request type; make_query_request did, with a silent time_series default.

Assisted-by: Claude Fable 5.1
2026-11-10 18:40:46 +05:30
Tushar Vats
f02b5521c6 feat(querier): heatmaps for the logs and traces signals (#13318)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

Extends the metrics heatmap to logs, traces and audit logs, so a heatmap
can be built ad hoc from raw events with any filter instead of from a
pre-generated metric.

- The aggregation is the bare field to bucket, e.g. `duration_nano`.
Each cell counts events. Any other aggregation, such as `count()`, is
rejected, and a query takes one aggregation.
- Rows without the field are excluded by default: `<field> EXISTS` is
ANDed onto the filter, so it also applies to the resource filter and the
group limit. A present but non-numeric value counts as 0.
- `bucketOptions` is optional and defaults to the log axis, as for
metrics. Distribution requests work the same way.
- The heatmap is the time series query with the value bucket joining the
group keys, so a row is one cell and the response shape is unchanged.
Results are cached, and a cached axis is trimmed to the requested
window.
- The bucket expressions move to `pkg/querybuilder/heatmap.go`, shared
with metrics. The value is read once through a `WITH` alias instead of
being repeated in every bound. The metrics SQL is unchanged.
- A disabled query in a heatmap request feeds a formula, so it builds as
a time series, as metrics already did.
- A field nothing carries draws an empty chart with the key-not-found
warning.

#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/338

#### Additional Information

- No API schema change.
- Integration tests (`querierlogs/17_heatmap.py`,
`queriertraces/16_heatmap.py`, `auditquerier/02_heatmap.py`) cover
linear, log and default axes, group by, rows without the field, and the
rejections.
- Backend only. Dashboards first, explorers later.
2026-11-10 09:05:04 +00:00
1332 changed files with 15550 additions and 20326 deletions

View File

@@ -8,5 +8,5 @@
.title {
color: var(--l1-foreground);
font-size: var(--label-large-400-font-size);
font-size: var(--font-size-lg);
}

View File

@@ -497,11 +497,7 @@
// Requires final else in if-else-if chains (was disabled)
"signoz/no-conditional-text-nodes-with-siblings": "warn",
// Vendored from eslint-plugin-react-google-translate
"signoz/no-return-text-nodes": "warn",
"signoz/no-text-css": "warn",
// Font and text CSS in style props, CSSProperties objects and styled-components; text style goes through Typography props
"signoz/require-disable-reason": "error"
// A disable of signoz/no-text-css must say why Typography cannot render the text
"signoz/no-return-text-nodes": "warn"
},
"ignorePatterns": [
"src/parser/*.ts",

View File

@@ -45,8 +45,8 @@ const mockAliases = [
},
{
// jest: not replaced, a test asserts on the toast it fires.
find: /^@signozhq\/ui\/toast$/,
replacement: `${srcPath}/storybook/mocks/toast.mock.tsx`,
find: /^@signozhq\/ui\/sonner$/,
replacement: `${srcPath}/storybook/mocks/sonner.mock.tsx`,
},
];

View File

@@ -1,6 +1,8 @@
<!--
Every Google Fonts request the app makes is answered with an empty
stylesheet; the local faces stand in for them.
`index.html` links Inter from Google Fonts here. The link is parsed before the
msw worker has started, so that one request escapes the iframe while every
other font request the app makes is answered with an empty stylesheet; the
local faces stand in for both.
-->
<link rel="stylesheet" href="storybook-fonts.css" />

View File

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

View File

@@ -14,23 +14,9 @@ module.exports = {
path.join(__dirname, 'stylelint-rules/css-modules/class-name-pattern.js'),
],
customSyntax: 'postcss-scss',
// Disables of property-disallowed-list must say why after "--"
reportDescriptionlessDisables: [
false,
{ except: ['property-disallowed-list'], severity: 'error' },
],
rules: {
// Applies to all SCSS files
'local/no-unsupported-asset-url': true,
// Text style goes through @signozhq/ui/typography props
'property-disallowed-list': [
['/^font/', '/^text-/', 'line-height', 'letter-spacing'],
{
severity: 'warning',
message: (property) =>
`Do not set "${property}" in CSS. Render the text with @signozhq/ui/typography and use its props, or disable this rule with a reason after "--" saying why Typography cannot.`,
},
],
},
overrides: [
{

View File

@@ -191,30 +191,34 @@ You can read the ./node_modules/@signozhq/design-tokens/dist/style.css to find c
### Typography
Text styles are tokens named `--{variant}-{size}-{weight}-{property}`:
- Variant: `label` (line height 1), `paragraph` (line height 18, 20 or 26px), `uppercase` (11 and 13px) or `code` (SF Mono, 11 and 13px).
- Size: `small` (11px), `base` (13px), `medium` (16px) or `large` (18px). Paragraph has no `large`; uppercase and code have only `small` and `base`.
- Weight: `400`, `500` or `600`.
- Property: `font-family`, `font-size`, `font-weight`, `line-height`, `letter-spacing` or `text-transform`.
```scss
// Font sizes (preferred)
.title {
font-family: var(--label-large-500-font-family);
font-size: var(--label-large-500-font-size);
font-weight: var(--label-large-500-font-weight);
line-height: var(--label-large-500-line-height);
letter-spacing: var(--label-large-500-letter-spacing);
font-size: var(--periscope-font-size-large); // 18px
font-size: var(--periscope-font-size-medium); // 16px
font-size: var(--periscope-font-size-base); // 13px
font-size: var(--periscope-font-size-small); // 11px
}
.hint {
font-size: var(--paragraph-small-400-font-size); // 11px
line-height: var(--paragraph-small-400-line-height); // 18px
// Alternative scale (rem-based)
.heading {
font-size: var(--font-size-xl); // 1.25rem
font-size: var(--font-size-lg); // 1.125rem
font-size: var(--font-size-base); // 1rem
font-size: var(--font-size-sm); // 0.875rem
}
.id {
font-family: var(--code-base-400-font-family);
font-size: var(--code-base-400-font-size); // 13px
// Font weights
.bold {
font-weight: var(--font-weight-semibold); // 600
font-weight: var(--font-weight-medium); // 500
font-weight: var(--font-weight-normal); // 400
}
// Line heights
.text {
line-height: var(--line-height-20); // 20px
line-height: var(--line-height-24); // 24px
}
```
@@ -308,7 +312,7 @@ Each component exposes `--<component>-<property>` variables:
--button-background: var(--success-background);
--button-border-radius: var(--radius-2);
--button-padding: var(--spacing-4) var(--spacing-8);
--button-font-size: var(--label-base-400-font-size);
--button-font-size: var(--periscope-font-size-base);
}
// Override Input
@@ -338,7 +342,7 @@ Components use data attributes for variants/states. Target them for state-specif
// Target size
.wrapper :global([data-size="sm"]) {
--button-font-size: var(--label-small-400-font-size);
--button-font-size: var(--periscope-font-size-small);
}
// Target color

View File

@@ -1,191 +0,0 @@
# Popover migration analysis
The new `@signozhq/ui` Popover is click-only. This note maps the current app onto that API so a later migration does not break open state, focus, or the global `.ant-popover` overrides. It does not change any call site.
The component this describes lives in the design-system repo (`packages/ui/src/popover`). Storybook stories for it are `Playground`, `SaveViewForm`, `ControlledOpen`, `InsideDialog`, `MaxHeightScrolling`, and `AnchoredTypeahead`.
## The API to migrate onto
```tsx
<Popover
nativeButton
side="bottom"
align="start"
trigger={<button type="button">Open</button>}
open={open}
onOpenChange={setOpen}
>
{content}
</Popover>
```
| Today | New Popover |
| --- | --- |
| antd `children` (the trigger) | `trigger` (required) |
| antd `content`, or Radix `<PopoverContent>` | `children` |
| antd `trigger="click"` | the only mode; there is no `openOn` |
| antd `trigger="hover"` | not this component; a Tooltip, or a click popover when the body is interactive |
| `placement="bottomRight"` | `side="bottom"` `align="end"` |
| `placement="bottomLeft"` | `side="bottom"` `align="start"` |
| `open` / `onOpenChange` | kept; `onOpenChange` receives Base UI's reason and `details.cancel()` |
| `defaultOpen` | removed; nothing in the app starts open |
| `overlayClassName`, `overlayStyle`, `overlayInnerStyle`, `className` | rejected on the shell; size is `width` / `maxWidth` / `maxHeight` |
| `getPopupContainer` | `container` |
| `destroyTooltipOnHide` | the only behavior; the popup unmounts on close |
| `arrow` / `showArrow` | rejected; the shell has no arrow |
| Radix `PopoverAnchor` | `anchor`, and the trigger is still required |
| Radix `withPortal={false}` | `container` (inside a Dialog the popup already portals into the panel) |
| Radix `onInteractOutside` | `onOpenChange` + `details.cancel()` when `reason === 'outside-press'` |
| `sideOffset` / `alignOffset` | not props; fixed at 4px and 0 |
`nativeButton` is required. `true` when `trigger` renders a `<button>` (including `Button`). `false` for anything else.
A node that renders nothing (`null`, `false`, `''`) renders the trigger alone, with no listeners and no portal.
## What is in the app today
Counted from `frontend/src` on this branch:
- **29 files** import `Popover` from `antd`.
- **7 files** import `Popover` from `@signozhq/ui` (the Radix wrapper this rework replaces). Those seven break as soon as the package ships, because `children` stops being the trigger.
- **18 SCSS files** override `.ant-popover*`.
antd `Dropdown` is already on `@signozhq/ui/dropdown`. antd `Tooltip` (on the order of 100 importers) is a separate migration.
### antd files
`src/components/CustomTimePicker/CustomTimePicker.tsx`
`src/components/DownloadOptionsMenu/DownloadOptionsMenu.tsx`
`src/components/ErrorPopover/ErrorPopover.tsx`
`src/components/ExplorerCard/ExplorerCard.tsx`
`src/components/HeaderRightSection/HeaderRightSection.tsx`
`src/components/Logs/AddToQueryHOC.tsx`
`src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx`
`src/components/QueryBuilderV2/QueryV2/QueryAggregation/QueryAggregationSelect.tsx`
`src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.tsx`
`src/components/QueryBuilderV2/QueryV2/TraceOperator/TraceOperatorEditor.tsx`
`src/components/TableRenderer/LabelColumn.tsx`
`src/components/WarningPopover/WarningPopover.tsx`
`src/container/AlertHistory/AlertPopover/AlertPopover.tsx`
`src/container/CreateAlertV2/EvaluationSettings/EvaluationSettings.tsx`
`src/container/Home/Home.tsx`
`src/container/MetricsExplorer/MetricDetails/AllAttributes.tsx`
`src/container/MetricsExplorer/MetricDetails/AllAttributesValue.tsx`
`src/container/MetricsExplorer/Summary/MetricNameSearch.tsx`
`src/container/OptionsMenu/index.tsx`
`src/container/TopNav/AutoRefreshV2/index.tsx`
`src/pages/DashboardsListPage/components/ListHeader/ListHeader.tsx`
`src/pages/MetricsApplication/ApDex/ApDexApplication.tsx`
`src/pages/PublicDashboard/PublicDashboardView/PublicAutoRefresh/PublicAutoRefresh.tsx`
`src/pages/TraceDetailsV3/SpanDetailsPanel/Events/components/AttributeWithExpandablePopover.tsx`
`src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStepPopover.tsx`
`src/pages/TracesFunnels/components/FunnelsList/FunnelItemPopover.tsx`
`src/pages/TracesFunnels/components/SearchBar/SearchBar.tsx`
`src/periscope/components/ContextMenu/index.tsx`
`src/periscope/components/SeeMore/SeeMore.tsx`
### Existing `@signozhq/ui` popovers
`src/components/ExportMenu/ExportMenu.tsx`
`src/components/MarkdownEditor/MarkdownHelp.tsx`
`src/components/TanStackTableView/TanStackHeaderRow.tsx`
`src/container/AIAssistant/components/ChatInput/ChatInput.tsx`
`src/container/ListAlertRules/components/ColumnSelector.tsx`
`src/pages/DashboardPage/DashboardContainer/PanelEditor/ConfigPane/sections/ContextLinksSection/VariablesPopover.tsx`
`src/pages/DashboardsListPage/components/ViewsRail/ViewNamePopover.tsx`
### SCSS that overrides `.ant-popover`
`src/components/DownloadOptionsMenu/DownloadOptionsMenu.styles.scss`
`src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.styles.scss`
`src/components/QueryBuilderV2/QueryV2/QueryAggregation/QueryAggregation.styles.scss`
`src/components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch.styles.scss`
`src/container/AlertHistory/AlertPopover/AlertPopover.styles.scss`
`src/container/AlertHistory/Statistics/TopContributorsCard/TopContributorsCard.styles.scss`
`src/container/CreateAlertV2/EvaluationSettings/styles.scss`
`src/container/Home/Home.styles.scss`
`src/container/MetricsExplorer/MetricDetails/MetricDetails.styles.scss`
`src/container/MetricsExplorer/Summary/Summary.styles.scss`
`src/container/TopNav/AutoRefreshV2/AutoRefreshV2.styles.scss`
`src/container/TopNav/DateTimeSelectionV2/DateTimeSelectionV2.styles.scss`
`src/container/WidgetCard/WidgetCard.styles.scss`
`src/pages/DashboardsListPage/components/ListHeader/ListHeader.module.scss`
`src/pages/TracesFunnels/TracesFunnels.styles.scss`
`src/pages/TracesFunnels/components/FunnelsList/FunnelsList.styles.scss`
`src/pages/TracesFunnels/components/SearchBar/SearchBar.styles.scss`
`src/periscope/components/ContextMenu/styles.scss`
Three of these are unscoped and restyle every antd popover on the page, not just their own:
- `src/container/MetricsExplorer/Summary/Summary.styles.scss`
- `src/container/CreateAlertV2/EvaluationSettings/styles.scss`
- `src/container/AlertHistory/Statistics/TopContributorsCard/TopContributorsCard.styles.scss`
Scope or delete those three before moving any nearby popover, or a migrated popup and a leftover antd popup will share rules that only one of them understands.
Most of the other files repeat one recipe: padding 0, 4px radius, a 1px border, a translucent background, the arrow hidden. The new shell already has zero padding, `--radius-2`, and no arrow. Per-site size (fixed widths around 187–500px, max-width 600px, max-height 254px) becomes `width` / `maxWidth` / `maxHeight`. Do not re-apply the recipe as a `className`; the component rejects it.
## What should not become this Popover
**Hover descriptions.** `AddToQueryHOC`, `LabelColumn`, the three Query Builder validation popovers, `SeeMore`, and `AttributeWithExpandablePopover` open on hover. Those are Tooltips. `AttributeWithExpandablePopover` shows a value the user copies; if Tooltip cannot hold that, make it a click popover instead of keeping hover.
**WarningPopover** (imported from about 10 files) is hover today and contains docs links. A tooltip cannot host those links. Either the links move out and the summary becomes a Tooltip, or the wrapper becomes a click Popover with `maxWidth` around 600.
**Menus.** Funnel item and funnel step action lists, the sort lists in `ListHeader` and Traces Funnels `SearchBar`, `AutoRefreshV2` / `PublicAutoRefresh` interval lists, and `AlertPopover`'s two links are rows that close when one is picked. Those are Dropdowns. `AllAttributes` per-row actions are the same shape.
**ContextMenu** (`src/periscope/components/ContextMenu/index.tsx`, imported across the dashboard drilldown) opens at chart-click coordinates. There is no trigger element. The new Popover requires `trigger`, so this stays a context menu. Do not fold the phantom span, `position: fixed` overlay, `zIndex: 10000`, or `calculatePopoverPosition.ts` into `anchor`. `anchor` is for a popover that already has a trigger and should position against something else (the typeahead input row).
**Inspect charts.** `MetricsExplorer/Inspect` `GraphPopover` and `HoverPopover` are absolutely positioned cards, not antd Popover. Leave them until there is a context-menu or anchor design that matches them.
**PanelStatusPopover** is a Tooltip despite the name.
## What should become this Popover
Click panels whose content is a form, a picker, or a composed layout:
- `CustomTimePicker` — the most visible one. Controlled `open`, `container` for `getPopupContainer`, and it already unmounts on hide. It opens from both input focus and click; that logic stays in the caller, with `onOpenChange`.
- `EvaluationSettings` / `EvaluationWindowPopover` — controlled, wide (`width` / `maxWidth`), and sitting on a global `.ant-popover` override. Migrate the override in the same change.
- `HeaderRightSection` — three controlled forms (feedback, announcements, share).
- `ExplorerCard` save-view form and `ViewNamePopover` — close only after the save succeeds, so `open` stays controlled. `ViewNamePopover` is already on the library and will not compile against the new props.
- `ApDex` threshold form.
- `Home` welcome checklist — drop the transparent-shell override; the shell is the component's.
- `OptionsMenu`, `DownloadOptionsMenu`, `LogsFormatOptionsMenu` — click panels. `LogsFormatOptionsMenu` nests a submenu; the inner popup must be portalled, and the outer `onOpenChange` must `cancel()` an outside press that lands in the inner popup.
- `MetricNameSearch` — controlled search panel. Scope the global padding override in `Summary.styles.scss` in the same change.
- `AllAttributesValue` — one popover open at a time in a list. That is controlled `open` keyed by row, which the API still allows.
- `ChatInput` context picker — already on the library; `side="top"` `align="end"` plus `width`.
- `VariablesPopover` — already on the library. Keep `anchor` on the input row, `initialFocus={false}` so the caret stays, and `details.cancel()` when the outside press is the anchor. `withPortal={false}` becomes the default dialog `container`, not an unportalled popup.
- `ColumnSelector`, `MarkdownHelp`, `TanStackHeaderRow`, `ExportMenu` — Radix `asChild` trigger plus content. The trigger moves to the `trigger` prop, content moves to `children`, and `className` on the shell becomes `width` / `maxWidth`.
`ErrorPopover` is a thin wrapper. Re-point it at the new props and its consumer follows.
## Order
Before any change: screenshot every current popover, in both themes, so each call site's pattern is on record and a component swap is reviewable against it. Flag the misusages to design as they turn up — links inside hover surfaces above all. Swapping the component during migration is expected where the old call site was on the wrong one; antd call sites stay untouched until their turn.
1. Scope the three global `.ant-popover` files so later migrations cannot restyle leftover antd popovers.
2. Move the seven existing `@signozhq/ui` call sites first. They break at the package bump; the 29 antd files do not, until each one is switched.
3. Migrate shared wrappers that are actually popovers (`ErrorPopover`, then `WarningPopover` once the links decision is made). Leave `ContextMenu` on antd until a context menu exists.
4. Migrate click forms (`CustomTimePicker`, evaluation window, header menus, save-view, ApDex).
5. Send hover descriptions to Tooltip and row menus to Dropdown, instead of forcing them through Popover.
6. Delete the matching `.ant-popover` rules in the same change as each call site, so a half-migrated page is not styled twice.
## Risks
- **Controlled `open` is load-bearing.** Save forms close from inside the popup only when the mutation succeeds. A list keeps one popover open at a time. Dropping `open` and driving everything from `onOpenChange` alone cannot express either. Keep the controlled prop at those sites.
- **Focus moves in.** Click popovers focus the first tabbable child. Typeaheads (`VariablesPopover`, and any filter that must keep the caret) need `initialFocus={false}`.
- **Outside press and anchors.** A press on the anchor looks outside. Call `details.cancel()` or the typeahead closes when the user clicks the input it is anchored to.
- **Dialogs.** Inside a Dialog or Drawer the popup portals into the panel so the focus trap can reach it. Do not set `container={document.body}` there.
- **Z-index.** The shell starts at `--popover-z-index` (50) and lifts above the layer its trigger sits in. A Tooltip inside a row must still paint above the popup. The app ladder today is tooltip 1070, dropdown 1050, popover 1030, modal 1000. Check a truncated row tooltip inside a popover after the first real migration.
- **Scroll locking.** Chart popovers measure the wrong box when a modal has locked scroll. This popover does not lock scroll (`modal` is rejected). Re-check Infrastructure Monitoring charts if a surrounding modal is open.
- **`nativeButton` mismatches.** Base UI logs an error when the flag does not match the element. Every non-button trigger (a badge, a custom row, an input) needs `nativeButton={false}`.
## `--popover-foreground` audit
The shell reads `--popover-background`, `--popover-foreground`, `--popover-border`, and `--popover-shadow`. Those semantic values belong to the token package, not to call sites.
Two legacy shadcn variables predate those hooks:
- `--popover-foreground` is the foreground hook the shell uses. It should stay pointed at the same foreground as the rest of the light and dark themes (`--l1-foreground` today).
- `--popover` (no suffix) is the old background. `--calendar-select-background` still reads it. Do not repoint `--popover` at `--popover-background` or at `surface-3` until the calendar token is audited, or date selection changes color with the popover shell.
Call sites that pass antd's `color` (a gradient string on `AlertPopover`, a raw token on `SeeMore`) do not map to a prop. The shell color comes from the tokens. Content that needs its own background paints it inside `children`.

View File

@@ -14,6 +14,11 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdn.vercel.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet"
/>
<title data-react-helmet="true">
Open source Observability platform | SigNoz
</title>

View File

@@ -53,9 +53,9 @@
"@monaco-editor/react": "^4.7.0",
"@sentry/react": "10.57.0",
"@sentry/vite-plugin": "5.3.0",
"@signozhq/design-tokens": "2.6.0",
"@signozhq/design-tokens": "2.2.1",
"@signozhq/icons": "0.4.0",
"@signozhq/ui": "0.3.0",
"@signozhq/ui": "0.2.0",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",

View File

@@ -1,170 +0,0 @@
import { ruleTester } from './rule-tester.mjs';
const message = (property) =>
`Do not set '${property}' in CSS. Render the text with @signozhq/ui/typography and use its props, or disable this rule with a reason after \`--\` saying why Typography cannot.`;
await ruleTester({
rule: 'no-text-css',
valid: [
{
name: 'layout properties in a style prop',
code: 'export const A = () => <div style={{ width: 10, color: "red" }} />;',
},
{
name: 'font properties in an object that is not a style',
code: 'export const options = { fontSize: 12, font: "12px Inter" };',
},
{
name: 'font properties in a prop that is not a style',
code: 'export const A = () => <Chart options={{ fontSize: 12 }} />;',
},
{
name: 'computed key',
code:
'const k = "fontSize";\nexport const A = () => <div style={{ [k]: 12 }} />;',
},
{
name: 'style identifier from a parameter',
code: 'export const A = ({ s }) => <div style={s} />;',
},
{
name: 'template without a styled tag',
code: 'export const s = html`font-size: 12px;`;',
},
{
name: 'styled template with layout only',
code:
"import styled from 'styled-components';\nexport const A = styled.div`\n\tdisplay: flex;\n\ttext: none;\n`;",
},
{
name: 'custom property that ends in a banned name',
code:
"import styled from 'styled-components';\nexport const A = styled.div`\n\t--my-font-size: 12px;\n`;",
},
],
invalid: [
{
name: 'inline style',
code: 'export const A = () => <div style={{ fontSize: 12, width: 1 }} />;',
errors: [{ message: message('fontSize'), line: 1, column: 38 }],
},
{
name: 'string key',
code: "export const A = () => <div style={{ 'line-height': 1 }} />;",
errors: [{ message: message('line-height') }],
},
{
name: 'every banned family',
code:
'export const A = () => (\n\t<div\n\t\tstyle={{\n\t\t\tfont: "x",\n\t\t\tfontWeight: 500,\n\t\t\tfontFamily: "x",\n\t\t\ttextAlign: "center",\n\t\t\ttextTransform: "none",\n\t\t\tlineHeight: 1,\n\t\t\tletterSpacing: 1,\n\t\t}}\n\t/>\n);',
errors: [
{ message: message('font') },
{ message: message('fontWeight') },
{ message: message('fontFamily') },
{ message: message('textAlign') },
{ message: message('textTransform') },
{ message: message('lineHeight') },
{ message: message('letterSpacing') },
],
},
{
name: 'conditional, logical and spread',
code:
'export const A = ({ a, b }) => (\n\t<div style={{ ...(a ? { fontSize: 1 } : {}), ...(b && { lineHeight: 1 }) }} />\n);',
errors: [
{ message: message('fontSize') },
{ message: message('lineHeight') },
],
},
{
name: 'style prop with a Style suffix',
code:
'export const A = () => <Card headStyle={{ fontWeight: 600 }} bodyStyle={{ padding: 0 }} />;',
errors: [{ message: message('fontWeight') }],
},
{
name: 'antd semantic styles',
code:
'export const A = () => <Modal styles={{ body: { fontSize: 12 }, header: { padding: 0 } }} />;',
errors: [{ message: message('fontSize') }],
},
{
name: 'identifier resolved to a local object',
code:
'const s = { fontSize: 12 };\nexport const A = () => (\n\t<>\n\t\t<div style={s} />\n\t\t<span style={s} />\n\t</>\n);',
errors: [{ message: message('fontSize'), line: 1, column: 13 }],
},
{
name: 'useMemo style',
code:
"import { useMemo } from 'react';\nexport const A = () => {\n\tconst s = useMemo(() => ({ textAlign: 'left' }), []);\n\treturn <div style={s} />;\n};",
errors: [{ message: message('textAlign') }],
},
{
name: 'object typed CSSProperties',
code:
"import type { CSSProperties } from 'react';\nexport const s: CSSProperties = { fontStyle: 'italic' };",
errors: [{ message: message('fontStyle') }],
},
{
name:
'object typed React.CSSProperties, reported once when also used as style',
code:
"import React from 'react';\nconst s: React.CSSProperties = { fontSize: 1 };\nexport const A = () => <div style={s} />;",
errors: [{ message: message('fontSize') }],
},
{
name: 'Record of CSSProperties',
code:
"import type { CSSProperties } from 'react';\nexport const s: Record<string, CSSProperties> = { a: { lineHeight: 1 } };",
errors: [{ message: message('lineHeight') }],
},
{
name: 'as and satisfies CSSProperties',
code:
"import type { CSSProperties } from 'react';\nexport const a = { fontSize: 1 } as CSSProperties;\nexport const b = { fontWeight: 1 } satisfies CSSProperties;",
errors: [
{ message: message('fontSize') },
{ message: message('fontWeight') },
],
},
{
name: 'styled-components template',
code:
"import styled from 'styled-components';\nexport const A = styled.div`\n\tdisplay: flex;\n\tfont-size: 12px;\n\tcolor: ${(p) => p.c};\n\ttext-transform: none;line-height: 1;\n`;",
errors: [
{ message: message('font-size'), line: 4, column: 2 },
{ message: message('text-transform'), line: 6, column: 2 },
{ message: message('line-height'), line: 6, column: 23 },
],
},
{
name: 'function returning CSSProperties',
code:
"import type { CSSProperties } from 'react';\nexport const a = (): CSSProperties => ({ fontSize: 1 });\nexport function b(): CSSProperties {\n\treturn { lineHeight: 1 };\n}",
errors: [
{ message: message('fontSize') },
{ message: message('lineHeight') },
],
},
{
name: 'strings in styled interpolations',
code:
"import styled, { css } from 'styled-components';\nexport const A = styled.div`\n\t${(p) => (p.s ? 'font-size: 11px;' : `line-height: 1;`)}\n\t${(p) => p.b && css`font-weight: 500;`}\n`;",
errors: [
{ message: message('font-size'), line: 3, column: 19 },
{ message: message('line-height'), line: 3, column: 40 },
{ message: message('font-weight'), line: 4, column: 22 },
],
},
{
name: 'styled(Component) and css helper',
code:
"import styled, { css } from 'styled-components';\nexport const A = styled(B)`letter-spacing: 1px;`;\nexport const c = css`\n\tfont-weight: 500;\n`;",
errors: [
{ message: message('letter-spacing'), line: 2, column: 28 },
{ message: message('font-weight'), line: 4, column: 2 },
],
},
],
});

View File

@@ -1,49 +0,0 @@
import { ruleTester } from './rule-tester.mjs';
const message = 'Disabling signoz/no-text-css needs a reason after `--`.';
await ruleTester({
rule: 'require-disable-reason',
valid: [
{
name: 'reason after --',
code:
'// oxlint-disable-next-line signoz/no-text-css -- canvas label\nexport const a = 1;',
},
{
name: 'block comment with a reason',
code:
'/* eslint-disable signoz/no-text-css -- email HTML */\nexport const a = 1;',
},
{
name: 'another rule without a reason',
code: '// oxlint-disable-next-line no-console\nexport const a = 1;',
},
{
name: 'disable of every rule',
code: '// eslint-disable-next-line\nexport const a = 1;',
},
{
name: 'plain comment naming the rule',
code: '// signoz/no-text-css flags font CSS\nexport const a = 1;',
},
],
invalid: [
{
name: 'next-line without a reason',
code: '// oxlint-disable-next-line signoz/no-text-css\nexport const a = 1;',
errors: [{ message, line: 1, column: 1 }],
},
{
name: 'empty reason',
code: '// eslint-disable-line signoz/no-text-css --\nexport const a = 1;',
errors: [{ message }],
},
{
name: 'among other rules',
code:
'/* oxlint-disable no-console, signoz/no-text-css */\nexport const a = 1;',
errors: [{ message }],
},
],
});

View File

@@ -1,324 +0,0 @@
/**
* Rule: no-text-css
*
* Text style (font, line height, letter spacing, case, alignment, overflow)
* belongs to Typography props, not to CSS written in a component.
*
* This rule catches font and text properties in:
* <div style={{ fontSize: 12 }} /> style and *Style props
* <Modal styles={{ body: { fontWeight: 500 } }} />
* const s: CSSProperties = { lineHeight: 1 } objects typed CSSProperties
* ({ ... }) as CSSProperties, satisfies CSSProperties
* (): CSSProperties => ({ ... }) functions returning CSSProperties
* styled.div`font-size: 12px;` styled-components templates,
* ${(p) => p.small && 'font-size: 11px;'} strings in their interpolations
*
* Identifiers passed to a style prop are followed to a local object literal.
* SCSS is covered by the stylelint config.
*/
const MESSAGE = (property) =>
`Do not set '${property}' in CSS. Render the text with @signozhq/ui/typography and use its props, or disable this rule with a reason after \`--\` saying why Typography cannot.`;
const BANNED_JS = /^(font([A-Z]\w*)?|text[A-Z]\w*|lineHeight|letterSpacing)$/;
const BANNED_CSS =
/(^|[\s;{])(font(-[a-z]+)*|text-[a-z-]+|line-height|letter-spacing)\s*:/g;
const STYLE_TAGS = new Set(['css', 'createGlobalStyle', 'keyframes']);
const FUNCTIONS = new Set([
'ArrowFunctionExpression',
'FunctionExpression',
'FunctionDeclaration',
]);
const camelCase = (name) =>
name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
function propertyName(property) {
if (property.computed) {
return null;
}
if (property.key.type === 'Identifier') {
return property.key.name;
}
if (
property.key.type === 'Literal' &&
typeof property.key.value === 'string'
) {
return property.key.value;
}
return null;
}
function isCssPropertiesType(typeNode) {
if (!typeNode || typeNode.type !== 'TSTypeReference') {
return false;
}
const name = typeNode.typeName;
if (name.type === 'Identifier') {
return name.name === 'CSSProperties';
}
return name.type === 'TSQualifiedName' && name.right.name === 'CSSProperties';
}
// Record<K, CSSProperties> and { [key: string]: CSSProperties }
function isCssPropertiesMapType(typeNode) {
if (!typeNode) {
return false;
}
if (
typeNode.type === 'TSTypeReference' &&
typeNode.typeName.type === 'Identifier' &&
typeNode.typeName.name === 'Record'
) {
const params = (typeNode.typeArguments ?? typeNode.typeParameters)?.params;
return isCssPropertiesType(params?.[1]);
}
if (typeNode.type === 'TSTypeLiteral') {
return typeNode.members.some(
(member) =>
member.type === 'TSIndexSignature' &&
isCssPropertiesType(member.typeAnnotation?.typeAnnotation),
);
}
return false;
}
function rootIdentifier(node) {
let current = node;
while (current) {
if (current.type === 'Identifier') {
return current.name;
}
if (current.type === 'MemberExpression') {
current = current.object;
} else if (current.type === 'CallExpression') {
current = current.callee;
} else {
return null;
}
}
return null;
}
function isStyleTag(tag) {
if (tag.type === 'Identifier') {
return tag.name === 'styled' || STYLE_TAGS.has(tag.name);
}
return rootIdentifier(tag) === 'styled';
}
export default {
create(context) {
const reported = new Set();
const followed = new Set();
const reportedCss = new Set();
function report(property, name) {
if (reported.has(property)) {
return;
}
reported.add(property);
context.report({ node: property.key, message: MESSAGE(name) });
}
function resolveIdentifier(identifier) {
const scope = context.sourceCode.getScope(identifier);
let current = scope;
while (current) {
const variable = current.set.get(identifier.name);
if (variable) {
const definition = variable.defs[0];
if (
definition?.node?.type === 'VariableDeclarator' &&
definition.node.init
) {
return definition.node.init;
}
return null;
}
current = current.upper;
}
return null;
}
function checkObject(object) {
for (const property of object.properties) {
if (property.type === 'SpreadElement') {
checkStyle(property.argument);
continue;
}
const name = propertyName(property);
if (name && BANNED_JS.test(camelCase(name))) {
report(property, name);
}
}
}
function checkStyle(expression) {
if (!expression) {
return;
}
switch (expression.type) {
case 'ObjectExpression':
checkObject(expression);
return;
case 'ConditionalExpression':
checkStyle(expression.consequent);
checkStyle(expression.alternate);
return;
case 'LogicalExpression':
checkStyle(expression.left);
checkStyle(expression.right);
return;
case 'TSAsExpression':
case 'TSSatisfiesExpression':
case 'TSNonNullExpression':
case 'ParenthesizedExpression':
checkStyle(expression.expression);
return;
case 'CallExpression':
// useMemo(() => ({ ... }), deps)
if (
expression.callee.type === 'Identifier' &&
expression.callee.name === 'useMemo'
) {
checkReturned(expression.arguments[0]);
}
return;
case 'Identifier': {
const init = resolveIdentifier(expression);
if (!init || followed.has(init)) {
return;
}
followed.add(init);
checkStyle(init);
return;
}
default:
}
}
function checkReturned(fn) {
if (!fn || !FUNCTIONS.has(fn.type)) {
return;
}
if (fn.body.type !== 'BlockStatement') {
checkStyle(fn.body);
return;
}
for (const statement of fn.body.body) {
if (statement.type === 'ReturnStatement') {
checkStyle(statement.argument);
}
}
}
function checkStyleMap(expression) {
if (expression?.type !== 'ObjectExpression') {
return;
}
for (const property of expression.properties) {
if (property.type === 'Property') {
checkStyle(property.value);
}
}
}
// `start` is the index of the first character after the opening quote.
function checkCss(text, start) {
for (const match of text.matchAll(BANNED_CSS)) {
const index = start + match.index + match[1].length;
if (reportedCss.has(index)) {
continue;
}
reportedCss.add(index);
context.report({
loc: {
start: context.sourceCode.getLocFromIndex(index),
end: context.sourceCode.getLocFromIndex(index + match[2].length),
},
message: MESSAGE(match[2]),
});
}
}
// CSS returned from interpolations: ${(p) => p.small && 'font-size: 11px;'}
function checkCssStrings(node) {
if (!node || typeof node.type !== 'string') {
return;
}
if (node.type === 'TemplateLiteral') {
for (const quasi of node.quasis) {
checkCss(quasi.value.raw, quasi.range[0] + 1);
}
} else if (node.type === 'Literal' && typeof node.value === 'string') {
checkCss(node.raw.slice(1, -1), node.range[0] + 1);
return;
}
for (const [key, value] of Object.entries(node)) {
if (key === 'parent') {
continue;
}
if (Array.isArray(value)) {
value.forEach(checkCssStrings);
} else if (value && typeof value === 'object') {
checkCssStrings(value);
}
}
}
function checkFunction(node) {
if (isCssPropertiesType(node.returnType?.typeAnnotation)) {
checkReturned(node);
}
}
return {
JSXAttribute(node) {
if (
node.name.type !== 'JSXIdentifier' ||
node.value?.type !== 'JSXExpressionContainer'
) {
return;
}
const name = node.name.name;
if (name === 'styles') {
checkStyleMap(node.value.expression);
} else if (name === 'style' || name.endsWith('Style')) {
checkStyle(node.value.expression);
}
},
VariableDeclarator(node) {
const type = node.id.typeAnnotation?.typeAnnotation;
if (isCssPropertiesType(type)) {
checkStyle(node.init);
} else if (isCssPropertiesMapType(type)) {
checkStyleMap(node.init);
}
},
TSAsExpression(node) {
if (isCssPropertiesType(node.typeAnnotation)) {
checkStyle(node.expression);
}
},
TSSatisfiesExpression(node) {
if (isCssPropertiesType(node.typeAnnotation)) {
checkStyle(node.expression);
} else if (isCssPropertiesMapType(node.typeAnnotation)) {
checkStyleMap(node.expression);
}
},
TaggedTemplateExpression(node) {
if (!isStyleTag(node.tag)) {
return;
}
checkCssStrings(node.quasi);
},
ArrowFunctionExpression: checkFunction,
FunctionExpression: checkFunction,
FunctionDeclaration: checkFunction,
};
},
};

View File

@@ -1,38 +0,0 @@
/**
* Rule: require-disable-reason
*
* A disable directive that names one of RULES must say why after `--`:
* // oxlint-disable-next-line signoz/no-text-css -- canvas text, not DOM
*
* Directives that disable every rule are not checked.
*/
const RULES = new Set(['signoz/no-text-css']);
const DIRECTIVE = /^\s*(?:eslint|oxlint)-disable(?:-next-line|-line)?\s+(.*)$/s;
export default {
create(context) {
return {
Program() {
for (const comment of context.sourceCode.getAllComments()) {
const match = comment.value.match(DIRECTIVE);
if (!match) {
continue;
}
const [rules, reason] = match[1].split(/\s--\s?/);
const named = rules
.split(',')
.map((rule) => rule.trim())
.filter((rule) => RULES.has(rule));
if (named.length > 0 && !reason?.trim()) {
context.report({
node: comment,
message: `Disabling ${named.join(', ')} needs a reason after \`--\`.`,
});
}
}
},
};
},
};

View File

@@ -17,8 +17,6 @@ import noConditionalTextNodesWithSiblings from './rules/no-conditional-text-node
import noReturnTextNodes from './rules/no-return-text-nodes.mjs';
import noMswInStoryFile from './rules/no-msw-in-story-file.mjs';
import noDirectReactRouterImport from './rules/no-direct-react-router-import.mjs';
import noTextCss from './rules/no-text-css.mjs';
import requireDisableReason from './rules/require-disable-reason.mjs';
export default {
meta: {
@@ -37,7 +35,5 @@ export default {
'no-return-text-nodes': noReturnTextNodes,
'no-msw-in-story-file': noMswInStoryFile,
'no-direct-react-router-import': noDirectReactRouterImport,
'no-text-css': noTextCss,
'require-disable-reason': requireDisableReason,
},
};

450
frontend/pnpm-lock.yaml generated
View File

@@ -74,14 +74,14 @@ importers:
specifier: 5.3.0
version: 5.3.0
'@signozhq/design-tokens':
specifier: 2.6.0
version: 2.6.0
specifier: 2.2.1
version: 2.2.1
'@signozhq/icons':
specifier: 0.4.0
version: 0.4.0
'@signozhq/ui':
specifier: 0.3.0
version: 0.3.0(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
specifier: 0.2.0
version: 0.2.0(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -2909,6 +2909,9 @@ packages:
'@posthog/core@1.6.0':
resolution: {integrity: sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==}
'@radix-ui/number@1.1.3':
resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==}
'@radix-ui/primitive@1.1.7':
resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==}
@@ -2925,6 +2928,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.15':
resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.1.5':
resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==}
peerDependencies:
@@ -2956,6 +2972,15 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.4':
resolution: {integrity: sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.19':
resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==}
peerDependencies:
@@ -3070,6 +3095,71 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-progress@1.1.16':
resolution: {integrity: sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-radio-group@1.4.7':
resolution: {integrity: sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-roving-focus@1.1.19':
resolution: {integrity: sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-select@2.3.7':
resolution: {integrity: sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slider@1.4.7':
resolution: {integrity: sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.3.3':
resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==}
peerDependencies:
@@ -3079,6 +3169,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-tabs@1.1.21':
resolution: {integrity: sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.4':
resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==}
peerDependencies:
@@ -3106,6 +3209,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-is-hydrated@0.1.3':
resolution: {integrity: sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.4':
resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==}
peerDependencies:
@@ -3115,6 +3227,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-previous@1.1.4':
resolution: {integrity: sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-rect@1.1.4':
resolution: {integrity: sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==}
peerDependencies:
@@ -3133,6 +3254,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-visually-hidden@1.2.11':
resolution: {integrity: sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/rect@1.1.3':
resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==}
@@ -3451,14 +3585,14 @@ packages:
'@sideway/pinpoint@2.0.0':
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
'@signozhq/design-tokens@2.6.0':
resolution: {integrity: sha512-7+lBxC3N4r1A2M8awhop7mtYMMd5yOg4BKfRAN3hnle4lclZHlxm7b3gdplfNN10JA1mcwRSQvWrjQPmA/euqw==}
'@signozhq/design-tokens@2.2.1':
resolution: {integrity: sha512-ulf6lhWUP6qxqsyemQySUMfOIKRvefvZ5rBRv+wlhOpPfYPk3uatPmPkKEhAjISpX8f9g1Gg8wgRZxUCysf20Q==}
'@signozhq/icons@0.4.0':
resolution: {integrity: sha512-QctNvNPlQlsefFdIVmSy/P/7a1IcQ3IryKP0KPMqtoqew3q89FGUmFa5IfEJ7Z3CHhtG1zaXwBKqP72QkLyA3g==}
'@signozhq/ui@0.3.0':
resolution: {integrity: sha512-Khzr3/YC/QACwBi1zfQwt+h88iyuIu9hm5hkcIkuCTteD9P+r13US/NN4kkbJe8IHyZXCoDkoIpmQxzbrA3/SA==}
'@signozhq/ui@0.2.0':
resolution: {integrity: sha512-j20ldiOkR150ON0BCDhlq6fXGoPN++7w6e4t2HtM2XMM4buJO7aHzPzNKvDRZ2UUf/LGPULPAW8w6sg6gUVHyw==}
peerDependencies:
'@signozhq/icons': 0.3.0
react: ^18.2.0
@@ -4791,6 +4925,12 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cmdk@1.1.1:
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -7319,6 +7459,12 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
next-themes@0.4.6:
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
peerDependencies:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
@@ -8287,8 +8433,8 @@ packages:
'@types/react':
optional: true
react-resizable-panels@4.12.2:
resolution: {integrity: sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q==}
react-resizable-panels@4.12.3:
resolution: {integrity: sha512-GHMJWnDXui/3RX4bT+cgBP+N3N2nkwcoZATr/2xLFpqQQe7TlBrE0cGM0dUa9ceT7A90tUrSRsiqgRG+g0/2AA==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
@@ -8788,6 +8934,12 @@ packages:
resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==}
engines: {node: '>=20'}
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
sort-asc@0.1.0:
resolution: {integrity: sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==}
engines: {node: '>=0.10.0'}
@@ -9798,7 +9950,7 @@ snapshots:
'@ant-design/cssinjs@1.17.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@emotion/hash': 0.8.0
'@emotion/unitless': 0.7.5
classnames: 2.3.2
@@ -9814,7 +9966,7 @@ snapshots:
dependencies:
'@ant-design/colors': 7.0.0
'@ant-design/icons-svg': 4.3.1
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -9822,7 +9974,7 @@ snapshots:
'@ant-design/react-slick@1.0.2(react@18.2.0)':
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
json2mq: 0.2.0
react: 18.2.0
@@ -12346,6 +12498,8 @@ snapshots:
dependencies:
cross-spawn: 7.0.6
'@radix-ui/number@1.1.3': {}
'@radix-ui/primitive@1.1.7': {}
'@radix-ui/react-arrow@1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
@@ -12357,6 +12511,18 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-collection@1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-compose-refs@1.1.5(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -12392,6 +12558,12 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-direction@1.1.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
@@ -12502,6 +12674,101 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-progress@1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-radio-group@1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-size': 1.1.4(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-roving-focus@1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-collection': 1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-select@2.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/number': 1.1.3
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-collection': 1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-focus-guards': 1.1.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-popper': 1.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-portal': 1.1.17(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-previous': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
aria-hidden: 1.2.6
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.7.2(@types/react@18.0.26)(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-slider@1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/number': 1.1.3
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-collection': 1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-previous': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-size': 1.1.4(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-slot@1.3.3(@types/react@18.0.26)(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
@@ -12509,6 +12776,22 @@ snapshots:
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-tabs@1.1.21(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-use-callback-ref@1.1.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -12531,12 +12814,24 @@ snapshots:
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-use-is-hydrated@0.1.3(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-use-layout-effect@1.1.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-use-previous@1.1.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-use-rect@1.1.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
'@radix-ui/rect': 1.1.3
@@ -12551,11 +12846,20 @@ snapshots:
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/rect@1.1.3': {}
'@rc-component/color-picker@1.4.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@ctrl/tinycolor': 3.6.1
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -12575,7 +12879,7 @@ snapshots:
'@rc-component/mutate-observer@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -12591,7 +12895,7 @@ snapshots:
'@rc-component/tour@1.10.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
@@ -12601,7 +12905,7 @@ snapshots:
'@rc-component/trigger@1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -12846,33 +13150,41 @@ snapshots:
'@sideway/pinpoint@2.0.0': {}
'@signozhq/design-tokens@2.6.0': {}
'@signozhq/design-tokens@2.2.1': {}
'@signozhq/icons@0.4.0': {}
'@signozhq/ui@0.3.0(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.2.0(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)':
dependencies:
'@base-ui/react': 1.7.0(@date-fns/tz@1.4.1)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@chenglou/pretext': 0.0.8
'@radix-ui/react-dialog': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-icons': 1.3.2(react@18.2.0)
'@radix-ui/react-popover': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-progress': 1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-radio-group': 1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-select': 2.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slider': 1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-tabs': 1.1.21(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/icons': 0.4.0
'@tanstack/react-table': 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@tanstack/react-virtual': 3.14.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@types/lodash-es': 4.17.12
clsx: 2.1.1
cmdk: 1.1.1(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
copy-text-to-clipboard: 3.2.2
dayjs: 1.11.23
lodash-es: 4.18.1
motion: 11.18.2(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-themes: 0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nuqs: 2.9.6(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-compiler-runtime: 1.0.0(react@18.2.0)
react-day-picker: 9.9.0(react@18.2.0)
react-dom: 18.2.0(react@18.2.0)
react-resizable-panels: 4.12.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-resizable-panels: 4.12.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
sonner: 2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
transitivePeerDependencies:
- '@date-fns/tz'
- '@emotion/is-prop-valid'
@@ -14122,7 +14434,7 @@ snapshots:
better-xlsx@0.7.6:
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
jszip: 3.10.1
kind-of: 6.0.3
@@ -14159,7 +14471,7 @@ snapshots:
broadcast-channel@3.7.0:
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
detect-node: 2.1.0
js-sha3: 0.8.0
microseconds: 0.2.0
@@ -14368,6 +14680,18 @@ snapshots:
clsx@2.1.1: {}
cmdk@1.1.1(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-dialog': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
co@4.6.0: {}
codemirror@6.0.1:
@@ -16916,7 +17240,7 @@ snapshots:
match-sorter@6.3.1:
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
remove-accents: 0.4.2
math-intrinsics@1.1.0: {}
@@ -17426,6 +17750,11 @@ snapshots:
neo-async@2.6.2: {}
next-themes@0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
node-addon-api@7.1.1:
optional: true
@@ -18049,7 +18378,7 @@ snapshots:
rc-cascader@3.20.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
array-tree-filter: 2.1.0
classnames: 2.3.2
rc-select: 14.10.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18060,7 +18389,7 @@ snapshots:
rc-checkbox@3.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18068,7 +18397,7 @@ snapshots:
rc-collapse@3.7.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18077,7 +18406,7 @@ snapshots:
rc-dialog@9.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18087,7 +18416,7 @@ snapshots:
rc-drawer@6.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18097,7 +18426,7 @@ snapshots:
rc-dropdown@4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18106,7 +18435,7 @@ snapshots:
rc-field-form@1.40.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
async-validator: 4.2.5
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18114,7 +18443,7 @@ snapshots:
rc-image@7.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-dialog: 9.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18125,7 +18454,7 @@ snapshots:
rc-input-number@8.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/mini-decimal': 1.0.1
classnames: 2.3.2
rc-input: 1.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18135,7 +18464,7 @@ snapshots:
rc-input@1.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18143,7 +18472,7 @@ snapshots:
rc-mentions@2.9.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-input: 1.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18155,7 +18484,7 @@ snapshots:
rc-menu@9.12.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18166,7 +18495,7 @@ snapshots:
rc-motion@2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18174,7 +18503,7 @@ snapshots:
rc-notification@5.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18183,7 +18512,7 @@ snapshots:
rc-overflow@1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18192,7 +18521,7 @@ snapshots:
rc-pagination@3.7.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18200,7 +18529,7 @@ snapshots:
rc-picker@3.14.6(date-fns@4.1.0)(dayjs@1.11.20)(moment@2.30.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18213,7 +18542,7 @@ snapshots:
rc-progress@3.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18221,7 +18550,7 @@ snapshots:
rc-rate@2.12.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18229,7 +18558,7 @@ snapshots:
rc-resize-observer@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18238,7 +18567,7 @@ snapshots:
rc-segmented@2.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18259,7 +18588,7 @@ snapshots:
rc-slider@10.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18267,7 +18596,7 @@ snapshots:
rc-steps@6.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18275,7 +18604,7 @@ snapshots:
rc-switch@4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18283,7 +18612,7 @@ snapshots:
rc-table@7.35.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/context': 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18294,7 +18623,7 @@ snapshots:
rc-tabs@12.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-dropdown: 4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-menu: 9.12.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18306,7 +18635,7 @@ snapshots:
rc-textarea@1.5.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-input: 1.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18316,7 +18645,7 @@ snapshots:
rc-tooltip@6.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
'@rc-component/trigger': 1.18.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
classnames: 2.3.2
react: 18.2.0
@@ -18324,7 +18653,7 @@ snapshots:
rc-tree-select@5.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-select: 14.10.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-tree: 5.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18334,7 +18663,7 @@ snapshots:
rc-tree@5.8.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-motion: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18344,7 +18673,7 @@ snapshots:
rc-upload@4.3.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -18352,14 +18681,14 @@ snapshots:
rc-util@5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-is: 18.3.1
rc-virtual-list@3.11.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
classnames: 2.3.2
rc-resize-observer: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -18585,7 +18914,7 @@ snapshots:
optionalDependencies:
'@types/react': 18.0.26
react-resizable-panels@4.12.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
react-resizable-panels@4.12.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -18636,7 +18965,7 @@ snapshots:
react-router@5.3.4(react@18.2.0):
dependencies:
'@babel/runtime': 7.29.2
'@babel/runtime': 7.28.2
history: 4.10.1
hoist-non-react-statics: 3.3.2
loose-envify: 1.4.0
@@ -19176,6 +19505,11 @@ snapshots:
ansi-styles: 6.2.3
is-fullwidth-code-point: 5.1.0
sonner@2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
sort-asc@0.1.0: {}
sort-desc@0.1.1: {}

View File

@@ -1,92 +0,0 @@
Copyright (c) 2023 Vercel, in collaboration with basement.studio
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -90,17 +90,10 @@ function AddCreditCardModal({
</AuthZTooltip>,
]}
>
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
<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>
</Typography.Text>
</Modal>
);
}

View File

@@ -1,9 +1,6 @@
.breadcrumb {
padding: var(--spacing-4) 16px var(--spacing-2);
// The antd breadcrumb provides the item spacing.
--button-padding: 0;
ol {
align-items: center;
}
@@ -11,4 +8,13 @@
:global(.ant-breadcrumb-separator) {
color: var(--muted-foreground);
}
[data-slot='button'] {
padding: 0px;
}
}
.divider {
--divider-color: var(--l1-border);
--divider-margin: 10px 0 16px 0;
}

View File

@@ -28,7 +28,7 @@ function AlertBreadcrumb({
items={breadcrumbItems}
data-testid={testId}
/>
{showDivider && <Divider spacing="10px 16px" />}
{showDivider && <Divider className={styles.divider} />}
</>
);
}

View File

@@ -1,4 +1,5 @@
.itemLast {
color: var(--button-secondary-label);
font-size: var(--button-font-size, var(--periscope-font-size-base));
line-height: var(--button-size-line-height, 16px);
}

View File

@@ -1,6 +1,6 @@
import { Copy } from '@signozhq/icons';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import { Tooltip } from '@signozhq/ui/tooltip';
import cx from 'classnames';
import { useLayoutEffect, useRef, useState } from 'react';

View File

@@ -1,6 +1,6 @@
import { Copy } from '@signozhq/icons';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import { Tooltip } from '@signozhq/ui/tooltip';
import { useCopyToClipboard } from 'react-use';

View File

@@ -26,6 +26,8 @@
}
.brand-title {
font-size: 20px;
font-weight: 600;
color: var(--l1-foreground); // Primary text
margin: 0;
}

View File

@@ -28,29 +28,15 @@ function AppLoading(): JSX.Element {
<div className="brand">
<img src={signozBrandLogoUrl} alt="SigNoz" className="brand-logo" />
<Typography
variant="label"
size="large"
weight="bold"
color="main"
as="h2"
textOverflow="wrap"
>
<Typography.Title level={2} className="brand-title">
SigNoz
</Typography>
</Typography.Title>
</div>
<div className="brand-tagline">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
<Typography.Text>
OpenTelemetry-Native Logs, Metrics and Traces in a single pane
</Typography>
</Typography.Text>
</div>
<div className="loader" />

View File

@@ -1,11 +0,0 @@
// Clears the pagination bars and footers that sit above the bottom strip, and the 56px footer of
// a drawer or dialog panel, which sits 16px from the edge of the window.
.toaster[data-slot='toaster'][data-position^='bottom'] {
bottom: calc(var(--bottom-strip-height, 0px) + var(--spacing-48));
}
// Clears the header of a drawer or dialog panel, 16px from the edge plus 48px or 56px.
:global(body):has([data-slot='drawer-popup'], [data-slot='dialog-popup'])
.toaster[data-slot='toaster'][data-position^='top'] {
top: var(--spacing-40);
}

View File

@@ -1,11 +0,0 @@
import { Toaster } from '@signozhq/ui/toast';
import styles from './AppToaster.module.scss';
function AppToaster(): JSX.Element {
return (
<Toaster position="bottom-right" offset={24} className={styles.toaster} />
);
}
export default AppToaster;

View File

@@ -35,6 +35,7 @@
&__error-code {
color: var(--bg-cherry-100);
font-size: 13px;
font-weight: 500;
line-height: 1;
letter-spacing: -0.065px;
}
@@ -42,6 +43,8 @@
&__error-message {
color: var(--bg-cherry-300);
font-size: 13px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
}
@@ -72,6 +75,7 @@
&__message-item {
color: var(--bg-cherry-300);
font-size: 13px;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;

View File

@@ -45,8 +45,9 @@
}
.auth-footer-text {
font-family: var(--label-small-400-font-family);
font-family: var(--font-family-inter, Inter, sans-serif);
font-size: 11px;
font-weight: 400;
line-height: 1;
color: var(--l2-foreground);
text-align: center;
@@ -56,6 +57,7 @@
display: flex;
align-items: center;
gap: 6px;
text-decoration: none;
transition: opacity 0.2s ease;
&:hover {

View File

@@ -16,6 +16,7 @@
display: flex;
align-items: center;
gap: 4.9px;
text-decoration: none;
}
.auth-header-logo-icon {
@@ -25,7 +26,7 @@
}
.auth-header-logo-text {
font-family: Satoshi, var(--label-medium-500-font-family), sans-serif;
font-family: Satoshi, var(--font-family-inter, Inter), sans-serif;
font-size: 15.4px;
font-weight: 500;
line-height: 17.5px;

View File

@@ -17,3 +17,8 @@
letter-spacing: -0.01em;
color: var(--l1-foreground);
}
.divider {
--divider-color: var(--l1-border);
--divider-margin: 10px 0 16px 0;
}

View File

@@ -44,7 +44,7 @@ function BackHeader({
</Button>
<div className={styles.title}>{title}</div>
</div>
<Divider spacing="10px 16px" />
<Divider className={styles.divider} />
</>
);
}

View File

@@ -23,6 +23,7 @@
.ant-table {
.ant-table-thead > tr > th {
padding: 12px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
@@ -30,7 +31,10 @@
border-bottom: none;
color: var(--l2-foreground);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.44px;
@@ -41,6 +45,7 @@
.ant-table-cell {
padding: 12px;
font-size: 13px;
line-height: 20px;
color: var(--l1-foreground);
background: var(--l1-background);
@@ -62,6 +67,12 @@
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.column-header-right {
text-align: right;
}
.column-header-left {
text-align: left;
}
.ant-table-tbody > tr > td {
border-bottom: none;
}

View File

@@ -40,27 +40,10 @@ const INITIAL_PAGE_SIZE = 20;
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
<>
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
<Typography.Text className="numbers">
{range[0]} &#8212; {range[1]}
</Typography>
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
{' '}
of {total}
</Typography>
</Typography.Text>
<Typography.Text className="total"> of {total}</Typography.Text>
</>
);
@@ -73,15 +56,22 @@ function ProgressRender(item: string | number): JSX.Element {
const percent = Number(Number(item).toFixed(1));
return (
<div className="progress-container">
<div className="progress-bar">
<Progress
percent={percent}
showInfo
color={
percent >= 90 ? 'highlight-danger' : percent >= 60 ? 'warning' : 'success'
<Progress
percent={percent}
strokeLinecap="butt"
showInfo
strokeColor={((): string => {
const cpuPercent = percent;
if (cpuPercent >= 90) {
return Color.BG_SAKURA_500;
}
/>
</div>
if (cpuPercent >= 60) {
return Color.BG_AMBER_500;
}
return Color.BG_FOREST_500;
})()}
className="progress-bar"
/>
</div>
);
}
@@ -536,18 +526,7 @@ export default function CeleryOverviewTable({
),
}}
locale={{
emptyText: isLoading ? null : (
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
No data
</Typography>
),
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,
}}
scroll={{ x: 'max-content' }}
showSorterTooltip

View File

@@ -24,16 +24,9 @@ function CeleryTaskConfigOptions(): JSX.Element {
return (
<div className="celery-task-filters">
<div className="celery-filters">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="visible"
>
<Typography.Text style={{ whiteSpace: 'nowrap' }}>
Task Name
</Typography>
</Typography.Text>
<Select
data-testid="celery-task-name-select"
placeholder="Task Name"

View File

@@ -49,7 +49,11 @@
.subtitle {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
}
@@ -60,6 +64,11 @@
.footer-text {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
}

View File

@@ -65,38 +65,15 @@ export default function CeleryTaskDetail({
width="45%"
title={
<div>
<Typography
variant="label"
size="large"
weight="bold"
color="main"
as="span"
textOverflow="wrap"
>{`Details - ${taskData.entity}`}</Typography>
<Typography.Text className="title">{`Details - ${taskData.entity}`}</Typography.Text>
<div>
<Typography
variant="paragraph"
size="small"
weight="regular"
color="secondary"
as="span"
textOverflow="wrap"
>
<Typography.Text className="subtitle">
{`${formatTimestamp(startTime)} ${
endTime ? `- ${formatTimestamp(endTime)}` : ''
}`}
</Typography>
<Divider orientation="vertical" />
<Typography
variant="paragraph"
size="small"
weight="regular"
color="secondary"
as="span"
textOverflow="wrap"
>
{taskData.value}
</Typography>
</Typography.Text>
<Divider type="vertical" />
<Typography.Text className="subtitle">{taskData.value}</Typography.Text>
</div>
</div>
}
@@ -111,14 +88,7 @@ export default function CeleryTaskDetail({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
footer={
<Typography
variant="paragraph"
size="base"
weight="regular"
color="secondary"
as="span"
textOverflow="wrap"
>{`Total Task: ${totalTask}`}</Typography>
<Typography.Text className="footer-text">{`Total Task: ${totalTask}`}</Typography.Text>
}
>
<CeleryTaskGraph

View File

@@ -52,6 +52,11 @@
display: flex;
justify-content: center;
align-items: center;
.celery-task-graph-worker-count-text {
font-size: 2.5vw;
text-align: center;
}
}
.worker-count-header {
@@ -143,6 +148,8 @@
.configure-option-Info-text {
color: var(--l2-foreground);
font-size: 12px;
font-weight: 400;
}
}
@@ -210,6 +217,7 @@
}
&__label {
font-family: 'Inter';
font-size: 14px;
color: var(--l2-foreground);
line-height: 20px;
@@ -217,7 +225,7 @@
}
&__value {
font-family: var(--code-base-400-font-family);
font-family: 'Geist Mono';
font-size: 24px;
color: var(--l1-foreground);
line-height: 32px;

View File

@@ -113,16 +113,9 @@ export default function CeleryTaskGraphGrid({
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<div className="row-panel">
<div className="row-panel-section">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="secondary"
as="span"
textOverflow="wrap"
>
<Typography.Text className="section-title">
Flower Metrics
</Typography>
</Typography.Text>
{collapsedSections.metricBasedGraphs ? (
<ChevronDown
size={14}
@@ -153,30 +146,14 @@ export default function CeleryTaskGraphGrid({
/>
<Card className="celery-task-graph-worker-count">
<div className="worker-count-header">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
<Typography.Text className="worker-count-header-text">
Worker Online
</Typography>
</Typography.Text>
</div>
<div className="worker-count-text-container">
<Typography
/* TODO(typography): not lifted from className: font-size: 2.5vw */ variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
display="inline-block"
align="center"
textOverflow="wrap"
>
<Typography.Text className="celery-task-graph-worker-count-text">
{options.filter((option) => option.value).length}
</Typography>
</Typography.Text>
</div>
</Card>
</div>
@@ -187,16 +164,9 @@ export default function CeleryTaskGraphGrid({
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<div className="row-panel">
<div className="row-panel-section">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="secondary"
as="span"
textOverflow="wrap"
>
<Typography.Text className="section-title">
Span Based Stats
</Typography>
</Typography.Text>
{collapsedSections.traceBasedGraphs ? (
<ChevronDown
size={14}
@@ -215,16 +185,9 @@ export default function CeleryTaskGraphGrid({
</CardContainer>
<div className="configure-option-Info">
{configureOptionComponent}
<Typography
variant="paragraph"
size="small"
weight="regular"
color="secondary"
as="span"
textOverflow="wrap"
>
<Typography.Text className="configure-option-Info-text">
Click on a graph co-ordinate to see more details
</Typography>
</Typography.Text>
</div>
</div>
{!collapsedSections.traceBasedGraphs && (

View File

@@ -11,7 +11,9 @@
justify-content: center;
}
&__label {
font-family: var(--code-base-400-font-family);
font-family: 'Geist Mono';
font-size: 12px;
line-height: normal;
}
&--positive {
.change-percentage-pill {

View File

@@ -17,6 +17,10 @@
color: var(--l2-foreground);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;

View File

@@ -17,13 +17,13 @@
pre {
color: var(--l2-foreground) !important;
font-family: var(--code-small-400-font-family) !important;
font-family: 'Geist Mono' !important;
font-size: 12px !important;
}
code {
color: var(--l1-foreground) !important;
font-family: var(--code-small-400-font-family) !important;
font-family: 'Geist Mono' !important;
font-size: 12px !important;
}
}

View File

@@ -1,3 +1,36 @@
.create-sa-modal {
max-width: 530px;
background: var(--popover);
border: 1px solid var(--secondary);
border-radius: 4px;
box-shadow: 0 4px 9px 0 rgba(0, 0, 0, 0.04);
[data-slot='dialog-header'] {
padding: var(--padding-4);
border-bottom: 1px solid var(--secondary);
flex-shrink: 0;
background: transparent;
margin: 0;
}
[data-slot='dialog-title'] {
font-size: var(--label-base-400-font-size);
font-weight: var(--label-base-400-font-weight);
line-height: var(--label-base-400-line-height);
letter-spacing: -0.065px;
color: var(--l1-foreground);
margin: 0;
}
[data-slot='dialog-description'] {
padding: 0;
.create-sa-modal__content {
padding: var(--padding-4);
}
}
}
.create-sa-form {
display: flex;
flex-direction: column;
@@ -10,11 +43,32 @@
margin-bottom: var(--spacing-4);
> label {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
letter-spacing: -0.07px;
}
}
&__input {
height: 32px;
color: var(--l1-foreground);
background-color: var(--l2-background);
border-color: var(--l1-border);
font-size: var(--paragraph-base-400-font-size);
border-radius: 2px;
width: 100%;
&::placeholder {
color: var(--l3-foreground);
}
&:focus {
border-color: var(--primary);
box-shadow: none;
}
}
&__error {
font-size: var(--paragraph-small-400-font-size);
color: var(--destructive);
@@ -23,7 +77,22 @@
}
&__helper {
font-size: var(--paragraph-small-400-font-size);
color: var(--l3-foreground);
margin: calc(var(--spacing-2) * -1) 0 var(--spacing-4) 0;
line-height: var(--paragraph-small-400-line-height);
}
}
.create-sa-modal__footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
padding: 0 var(--padding-4);
height: 56px;
min-height: 56px;
border-top: 1px solid var(--secondary);
gap: var(--spacing-4);
flex-shrink: 0;
}

View File

@@ -5,9 +5,9 @@ import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { Dialog } from '@signozhq/ui/dialog';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
invalidateListServiceAccounts,
@@ -35,7 +35,7 @@ function CreateServiceAccountModal(): JSX.Element {
);
const [, setSelectedAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT);
const { showErrorModal } = useErrorModal();
const { showErrorModal, isErrorModalVisible } = useErrorModal();
const {
control,
@@ -82,75 +82,84 @@ function CreateServiceAccountModal(): JSX.Element {
}
return (
<Dialog
<DialogWrapper
title="New Service Account"
open={isOpen}
onClose={handleClose}
width="base"
onOpenChange={(open): void => {
if (!open) {
handleClose();
}
}}
showCloseButton
width="narrow"
className="create-sa-modal"
disableOutsideClick={isErrorModalVisible}
testId="create-service-account-modal"
footerSuffix={
<>
<Button
size="md"
type="button"
variant="solid"
color="secondary"
onClick={handleClose}
testId="create-sa-cancel-btn"
prefix={<X size={12} />}
>
Cancel
</Button>
<AuthZButton
size="md"
checks={[SACreatePermission]}
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
testId="create-sa-submit-btn"
>
Create Service Account
</AuthZButton>
</>
}
>
<AuthZGuardContent
checks={[SACreatePermission]}
fallbackOnLoading={<Skeleton active paragraph={{ rows: 1 }} />}
>
<form
id="create-sa-form"
className="create-sa-form"
onSubmit={handleSubmit(handleCreate)}
<div className="create-sa-modal__content">
<AuthZGuardContent
checks={[SACreatePermission]}
fallbackOnLoading={<Skeleton active paragraph={{ rows: 1 }} />}
>
<div className="create-sa-form__item">
<label htmlFor="sa-name">Name</label>
<Controller
name="name"
control={control}
rules={{ required: 'Name is required' }}
render={({ field }): JSX.Element => (
<Input
id="sa-name"
placeholder="Enter a name"
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
testId="create-sa-name-input"
/>
<form
id="create-sa-form"
className="create-sa-form"
onSubmit={handleSubmit(handleCreate)}
>
<div className="create-sa-form__item">
<label htmlFor="sa-name">Name</label>
<Controller
name="name"
control={control}
rules={{ required: 'Name is required' }}
render={({ field }): JSX.Element => (
<Input
id="sa-name"
placeholder="Enter a name"
className="create-sa-form__input"
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
data-testid="create-sa-name-input"
/>
)}
/>
{errors.name && (
<p className="create-sa-form__error">{errors.name.message}</p>
)}
/>
{errors.name && (
<p className="create-sa-form__error">{errors.name.message}</p>
)}
</div>
</form>
</AuthZGuardContent>
</Dialog>
</div>
</form>
</AuthZGuardContent>
</div>
<DialogFooter className="create-sa-modal__footer">
<Button
size="md"
type="button"
variant="solid"
color="secondary"
onClick={handleClose}
testId="create-sa-cancel-btn"
prefix={<X size={12} />}
>
Cancel
</Button>
<AuthZButton
size="md"
checks={[SACreatePermission]}
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
testId="create-sa-submit-btn"
>
Create Service Account
</AuthZButton>
</DialogFooter>
</DialogWrapper>
);
}

View File

@@ -1,4 +1,4 @@
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import {
setupAuthzAdmin,
setupAuthzDenyAll,
@@ -10,9 +10,9 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
jest.mock('@signozhq/ui/toast', () => ({
...jest.requireActual('@signozhq/ui/toast'),
toast: { success: jest.fn(), danger: jest.fn() },
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
const mockToast = jest.mocked(toast);
@@ -23,6 +23,7 @@ jest.mock('providers/ErrorModalProvider', () => ({
...jest.requireActual('providers/ErrorModalProvider'),
useErrorModal: jest.fn(() => ({
showErrorModal,
isErrorModalVisible: false,
})),
}));
@@ -57,7 +58,7 @@ describe('CreateServiceAccountModal', () => {
// The form only renders once the create check resolves, and the name field
// registers its `required` rule on mount, so the empty-form invalid state
// settles a tick later.
await screen.findByTestId('create-sa-name-input-field');
await screen.findByTestId('create-sa-name-input');
await waitFor(() =>
expect(screen.getByTestId('create-sa-submit-btn')).toHaveAttribute(
@@ -71,7 +72,7 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const nameInput = await screen.findByTestId('create-sa-name-input-field');
const nameInput = await screen.findByTestId('create-sa-name-input');
const submitBtn = await screen.findByTestId('create-sa-submit-btn');
await user.type(nameInput, 'test');
@@ -89,7 +90,7 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const nameInput = await screen.findByTestId('create-sa-name-input-field');
const nameInput = await screen.findByTestId('create-sa-name-input');
await user.type(nameInput, 'Deploy Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
@@ -127,7 +128,7 @@ describe('CreateServiceAccountModal', () => {
renderModal();
const nameInput = await screen.findByTestId<HTMLInputElement>(
'create-sa-name-input-field',
'create-sa-name-input',
);
await user.type(nameInput, 'Deploy Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
@@ -159,7 +160,7 @@ describe('CreateServiceAccountModal', () => {
renderModal();
const nameInput = await screen.findByTestId('create-sa-name-input-field');
const nameInput = await screen.findByTestId('create-sa-name-input');
await user.type(nameInput, 'Dupe Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
@@ -208,9 +209,7 @@ describe('CreateServiceAccountModal', () => {
screen.findByText(/is not authorized to perform/i),
).resolves.toBeInTheDocument();
expect(
screen.queryByTestId('create-sa-name-input-field'),
).not.toBeInTheDocument();
expect(screen.queryByTestId('create-sa-name-input')).not.toBeInTheDocument();
expect(
screen.getByTestId('create-service-account-modal'),
).toBeInTheDocument();
@@ -245,7 +244,7 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const nameInput = await screen.findByTestId('create-sa-name-input-field');
const nameInput = await screen.findByTestId('create-sa-name-input');
await user.type(nameInput, 'Bot');
await user.clear(nameInput);

View File

@@ -65,6 +65,8 @@
.valid-format-error {
margin-top: 4px;
color: var(--bg-cherry-400) !important;
font-size: 13px !important;
font-weight: 400 !important;
}
.info-text {
@@ -74,6 +76,11 @@
padding: 4px;
cursor: default;
color: var(--l2-foreground) !important;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
}
.info-text:hover {
@@ -97,11 +104,15 @@
gap: 6px;
.timezone__name {
font-size: 12px;
line-height: 16px;
color: var(--bg-robin-400);
font-weight: 500;
}
.timezone__separator {
font-size: 12px;
line-height: 16px;
color: var(--bg-robin-300);
font-weight: 500;
}
@@ -217,6 +228,10 @@
border-radius: 2px;
background: color-mix(in srgb, var(--bg-robin-200) 4%, transparent);
color: var(--l1-foreground);
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
cursor: pointer;
height: 20px;
width: 20px;

View File

@@ -531,7 +531,7 @@ function CustomTimePicker({
// Use setTimeout to wait for React to update the DOM and make input editable
setTimeout(() => {
const inputElement = inputRef.current?.input;
if (inputElement && !document.activeElement?.closest('.timezone-picker')) {
if (inputElement) {
inputElement.focus();
inputElement.select();
}

View File

@@ -21,6 +21,7 @@ $item-spacing: 8px;
.timezone-picker {
width: 532px;
color: var(--l2-foreground);
font-family: $font-family;
&__search {
@include flex-center;
@@ -35,6 +36,34 @@ $item-spacing: 8px;
width: -webkit-fill-available;
}
&__input {
@include text-style-base;
width: 100%;
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
outline: none;
height: auto;
color: var(--l1-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
padding: 0;
&:focus,
&:focus-visible,
&:hover {
border: none;
box-shadow: none;
outline: none;
}
&::placeholder {
color: var(--l2-foreground);
}
}
&__esc-key {
@include text-style-base;
font-size: 8px;

View File

@@ -65,8 +65,7 @@ function SearchBar({
/>
<Input
type="text"
variant="unstyled"
noFocusRing
className="timezone-picker__input"
placeholder="Search timezones..."
value={value}
onChange={(e): void => onChange(e.target.value)}

View File

@@ -12,4 +12,5 @@
.name {
color: var(--danger-background);
font-weight: var(--font-weight-medium);
}

View File

@@ -1,4 +1,7 @@
.details-header {
// ghost + secondary missing hover bg token in @signozhq/button
--button-ghost-hover-background: var(--l3-background);
box-sizing: border-box;
display: flex;
align-items: center;
@@ -9,6 +12,7 @@
background: var(--l2-background);
&__title {
font-size: 13px;
font-weight: 500;
color: var(--l1-foreground);
flex: 1;

View File

@@ -0,0 +1,17 @@
.drawer {
overflow: hidden;
border-color: var(--l2-border);
background: var(--l2-background);
box-shadow: -4px 10px 16px 2px var(--l1-shadow);
&:focus,
&:focus-visible {
outline: none;
}
}
.body {
display: flex;
flex-direction: column;
height: 100%;
}

View File

@@ -1,49 +1,56 @@
import { ReactNode } from 'react';
import { Drawer } from '@signozhq/ui/drawer';
import { Drawer, DrawerContent } from '@signozhq/ui/drawer';
import cx from 'classnames';
import { AnimatePresence } from 'motion/react';
import styles from './DetailsPanelDrawer.module.scss';
interface DetailsPanelDrawerProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
headerPrefix?: ReactNode;
headerSuffix?: ReactNode;
loading?: boolean;
children: React.ReactNode;
className?: string;
testId?: string;
}
function DetailsPanelDrawer({
isOpen,
onClose,
title,
children,
headerPrefix,
headerSuffix,
loading,
className,
testId,
}: DetailsPanelDrawerProps): JSX.Element {
return (
<Drawer
type="inspector"
stack="overlay"
open={isOpen}
onClose={onClose}
title={title}
width="extra-wide"
headerPrefix={headerPrefix}
headerSuffix={headerSuffix}
loading={loading}
testId={testId}
onOpenChange={(open): void => {
if (!open) {
onClose();
}
}}
modal={false}
>
{children}
<AnimatePresence>
{isOpen && (
<DrawerContent
forceMount
direction="right"
width="extra-wide"
showOverlay={false}
// Outside presses are left to the page, which knows what counts as inside.
onInteractOutside={(e): void => e.preventDefault()}
className={cx(styles.drawer, className)}
testId={testId}
>
<div className={styles.body}>{children}</div>
</DrawerContent>
)}
</AnimatePresence>
</Drawer>
);
}
DetailsPanelDrawer.defaultProps = {
headerPrefix: undefined,
headerSuffix: undefined,
loading: false,
className: undefined,
testId: undefined,
};

View File

@@ -68,17 +68,7 @@ export default function DownloadOptionsMenu({
aria-modal="true"
>
<div className="export-format">
<Typography
variant="uppercase"
size="small"
weight="medium"
color="main"
as="p"
marginBottom={4}
textOverflow="wrap"
>
FORMAT
</Typography>
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup
color="primary"
value={exportFormat}
@@ -93,17 +83,7 @@ export default function DownloadOptionsMenu({
<div className="horizontal-line" />
<div className="row-limit">
<Typography
variant="uppercase"
size="small"
weight="medium"
color="main"
as="p"
marginBottom={4}
textOverflow="wrap"
>
Number of Rows
</Typography>
<Typography.Text className="title">Number of Rows</Typography.Text>
<RadioGroup
color="primary"
value={String(rowLimit)}
@@ -121,17 +101,7 @@ export default function DownloadOptionsMenu({
<div className="horizontal-line" />
<div className="columns-scope">
<Typography
variant="uppercase"
size="small"
weight="medium"
color="main"
as="p"
marginBottom={4}
textOverflow="wrap"
>
Columns
</Typography>
<Typography.Text className="title">Columns</Typography.Text>
<RadioGroup
color="primary"
value={columnsScope}

View File

@@ -1,5 +1,6 @@
import { Trash2, X } from '@signozhq/icons';
import { AlertDialog } from '@signozhq/ui/alert-dialog';
import { Button } from '@signozhq/ui/button';
import { DialogWrapper } from '@signozhq/ui/dialog';
import { MemberRow } from 'components/MembersTable/MembersTable';
interface DeleteMemberDialogProps {
@@ -8,7 +9,7 @@ interface DeleteMemberDialogProps {
member: MemberRow | null;
isDeleting: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onConfirm: () => void;
}
function DeleteMemberDialog({
@@ -35,20 +36,47 @@ function DeleteMemberDialog({
</>
);
const footer = (
<>
<Button
size="md"
variant="solid"
color="secondary"
onClick={onClose}
prefix={<X size={12} />}
>
Cancel
</Button>
<Button
size="md"
variant="solid"
color="danger"
onClick={onConfirm}
testId="member-delete-confirm-btn"
loading={isDeleting}
prefix={<Trash2 size={12} />}
>
{isDeleting ? 'Processing...' : title}
</Button>
</>
);
return (
<AlertDialog
<DialogWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
onClose();
}
}}
title={title}
color="danger"
confirmText={isDeleting ? 'Processing...' : title}
confirmIcon={<Trash2 size={12} />}
cancelIcon={<X size={12} />}
onConfirm={onConfirm}
onCancel={onClose}
testId="member-delete"
width="narrow"
showCloseButton={false}
disableOutsideClick={false}
footer={footer}
>
{body}
</AlertDialog>
</DialogWrapper>
);
}

View File

@@ -20,9 +20,10 @@
}
&__label {
font-size: var(--paragraph-base-400-font-size);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--foreground);
line-height: var(--paragraph-base-400-line-height);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
cursor: default;
}
@@ -52,8 +53,10 @@
}
&__email-text {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--foreground);
line-height: var(--paragraph-small-400-line-height);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
white-space: nowrap;
overflow: hidden;
@@ -83,26 +86,41 @@
[data-slot='badge'] {
padding: var(--padding-1) var(--padding-2);
align-items: center;
font-size: var(--uppercase-small-500-font-size);
font-weight: var(--uppercase-small-500-font-weight);
line-height: 100%;
letter-spacing: 0.44px;
}
}
&__meta-label {
font-size: var(--uppercase-small-500-font-size);
font-weight: var(--uppercase-small-500-font-weight);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--foreground);
line-height: var(--uppercase-small-500-line-height);
line-height: var(--line-height-20);
letter-spacing: 0.48px;
text-transform: uppercase;
}
&__footer {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
&__footer-left {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
&__footer-right {
display: flex;
align-items: center;
gap: var(--spacing-6);
}
&__footer-divider {
width: 1px;
height: 21px;
@@ -145,6 +163,14 @@
}
.reset-link-dialog {
background: var(--l2-background);
border: 1px solid var(--l1-border);
[data-slot='dialog-header'] {
border-color: var(--l1-border);
color: var(--l1-foreground);
}
&__content {
display: flex;
flex-direction: column;
@@ -152,6 +178,8 @@
}
&__description {
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l2-foreground);
line-height: var(--paragraph-base-400-line-height);
letter-spacing: -0.065px;
@@ -183,8 +211,10 @@
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--padding-2);
font-size: var(--paragraph-base-400-font-size);
font-weight: var(--paragraph-base-400-font-weight);
color: var(--l2-foreground);
line-height: var(--paragraph-small-400-line-height);
line-height: var(--line-height-18);
letter-spacing: -0.07px;
}
}

View File

@@ -3,9 +3,9 @@ import { useCopyToClipboard } from 'react-use';
import { LockKeyhole, RefreshCw, Trash2, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Drawer } from '@signozhq/ui/drawer';
import { DrawerWrapper } from '@signozhq/ui/drawer';
import { Input } from '@signozhq/ui/input';
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import { Skeleton, Tooltip } from 'antd';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
@@ -210,7 +210,7 @@ function EditMemberDrawer({
const { mutateAsync: updateMyUser } = useUpdateMyUserV2();
const { mutateAsync: updateUser } = useUpdateUser();
const { mutateAsync: deleteUser, isLoading: isDeleting } = useDeleteUser({
const { mutate: deleteUser, isLoading: isDeleting } = useDeleteUser({
mutation: {
onSuccess: (): void => {
toast.success(
@@ -378,15 +378,11 @@ function EditMemberDrawer({
onComplete,
]);
const handleDelete = useCallback(async (): Promise<void> => {
const handleDelete = useCallback((): void => {
if (!member) {
return;
}
try {
await deleteUser({ pathParams: { id: member.id } });
} catch {
// `onError` already shows the error modal, and the confirmation stays open.
}
deleteUser({ pathParams: { id: member.id } });
}, [member, deleteUser]);
const handleGenerateResetLink = useCallback(async (): Promise<void> => {
@@ -413,8 +409,7 @@ function EditMemberDrawer({
setShowResetLinkDialog(true);
onClose();
} else {
toast.danger('Failed to generate password reset link', {
action: { label: 'Dismiss' },
toast.error('Failed to generate password reset link', {
position: 'top-right',
});
}
@@ -443,7 +438,7 @@ function EditMemberDrawer({
useEffect(() => {
if (copyState.error) {
toast.danger('Failed to copy link', { action: { label: 'Dismiss' } });
toast.error('Failed to copy link');
}
}, [copyState.error]);
@@ -476,19 +471,20 @@ function EditMemberDrawer({
<label className="edit-member-drawer__label" htmlFor="member-name">
Name
</label>
<Input
id="member-name"
value={localDisplayName}
onChange={(e): void => {
setLocalDisplayName(e.target.value);
setSaveErrors((prev) =>
prev.filter((err) => err.context !== 'Name update'),
);
}}
placeholder="Enter name"
disabled={isRootUser || isDeleted}
disabledTooltip={isRootUser ? ROOT_USER_TOOLTIP : undefined}
/>
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
<Input
id="member-name"
value={localDisplayName}
onChange={(e): void => {
setLocalDisplayName(e.target.value);
setSaveErrors((prev) =>
prev.filter((err) => err.context !== 'Name update'),
);
}}
placeholder="Enter name"
disabled={isRootUser || isDeleted}
/>
</Tooltip>
</div>
<div className="edit-member-drawer__field">
@@ -619,88 +615,96 @@ function EditMemberDrawer({
</div>
);
const footerStart = isDeleted ? null : (
<div className="edit-member-drawer__footer-left">
<Button
disabledTooltip={getDeleteTooltip(isRootUser, isSelf)}
size="md"
onClick={(): void => setShowDeleteConfirm(true)}
testId="member-delete-btn"
disabled={isRootUser || isSelf}
variant="link"
color="danger"
prefix={<Trash2 size={12} />}
>
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
const footer = (
<div className="edit-member-drawer__footer">
{!isDeleted && (
<>
<div className="edit-member-drawer__footer-left">
<Button
disabledTooltip={getDeleteTooltip(isRootUser, isSelf)}
size="md"
onClick={(): void => setShowDeleteConfirm(true)}
testId="member-delete-btn"
disabled={isRootUser || isSelf}
variant="link"
color="danger"
prefix={<Trash2 size={12} />}
>
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
<div className="edit-member-drawer__footer-divider" />
<Button
disabledTooltip={ROOT_USER_TOOLTIP}
size="md"
onClick={handleGenerateResetLink}
testId="member-reset-link-btn"
disabled={isRootUser}
loading={isGeneratingLink || isLoadingTokenStatus}
variant="link"
color="warning"
prefix={<RefreshCw size={12} />}
>
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
<div className="edit-member-drawer__footer-divider" />
<Button
disabledTooltip={ROOT_USER_TOOLTIP}
size="md"
onClick={handleGenerateResetLink}
testId="member-reset-link-btn"
disabled={isRootUser}
loading={isGeneratingLink || isLoadingTokenStatus}
variant="link"
color="warning"
prefix={<RefreshCw size={12} />}
>
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
</div>
<div className="edit-member-drawer__footer-right">
<Button
size="md"
variant="outlined"
color="secondary"
onClick={handleClose}
prefix={<X size={14} />}
>
Cancel
</Button>
<Button
disabledTooltip={isRootUser ? ROOT_USER_TOOLTIP : 'No changes to save'}
size="md"
variant="solid"
color="primary"
disabled={!isDirty || isRootUser}
onClick={handleSave}
testId="member-save-btn"
loading={isSaving}
>
{isSaving ? 'Saving...' : 'Save Member Details'}
</Button>
</div>
</>
)}
</div>
);
const footerActions = isDeleted ? null : (
<>
<Button
size="md"
variant="outlined"
color="secondary"
onClick={handleClose}
prefix={<X size={14} />}
>
Cancel
</Button>
<Button
disabledTooltip={isRootUser ? ROOT_USER_TOOLTIP : 'No changes to save'}
size="md"
variant="solid"
color="primary"
disabled={!isDirty || isRootUser}
onClick={handleSave}
testId="member-save-btn"
loading={isSaving}
>
{isSaving ? 'Saving...' : 'Save Member Details'}
</Button>
</>
);
return (
<>
<Drawer
type="task"
stack="overlay"
<DrawerWrapper
open={open}
onClose={handleClose}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
showCloseButton
showOverlay={false}
title="Member Details"
footer={footer}
width="wide"
footerPrefix={footerStart}
footerSuffix={footerActions}
>
{drawerContent}
</Drawer>
</DrawerWrapper>
<ResetLinkDialog
open={showResetLinkDialog}

View File

@@ -1,6 +1,6 @@
import { Check, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Dialog } from '@signozhq/ui/dialog';
import { DialogWrapper } from '@signozhq/ui/dialog';
interface ResetLinkDialogProps {
open: boolean;
@@ -22,11 +22,17 @@ function ResetLinkDialog({
onCopy,
}: ResetLinkDialogProps): JSX.Element {
return (
<Dialog
<DialogWrapper
open={open}
onClose={onClose}
onOpenChange={(isOpen): void => {
if (!isOpen) {
onClose();
}
}}
title={linkType === 'invite' ? 'Invite Link' : 'Password Reset Link'}
showCloseButton
width="base"
className="reset-link-dialog"
>
<div className="reset-link-dialog__content">
<p className="reset-link-dialog__description">
@@ -59,7 +65,7 @@ function ResetLinkDialog({
</p>
)}
</div>
</Dialog>
</DialogWrapper>
);
}

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
useCreateResetPasswordToken,
@@ -41,31 +41,52 @@ jest.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
jest.mock('@signozhq/ui/drawer', () => ({
...jest.requireActual('@signozhq/ui/drawer'),
Drawer: ({
DrawerWrapper: ({
children,
footerPrefix,
footerSuffix,
footer,
open,
}: {
children?: ReactNode;
footerPrefix?: ReactNode;
footerSuffix?: ReactNode;
footer?: ReactNode;
open: boolean;
}): JSX.Element | null =>
open ? (
<div>
{children}
{footerPrefix}
{footerSuffix}
{footer}
</div>
) : null,
}));
jest.mock('@signozhq/ui/toast', () => ({
...jest.requireActual('@signozhq/ui/toast'),
jest.mock('@signozhq/ui/dialog', () => ({
...jest.requireActual('@signozhq/ui/dialog'),
DialogWrapper: ({
children,
footer,
open,
title,
}: {
children?: ReactNode;
footer?: ReactNode;
open: boolean;
title?: string;
}): JSX.Element | null =>
open ? (
<div role="dialog" aria-label={title}>
{children}
{footer}
</div>
) : null,
DialogFooter: ({ children }: { children?: ReactNode }): JSX.Element => (
<div>{children}</div>
),
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: {
success: jest.fn(),
danger: jest.fn(),
error: jest.fn(),
},
}));
@@ -188,7 +209,7 @@ describe('EditMemberDrawer', () => {
isLoading: false,
});
(useDeleteUser as jest.Mock).mockReturnValue({
mutateAsync: mockDeleteMutate,
mutate: mockDeleteMutate,
isLoading: false,
});
// Token query: valid token for invited members
@@ -352,7 +373,7 @@ describe('EditMemberDrawer', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
mutateAsync: mockDeleteMutate.mockImplementation(() => {
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onSuccess?.();
}),
isLoading: false,
@@ -432,7 +453,7 @@ describe('EditMemberDrawer', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
mutateAsync: mockDeleteMutate.mockImplementation(() => {
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onSuccess?.();
}),
isLoading: false,
@@ -546,7 +567,7 @@ describe('EditMemberDrawer', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
mutateAsync: mockDeleteMutate.mockImplementation(() => {
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onError?.({});
}),
isLoading: false,
@@ -577,7 +598,7 @@ describe('EditMemberDrawer', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
mutateAsync: mockDeleteMutate.mockImplementation(() => {
mutate: mockDeleteMutate.mockImplementation(() => {
options?.mutation?.onError?.({});
}),
isLoading: false,

View File

@@ -1,16 +1,22 @@
// The mask of the error modal blurs the page and a dialog opened under it, and the
// dialog's own backdrop would otherwise dim the error modal that sits above the page.
body:has(.error-modal__wrap:not([style*='display: none'])) {
--dialog-backdrop-color: transparent;
--alert-dialog-backdrop-color: transparent;
}
.error-modal {
&__trigger {
width: fit-content;
display: flex;
align-items: center;
gap: 4px;
border-radius: 20px;
background: color-mix(in srgb, var(--danger-background) 20%, transparent);
padding-left: 3px;
padding-right: 8px;
cursor: pointer;
span {
color: var(--danger-background);
font-size: 10px;
font-weight: 500;
line-height: 20px; /* 200% */
letter-spacing: 0.4px;
text-transform: uppercase;
}
}
&__wrap {
background: linear-gradient(
@@ -59,6 +65,7 @@ body:has(.error-modal__wrap:not([style*='display: none'])) {
&__value {
padding: 4px 8px;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.48px;

View File

@@ -53,13 +53,8 @@ function ErrorModal({
onClick={(): void => setVisible(true)}
onKeyDown={undefined}
>
<Badge
variant="solid"
color="danger"
textTransform="uppercase"
prefix={<CircleAlert size={14} />}
>
error
<Badge variant="solid" color="danger">
<CircleAlert size={14} color={Color.BG_CHERRY_500} /> error
</Badge>
</span>
) : (

View File

@@ -38,6 +38,7 @@
margin: 0;
color: var(--l2-foreground);
font-size: 14px;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -48,6 +49,8 @@
gap: 6px;
padding: 9px 12.5px;
color: var(--l2-foreground);
font-size: 12px;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.12px;
border-radius: 2px;
@@ -76,6 +79,7 @@
color: var(--l2-foreground);
font-size: 12px;
font-weight: 500;
line-height: 18px; /* 150% */
letter-spacing: 0.48px;
pointer-events: none;
}
@@ -94,6 +98,7 @@
color: var(--l1-foreground);
font-size: 10px;
font-weight: 500;
line-height: 18px; /* 180% */
letter-spacing: 0.5px;
}
}
@@ -128,12 +133,16 @@
position: relative;
margin-bottom: 4px;
color: var(--l2-foreground);
font-family: var(--code-base-400-font-family);
font-family: Geist Mono;
font-size: 12px;
font-weight: 400;
line-height: 18px;
padding: 3px 12px;
padding-left: 26px;
}
&__message-item::before {
font-family: unset;
content: '';
position: absolute;
left: 12px;

View File

@@ -1,12 +1,12 @@
.export-menu-popover {
// The popover shell carries no padding of its own, so the content owns it.
padding: var(--spacing-8);
width: 240px;
padding: 0 12px 12px 12px;
.title {
display: flex;
color: var(--l1-foreground);
font-family: Inter;
font-size: var(--uppercase-small-500-font-size);
font-size: var(--periscope-font-size-small);
font-style: normal;
font-weight: 500;
line-height: 18px;
@@ -23,7 +23,7 @@
// radio option labels — radix popover content inherits the root font
// size; pin to the app's 13px base the antd popover used to impose
label {
font-size: var(--label-base-400-font-size);
font-size: var(--periscope-font-size-base);
}
}
}

View File

@@ -1,6 +1,6 @@
import { Download } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Popover, usePopoverHandle } from '@signozhq/ui/popover';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { RadioGroup } from '@signozhq/ui/radio-group';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
@@ -36,10 +36,6 @@ export default function ExportMenu({
}: ExportMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(ExportFormat.Csv);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
// Driven by a handle rather than `trigger`: the button carries a Tooltip, and a Tooltip
// routes every prop it does not recognise to its own popup, so a popover's press handlers
// would never reach the button underneath.
const menu = usePopoverHandle(setIsPopoverOpen);
const { isExporting, handleExport: handleClientExport } = useClientExport({
data,
@@ -49,72 +45,53 @@ export default function ExportMenu({
});
const handleExport = useCallback((): void => {
menu.close();
setIsPopoverOpen(false);
handleClientExport({ format: exportFormat as ExportFormat });
}, [menu, exportFormat, handleClientExport]);
}, [exportFormat, handleClientExport]);
return (
<>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<Tooltip title="Download">
<Button
variant="ghost"
color="secondary"
size="sm"
icon
aria-label="Download"
aria-haspopup="dialog"
aria-expanded={isPopoverOpen}
testId={`export-menu-${dataSource}`}
loading={isExporting}
onClick={(event): void => menu.showAt(event.currentTarget)}
>
<Download size={14} />
</Button>
</Tooltip>
<Popover
side="bottom"
align="end"
width={240}
handle={menu}
aria-label="Export format"
>
<div className="export-menu-popover">
<div className="export-format">
<Typography
variant="uppercase"
size="small"
weight="medium"
color="main"
as="p"
marginBottom={4}
textOverflow="wrap"
>
FORMAT
</Typography>
<RadioGroup
color="primary"
value={exportFormat}
onChange={setExportFormat}
items={[
{ value: ExportFormat.Csv, label: 'csv' },
{ value: ExportFormat.Jsonl, label: 'jsonl' },
]}
/>
</div>
<PopoverTrigger asChild>
<Button
size="md"
variant="solid"
color="primary"
width="100%"
onClick={handleExport}
variant="ghost"
color="secondary"
size="sm"
icon
aria-label="Download"
testId={`export-menu-${dataSource}`}
loading={isExporting}
prefix={<Download size={16} />}
>
Export
<Download size={14} />
</Button>
</PopoverTrigger>
</Tooltip>
<PopoverContent align="end" className="export-menu-popover">
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup
color="primary"
value={exportFormat}
onChange={setExportFormat}
items={[
{ value: ExportFormat.Csv, label: 'csv' },
{ value: ExportFormat.Jsonl, label: 'jsonl' },
]}
/>
</div>
</Popover>
</>
<Button
size="md"
variant="solid"
color="primary"
width="100%"
onClick={handleExport}
loading={isExporting}
prefix={<Download size={16} />}
>
Export
</Button>
</PopoverContent>
</Popover>
);
}

View File

@@ -117,16 +117,9 @@ function AddedFields({
<div className={styles.sectionHeader}>
<span>ADDED FIELDS</span>
{maxFields !== undefined && (
<Typography
variant="paragraph"
size="small"
weight="medium"
color="secondary"
as="span"
textOverflow="wrap"
>
<Typography.Text size="sm" weight="medium" color="muted">
Max Allowed: {maxFields}
</Typography>
</Typography.Text>
)}
</div>
<div className={styles.addedList}>

View File

@@ -32,15 +32,12 @@
}
}
// Full-bleed search bar: only top/bottom borders, square corners.
// Per-side widths cannot be expressed via --input-border-width.
.searchInput {
--input-border-radius: 0;
[data-slot='input'] {
border-left: none;
border-right: none;
}
background-color: var(--l1-background);
height: 40px;
border-radius: 0;
border-left: none;
border-right: none;
}
.section {

View File

@@ -1,5 +1,5 @@
import { useCallback, useMemo, useState } from 'react';
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import useDebouncedFn from 'hooks/useDebouncedFunction';
@@ -140,10 +140,10 @@ function FieldsSelectorContent({
<X className={styles.closeIcon} size={16} onClick={onClose} />
</div>
<section className={styles.searchInput}>
<section>
<Input
className={styles.searchInput}
type="text"
size="large"
value={inputValue}
placeholder="Search for a field..."
onChange={handleInputChange}

View File

@@ -13,9 +13,9 @@ jest.mock('hooks/querySuggestions/useFieldKeysSuggestion', () => ({
})),
}));
jest.mock('@signozhq/ui/toast', () => ({
...jest.requireActual('@signozhq/ui/toast'),
toast: { success: jest.fn(), danger: jest.fn() },
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
}));
// FloatingPanel is a react-rnd/portal shell — presentation only. Render its

View File

@@ -4,16 +4,9 @@ function AnnouncementsModal(): JSX.Element {
return (
<div className="announcements-modal-container">
<div className="announcements-modal-container-header">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
<Typography.Text className="announcements-modal-title">
Announcements
</Typography>
</Typography.Text>
</div>
</div>
);

View File

@@ -1,9 +1,8 @@
import { useCallback, useEffect, useState } from 'react';
import { useAppLocation } from 'lib/router/useAppLocation';
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import { Button, Input } from 'antd';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Button as SignozButton } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
@@ -40,8 +39,7 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
})
.catch(() => {
console.error(`Failed to submit ${entityName}`);
toast.danger(`Failed to submit ${entityName}`, {
action: { label: 'Dismiss' },
toast.error(`Failed to submit ${entityName}`, {
position: 'top-right',
});
})
@@ -138,23 +136,14 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
Submit
</Button>
<div className="feedback-modal-content-footer-info-text">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
<Typography.Text>
Have a specific issue?{' '}
<SignozButton
variant="link"
color="primary"
size="sm"
<Typography.Link
className="contact-support-link"
onClick={handleContactSupportClick}
>
Contact Support{' '}
</SignozButton>
</Typography.Link>
or{' '}
<a
href="https://signoz.io/docs/introduction/"
@@ -164,7 +153,7 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
>
Read our docs
</a>
</Typography>
</Typography.Text>
</div>
</div>
</div>

View File

@@ -68,6 +68,11 @@
.absolute-relative-time-toggler-label {
color: var(--l1-foreground);
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}

View File

@@ -128,16 +128,7 @@ function HeaderRightSection({
}
prefix={<Noz size={20} />}
>
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
Noz
</Typography>
<Typography.Text>Noz</Typography.Text>
</Button>
</Tooltip>
</span>

View File

@@ -141,16 +141,9 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
{(shareURLWithTime || isRouteToBeSharedWithTime) && (
<>
<div className="absolute-relative-time-toggler-container">
<Typography
variant="paragraph"
size="base"
weight="medium"
color="main"
as="span"
textOverflow="wrap"
>
<Typography.Text className="absolute-relative-time-toggler-label">
Enable absolute time
</Typography>
</Typography.Text>
<div className="absolute-relative-time-toggler">
{!isValidateRelativeTime && (
@@ -179,16 +172,9 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
{extraOption && (
<div className="absolute-relative-time-toggler-container">
<Typography
variant="paragraph"
size="base"
weight="medium"
color="main"
as="span"
textOverflow="wrap"
>
<Typography.Text className="absolute-relative-time-toggler-label">
{extraOption.label}
</Typography>
</Typography.Text>
<div className="absolute-relative-time-toggler">
<Switch
color="primary"
@@ -203,26 +189,12 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
<div className="share-link">
<div className="url-share-container">
<div className="url-share-container-header">
<Typography
variant="paragraph"
size="base"
weight="medium"
color="main"
as="span"
textOverflow="wrap"
>
<Typography.Text className="url-share-title">
Share page link
</Typography>
<Typography
variant="paragraph"
size="small"
weight="regular"
color="tertiary"
as="span"
textOverflow="wrap"
>
</Typography.Text>
<Typography.Text className="url-share-sub-title">
Share the current page link with your team member
</Typography>
</Typography.Text>
</div>
<Button

View File

@@ -1,6 +1,6 @@
// Mock dependencies before imports
import { useAppLocation } from 'lib/router/useAppLocation';
import { toast } from '@signozhq/ui/toast';
import { toast } from '@signozhq/ui/sonner';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { logEventMock } from '__tests__/logEventMock';
@@ -13,11 +13,11 @@ jest.mock('lib/router/useAppLocation', () => ({
useAppLocation: jest.fn(),
}));
jest.mock('@signozhq/ui/toast', () => ({
...jest.requireActual('@signozhq/ui/toast'),
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: {
success: jest.fn(),
danger: jest.fn(),
error: jest.fn(),
},
}));
@@ -49,7 +49,7 @@ describe('FeedbackModal', () => {
isCloudUser: false,
});
mockToast.success.mockClear();
mockToast.danger.mockClear();
mockToast.error.mockClear();
});
it('should render feedback modal with all tabs', () => {

View File

@@ -27,7 +27,7 @@
}
.infra-container-card-text {
font-size: var(--paragraph-base-400-font-size);
font-size: var(--font-size-sm);
color: var(--l2-foreground);
line-height: 20px;
letter-spacing: -0.07px;

View File

@@ -27,7 +27,7 @@
}
.infra-container-card-text {
font-size: var(--paragraph-base-400-font-size);
font-size: var(--font-size-sm);
color: var(--l2-foreground);
line-height: 20px;
letter-spacing: -0.07px;

View File

@@ -52,16 +52,9 @@ export default function WaitlistFragment({
return (
<div className="wait-list-container">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
<Typography.Text className="wait-list-text">
{t('waitlist_message')}
</Typography>
</Typography.Text>
<Button
className="periscope-btn join-waitlist-btn"

View File

@@ -4,9 +4,6 @@
border-radius: 2px 0px 0px 2px;
// The label chip and the Input share the --input-with-label-* theming
// contract (set by e.g. FormAlertRules, WidgetFullView) so the joined
// group recolors as one.
.label {
// Typography.Text takes its display from this token; at its `inline` default
// the label blockifies as a flex item and the text rides the top of the row.
@@ -34,41 +31,66 @@
display: flex;
justify-content: flex-start;
align-items: center;
font-weight: var(--font-weight-light);
}
.input {
flex: 1;
min-width: 150px;
font-family: 'Space Mono', monospace !important;
--input-border-radius: 0px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
font-size: 12px !important;
line-height: 25px;
position: relative;
// Square corners: the input sits mid-group between label and close button.
--input-border-radius: 0px;
--input-height: 36px;
--input-border: var(--input-with-label-border-color, var(--l2-border));
--input-background: var(
--input-with-label-background-color,
var(--l2-background)
);
--input-foreground: var(--input-with-label-color, var(--l2-foreground));
&:hover,
&:focus-within {
&:focus {
z-index: 1;
}
.ant-select-selector {
position: relative;
border-radius: inherit;
}
.ant-select:hover .ant-select-selector,
.ant-select-focused .ant-select-selector {
z-index: 1;
}
&.input__has-label-after {
margin-left: -1px;
.ant-select-selector {
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&.input__has-close-button {
.ant-select-selector {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
~ .close-btn {
margin-left: -1px;
}
}
// The Input has no token for the native number spinners.
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
&::placeholder {
color: var(--input-with-label-color, var(--l3-foreground)) !important;
font-size: 12px !important;
}
&[type='number']::-webkit-inner-spin-button,
&[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
@@ -92,7 +114,16 @@
&.labelAfter {
.input {
--input-border-radius: 2px 0 0 2px;
border-radius: 2px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
.ant-select-selector {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.label {

View File

@@ -49,51 +49,21 @@ function InputWithLabel({
labelAfter,
})}
>
{!labelAfter && (
<div className="label">
<Typography
variant="paragraph"
size="small"
weight="regular"
color="secondary"
as="p"
textOverflow="ellipsis"
>
{label}
</Typography>
</div>
)}
<div
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
className={cx('input', {
'input__has-label-after': !labelAfter,
'input__has-close-button': !!onClose,
})}
>
<Input
placeholder={placeholder}
type={type}
value={inputValue}
onChange={handleChange}
disabled={disabled}
disabledTooltip={undefined}
name={label.toLowerCase()}
testId={`input-${label}`}
/>
</div>
{labelAfter && (
<div className="label">
<Typography
variant="paragraph"
size="small"
weight="regular"
color="secondary"
as="p"
textOverflow="ellipsis"
>
{label}
</Typography>
</div>
)}
placeholder={placeholder}
type={type}
value={inputValue}
onChange={handleChange}
disabled={disabled}
name={label.toLowerCase()}
data-testid={`input-${label}`}
/>
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
{onClose && (
<Button
className="periscope-btn ghost close-btn"

View File

@@ -55,12 +55,26 @@
flex: 1 1 0;
min-width: 0;
// Matches the antd RolesSelect in the same row, which is forced to the
// same field background below.
--input-background: var(
--invite-members-field-background,
var(--l2-background)
);
--input-hover-background: var(
--invite-members-field-background,
var(--l2-background)
);
--input-focus-background: var(
--invite-members-field-background,
var(--l2-background)
);
--input-disabled-background: var(
--invite-members-field-background,
var(--l2-background)
);
input::placeholder {
color: var(--l3-foreground);
}
}
.cellRole {

View File

@@ -1,4 +1,4 @@
import { CircleAlert, Plus, SolidCheckCircle2, Trash2 } from '@signozhq/icons';
import { CircleAlert, Plus, Trash2 } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { Input } from '@signozhq/ui/input';
@@ -68,30 +68,20 @@ function InviteMembers({
<div className={styles.table}>
{showHeader && (
<div className={styles.header}>
<span className={styles.headerCellEmail}>
<Typography
variant="paragraph"
size="base"
weight="bold"
color="inherit"
as="span"
textOverflow="wrap"
>
Email address
</Typography>
</span>
<span className={styles.headerCellRole}>
<Typography
variant="paragraph"
size="base"
weight="bold"
color="inherit"
as="span"
textOverflow="wrap"
>
Roles
</Typography>
</span>
<Typography.Text
size="base"
weight="semibold"
className={styles.headerCellEmail}
>
Email address
</Typography.Text>
<Typography.Text
size="base"
weight="semibold"
className={styles.headerCellRole}
>
Roles
</Typography.Text>
<div className={styles.headerCellAction} />
</div>
)}
@@ -107,19 +97,12 @@ function InviteMembers({
onChange={(e): void => updateEmail(row.id, e.target.value)}
name={`invite-email-${row.id}`}
autoComplete="email"
testId={`invite-email-${row.id}`}
data-testid={`invite-email-${row.id}`}
/>
{emailValidity[row.id] === false && row.email.trim() !== '' && (
<Typography
variant="paragraph"
size="small"
weight="regular"
color="danger"
as="span"
textOverflow="wrap"
>
<Typography.Text size="small" className={styles.errorText}>
Invalid email address
</Typography>
</Typography.Text>
)}
</div>
@@ -169,90 +152,61 @@ function InviteMembers({
</div>
{hasValidationErrors && (
<div className={styles.callout}>
<Callout
color="danger"
size="sm"
icon={<CircleAlert />}
testId="invite-validation-error"
>
{getValidationErrorMessage()}
</Callout>
</div>
<Callout
type="error"
size="small"
showIcon
icon={<CircleAlert size={12} />}
className={styles.callout}
data-testid="invite-validation-error"
>
{getValidationErrorMessage()}
</Callout>
)}
{hasResults && hasFailures && (
<div className={styles.callout}>
<Callout
color="danger"
size="sm"
icon={<CircleAlert />}
testId="invite-api-error"
>
<div className={styles.results}>
{hasSuccesses && (
<Typography
variant="paragraph"
size="small"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
{successResults.length} invite(s) sent successfully.
</Typography>
)}
<Typography
variant="paragraph"
size="small"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
{failedResults.length} invite(s) failed:
</Typography>
<ul className={styles.resultsList}>
{failedResults.map((result) => (
<li key={result.email}>
<Typography
variant="paragraph"
size="small"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
{result.email}: {result.error}
</Typography>
</li>
))}
</ul>
</div>
</Callout>
</div>
<Callout
type="error"
size="small"
showIcon
icon={<CircleAlert size={12} />}
className={styles.callout}
data-testid="invite-api-error"
>
<div className={styles.results}>
{hasSuccesses && (
<Typography.Text size="small">
{successResults.length} invite(s) sent successfully.
</Typography.Text>
)}
<Typography.Text size="small">
{failedResults.length} invite(s) failed:
</Typography.Text>
<ul className={styles.resultsList}>
{failedResults.map((result) => (
<li key={result.email}>
<Typography.Text size="small">
{result.email}: {result.error}
</Typography.Text>
</li>
))}
</ul>
</div>
</Callout>
)}
{hasResults && !hasFailures && hasSuccesses && (
<div className={styles.callout}>
<Callout
color="success"
size="sm"
icon={<SolidCheckCircle2 />}
testId="invite-success"
>
<Typography
variant="paragraph"
size="small"
weight="regular"
color="inherit"
as="span"
textOverflow="wrap"
>
{successResults.length} invite(s) sent successfully!
</Typography>
</Callout>
</div>
<Callout
type="success"
size="small"
showIcon
className={styles.callout}
data-testid="invite-success"
>
<Typography.Text size="small">
{successResults.length} invite(s) sent successfully!
</Typography.Text>
</Callout>
)}
{renderFooter?.({

View File

@@ -25,6 +25,10 @@
}
.filtersLabel {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--l2-foreground);
margin-right: 2px;
}
@@ -103,6 +107,6 @@
}
.error {
font-size: var(--label-small-400-font-size);
font-size: var(--font-size-xs);
color: var(--danger-foreground);
}

View File

@@ -116,31 +116,15 @@ function FilterZone({
{rightSlot}
</div>
{error && (
<Typography
variant="paragraph"
size="small"
weight="regular"
color="danger"
as="span"
textOverflow="wrap"
<Typography.Text
className={styles.error}
testId={`${testIdPrefix}-query-error`}
>
{error}
</Typography>
</Typography.Text>
)}
<div className={styles.filtersRow}>
<Typography
variant="uppercase"
size="small"
weight="medium"
color="secondary"
as="span"
display="inline-block"
marginRight={1}
textOverflow="wrap"
>
Filters
</Typography>
<Typography.Text className={styles.filtersLabel}>Filters</Typography.Text>
{wrapControl(
<div
className={styles.filterControls}

View File

@@ -25,7 +25,7 @@
:global(.cm-dsl-logic) {
color: var(--bg-sakura-400);
font-weight: var(--label-base-500-font-weight);
font-weight: var(--font-weight-medium);
}
:global(.cm-dsl-string) {
@@ -59,7 +59,7 @@
content: 'SUGGESTIONS';
padding: 12px 14px 8px;
font-size: 11px;
font-weight: var(--uppercase-small-500-font-weight);
font-weight: var(--font-weight-medium);
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
@@ -71,7 +71,7 @@
padding: 10px 14px;
border-top: 1px solid var(--l1-border);
background: var(--l2-background);
font-size: var(--label-small-400-font-size);
font-size: var(--font-size-xs);
color: var(--l2-foreground);
}
@@ -131,7 +131,7 @@
border-radius: 50px;
background: color-mix(in srgb, var(--l1-foreground) 10%, transparent);
font-style: normal;
font-size: var(--label-small-400-font-size);
font-size: var(--font-size-xs);
color: var(--l2-foreground);
text-transform: capitalize;
}

View File

@@ -5,8 +5,6 @@
}
.headingBlock {
/* The Badge height, so the count does not jump when it loads. */
--skeleton-input-sm-height: var(--spacing-10);
display: flex;
align-items: center;
gap: 10px;
@@ -25,11 +23,21 @@
flex: none;
}
.countPill {
padding: 2px 9px;
border-radius: 999px;
background: var(--l2-background);
border: 1px solid var(--l2-border);
color: var(--l2-foreground);
font-size: var(--font-size-xs);
font-variant-numeric: tabular-nums;
}
.title {
color: var(--l1-foreground);
font-size: var(--label-large-500-font-size);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--label-large-500-font-weight);
font-weight: var(--font-weight-medium);
line-height: 28px;
letter-spacing: -0.09px;
overflow: hidden;

View File

@@ -1,6 +1,4 @@
import type { ReactNode } from 'react';
import { Badge } from '@signozhq/ui/badge';
import { Skeleton } from '@signozhq/ui/skeleton';
import { Typography } from '@signozhq/ui/typography';
import styles from './ListCommandHeader.module.scss';
@@ -9,7 +7,6 @@ interface Props {
label: string;
count: number;
isModified: boolean;
isCountLoading?: boolean;
action?: ReactNode;
// Ids: `${testIdPrefix}-list-heading`.
testIdPrefix?: string;
@@ -19,33 +16,20 @@ function ListCommandHeader({
label,
count,
isModified,
isCountLoading,
action,
testIdPrefix,
}: Props): JSX.Element {
return (
<div className={styles.commandHeader}>
<div className={styles.headingBlock}>
<Typography
variant="label"
size="large"
weight="medium"
color="main"
as="h1"
marginY={4}
textOverflow="ellipsis"
<Typography.Title
className={styles.title}
testId={testIdPrefix && `${testIdPrefix}-list-heading`}
>
{label}
</Typography>
</Typography.Title>
{isModified && <span className={styles.dirtyDot} title="Unsaved changes" />}
{isCountLoading ? (
<Skeleton.Input size="sm" width={24} />
) : (
<Badge variant="solid" color="secondary">
{count}
</Badge>
)}
<span className={styles.countPill}>{count}</span>
</div>
<div className={styles.grow} />
{action}

View File

@@ -10,6 +10,6 @@
}
.count {
font-size: var(--label-small-400-font-size);
font-size: var(--font-size-xs);
color: var(--l3-foreground);
}

View File

@@ -36,16 +36,9 @@ function ListStatusBar({
>
{collapsed ? 'Expand' : 'Collapse'}
</Button>
<Typography
variant="paragraph"
size="small"
weight="regular"
color="tertiary"
as="span"
textOverflow="wrap"
>
<Typography.Text className={styles.count}>
{count} of {total} {noun}
</Typography>
</Typography.Text>
</div>
);
}

View File

@@ -1,131 +0,0 @@
import {
type ChangeEvent,
type FormEvent,
useEffect,
useId,
useRef,
useState,
} from 'react';
import { Button } from '@signozhq/ui/button';
import { Dialog } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import { VIEW_NAME_MAX_LENGTH } from '../constants';
import styles from './ViewsRail.module.scss';
interface Props {
open: boolean;
onClose: () => void;
onSubmit: (name: string) => void;
title: string;
description: string;
confirmLabel: string;
/** Name the input holds each time the dialog opens. */
initialName?: string;
testIdPrefix: string;
}
function ViewNameDialog({
open,
onClose,
onSubmit,
title,
description,
confirmLabel,
initialName = '',
testIdPrefix,
}: Props): JSX.Element {
const [name, setName] = useState(initialName);
const inputRef = useRef<HTMLInputElement>(null);
const formId = useId();
const inputId = useId();
useEffect(() => {
if (open) {
setName(initialName);
}
}, [open, initialName]);
const trimmed = name.trim();
const canSave = trimmed.length > 0 && trimmed.length <= VIEW_NAME_MAX_LENGTH;
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
if (canSave) {
onSubmit(trimmed);
onClose();
}
};
return (
<Dialog
open={open}
onClose={onClose}
title={title}
description={description}
width="narrow"
initialFocus={inputRef}
testId={`${testIdPrefix}-dialog`}
footerSuffix={
<>
<Button
variant="outlined"
color="secondary"
size="md"
onClick={onClose}
testId={`${testIdPrefix}-cancel`}
>
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="md"
type="submit"
form={formId}
disabled={!canSave}
disabledTooltip={
trimmed.length === 0
? 'Enter a view name first'
: `Use at most ${VIEW_NAME_MAX_LENGTH} characters`
}
testId={`${testIdPrefix}-confirm`}
>
{confirmLabel}
</Button>
</>
}
>
<form id={formId} className={styles.saveForm} onSubmit={handleSubmit}>
<label htmlFor={inputId}>
<Typography
variant="paragraph"
size="small"
weight="regular"
color="main"
as="span"
textOverflow="wrap"
>
Name
</Typography>
</label>
<Input
ref={inputRef}
id={inputId}
value={name}
maxLength={VIEW_NAME_MAX_LENGTH}
placeholder="e.g. Prod alerts"
autoComplete="off"
testId={`${testIdPrefix}-name`}
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setName(e.target.value)
}
/>
</form>
</Dialog>
);
}
export default ViewNameDialog;

View File

@@ -24,8 +24,8 @@
.headerTitle {
margin: 0;
font-size: var(--label-medium-600-font-size);
font-weight: var(--label-medium-600-font-weight);
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
letter-spacing: -0.01em;
color: var(--l1-foreground);
text-align: left;
@@ -38,7 +38,7 @@
.searchEmpty {
padding: 12px;
color: var(--l3-foreground);
font-size: var(--label-small-400-font-size);
font-size: var(--font-size-xs);
text-align: center;
}
@@ -54,8 +54,8 @@
align-items: center;
gap: 6px;
padding: 16px 8px 8px;
font-size: var(--uppercase-small-600-font-size);
font-weight: var(--uppercase-small-600-font-weight);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--l3-foreground);
@@ -68,6 +68,9 @@
}
.groupCount {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-normal);
letter-spacing: 0;
color: var(--l3-foreground);
background: var(--l2-background);
border-radius: 10px;
@@ -76,6 +79,7 @@
.deleteName {
color: var(--danger-background);
font-weight: var(--font-weight-medium);
}
.row {
@@ -157,7 +161,7 @@
border: 1px dashed var(--l2-border);
border-radius: 8px;
color: var(--l3-foreground);
font-size: var(--label-small-400-font-size);
font-size: var(--font-size-xs);
line-height: 1.5;
text-align: center;
}
@@ -174,8 +178,8 @@
}
.dirtyTitle {
font-size: var(--uppercase-small-600-font-size);
font-weight: var(--uppercase-small-600-font-weight);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--l2-foreground);
@@ -187,9 +191,3 @@
flex-wrap: wrap;
gap: 6px;
}
.saveForm {
display: flex;
flex-direction: column;
gap: var(--spacing-3);
}

View File

@@ -6,17 +6,16 @@ import {
} from 'react';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { Bookmark, Plus, Search } from '@signozhq/icons';
import cx from 'classnames';
import { useDeleteConfirm } from 'components/DeleteConfirmModal/useDeleteConfirm';
import ViewNamePopover from 'components/ViewNamePopover/ViewNamePopover';
import type { BuiltinListView, ViewSaveMode } from '../types';
import ViewsRailDirtyPanel from './ViewsRailDirtyPanel';
import ViewsRailItem from './ViewsRailItem';
import ViewNameDialog from './ViewNameDialog';
import styles from './ViewsRail.module.scss';
@@ -73,12 +72,8 @@ function ViewsRail({
onViewRenamed,
onViewDeleted,
}: Props): JSX.Element {
const [saveOpen, setSaveOpen] = useState(false);
const [query, setQuery] = useState('');
const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
const [renameTarget, setRenameTarget] = useState<{
id: string;
label: string;
} | null>(null);
const { contextHolder, confirmDelete } = useDeleteConfirm();
const q = query.trim().toLowerCase();
@@ -99,28 +94,11 @@ function ViewsRail({
(id: string, label: string): void => {
confirmDelete({
title: (
<Typography
variant="paragraph"
size="medium"
weight="bold"
color="inherit"
as="h5"
marginY={4}
textOverflow="wrap"
>
<Typography.Title level={5}>
Delete the{' '}
<Typography
variant="paragraph"
size="base"
weight="medium"
color="danger"
as="span"
textOverflow="wrap"
>
{label}
</Typography>{' '}
<Typography.Text className={styles.deleteName}>{label}</Typography.Text>{' '}
view?
</Typography>
</Typography.Title>
),
content: deleteDescription,
// Return the delete promise so the modal's Delete button stays loading
@@ -134,28 +112,19 @@ function ViewsRail({
[confirmDelete, deleteDescription, onDelete, onViewDeleted],
);
const openRenameDialog = useCallback(
(id: string, label: string): void => setRenameTarget({ id, label }),
[],
const handleRename = useCallback(
(id: string, name: string): void => {
onViewRenamed?.();
onRename(id, name);
},
[onRename, onViewRenamed],
);
const closeRenameDialog = (): void => setRenameTarget(null);
const handleRename = (name: string): void => {
if (!renameTarget) {
return;
}
onViewRenamed?.();
onRename(renameTarget.id, name);
};
const handleSaveAsView = (name: string): void => {
onViewSaved?.('new');
onSave(name);
};
const openSaveDialog = (): void => setIsSaveDialogOpen(true);
const closeSaveDialog = (): void => setIsSaveDialogOpen(false);
const handleSaveViewChanges = (): void => {
onViewSaved?.('update');
onSaveChanges();
@@ -170,6 +139,7 @@ function ViewsRail({
active={v.id === activeViewId}
isModified={isModified}
testIdPrefix={testIdPrefix}
renameTestIdPrefix={renameTestIdPrefix}
onSelect={onSelect}
/>
);
@@ -178,19 +148,27 @@ function ViewsRail({
<>
<div className={styles.header}>
<h4 className={styles.headerTitle}>Views</h4>
<Tooltip title="Save current filters as a view">
<Button
aria-label="Save current filters as a view"
variant="ghost"
color="secondary"
size="sm"
icon
testId={`${testIdPrefix}-view-save-trigger`}
onClick={openSaveDialog}
>
<Plus size={14} />
</Button>
</Tooltip>
<ViewNamePopover
open={saveOpen}
onOpenChange={setSaveOpen}
onSubmit={handleSaveAsView}
title="Save as view"
confirmLabel="Save view"
testIdPrefix={saveTestIdPrefix}
tooltip="Save current filters as a view"
trigger={
<Button
aria-label="Save current filters as a view"
variant="ghost"
color="secondary"
size="sm"
icon
testId={`${testIdPrefix}-view-save-trigger`}
>
<Plus size={14} />
</Button>
}
/>
</div>
<div className={styles.search}>
@@ -226,18 +204,9 @@ function ViewsRail({
<>
<div className={cx(styles.groupLabel, styles.groupLabelSpaced)}>
{customSectionLabel}
<span className={styles.groupCount}>
<Typography
variant="paragraph"
size="small"
weight="regular"
color="tertiary"
as="span"
textOverflow="wrap"
>
{customViews.length}
</Typography>
</span>
<Typography.Text className={styles.groupCount}>
{customViews.length}
</Typography.Text>
</div>
{customViewsLoading ? (
<div className={styles.empty}>Loading views…</div>
@@ -255,8 +224,9 @@ function ViewsRail({
active={v.id === activeViewId}
isModified={isModified}
testIdPrefix={testIdPrefix}
renameTestIdPrefix={renameTestIdPrefix}
onSelect={onSelect}
onRename={openRenameDialog}
onRename={handleRename}
onDelete={onConfirmDelete}
/>
))
@@ -276,29 +246,10 @@ function ViewsRail({
isCustomActive={isCustomActive}
testIdPrefix={testIdPrefix}
onSaveChanges={handleSaveViewChanges}
onSaveAs={openSaveDialog}
onSaveAs={(): void => setSaveOpen(true)}
onReset={onReset}
/>
)}
<ViewNameDialog
open={isSaveDialogOpen}
onClose={closeSaveDialog}
onSubmit={handleSaveAsView}
title="Save as view"
description="Save the current filters as a view you can return to."
confirmLabel="Save view"
testIdPrefix={saveTestIdPrefix}
/>
<ViewNameDialog
open={renameTarget !== null}
onClose={closeRenameDialog}
onSubmit={handleRename}
title="Rename view"
description="The view keeps its filters under the new name."
confirmLabel="Rename"
initialName={renameTarget?.label}
testIdPrefix={renameTestIdPrefix}
/>
{contextHolder}
</>
);

View File

@@ -8,7 +8,6 @@ interface Props {
isCustomActive: boolean;
testIdPrefix: string;
onSaveChanges: () => void;
/** Opens the save-as dialog. */
onSaveAs: () => void;
onReset: () => void;
}

View File

@@ -1,7 +1,9 @@
import { useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import { PenLine, Trash2 } from '@signozhq/icons';
import cx from 'classnames';
import ViewNamePopover from 'components/ViewNamePopover/ViewNamePopover';
import type { ListViewIcon } from '../types';
@@ -14,9 +16,10 @@ interface Props {
active: boolean;
isModified: boolean;
testIdPrefix: string;
renameTestIdPrefix: string;
onSelect: (id: string) => void;
/** Present only for custom (editable) views. Opens the rename dialog. */
onRename?: (id: string, label: string) => void;
/** Present only for custom (editable) views. */
onRename?: (id: string, name: string) => void;
onDelete?: (id: string, label: string) => void;
}
@@ -27,10 +30,13 @@ function ViewsRailItem({
active,
isModified,
testIdPrefix,
renameTestIdPrefix,
onSelect,
onRename,
onDelete,
}: Props): JSX.Element {
const [renaming, setRenaming] = useState(false);
return (
<div className={cx(styles.row, { [styles.rowActive]: active })}>
<Button
@@ -51,22 +57,29 @@ function ViewsRailItem({
</Button>
{onRename && onDelete && (
<div className={styles.itemActions}>
<Tooltip title="Rename view">
<Button
variant="ghost"
color="secondary"
size="sm"
icon
aria-label="Rename view"
testId={`${testIdPrefix}-view-rename-${id}`}
onClick={(e): void => {
e.stopPropagation();
onRename(id, label);
}}
>
<PenLine size={12} />
</Button>
</Tooltip>
<ViewNamePopover
open={renaming}
onOpenChange={setRenaming}
onSubmit={(name): void => onRename(id, name)}
title="Rename view"
confirmLabel="Rename"
initialName={label}
testIdPrefix={renameTestIdPrefix}
tooltip="Rename view"
trigger={
<Button
variant="ghost"
color="secondary"
size="sm"
icon
aria-label="Rename view"
testId={`${testIdPrefix}-view-rename-${id}`}
onClick={(e): void => e.stopPropagation()}
>
<PenLine size={12} />
</Button>
}
/>
<Tooltip title="Delete view">
<Button
variant="ghost"

View File

@@ -1,5 +1,5 @@
import { Bookmark, Pin } from '@signozhq/icons';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { render, screen, userEvent } from 'tests/test-utils';
import type { BuiltinListView } from '../../types';
import ViewsRail from '../ViewsRail';
@@ -63,7 +63,7 @@ describe('ViewsRail', () => {
it('filters views by name and says when none match', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderRail();
const search = screen.getByTestId(`${PREFIX}-view-search-field`);
const search = screen.getByTestId(`${PREFIX}-view-search`);
await user.type(search, 'prod');
@@ -137,93 +137,6 @@ describe('ViewsRail', () => {
).not.toBeInTheDocument();
});
it('saves the current filters as a named view from the dialog', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onViewSaved = jest.fn();
const props = renderRail({ onViewSaved });
await user.click(screen.getByTestId(`${PREFIX}-view-save-trigger`));
await expect(
screen.findByTestId('save-view-dialog'),
).resolves.toHaveAttribute('aria-modal', 'true');
expect(screen.getByTestId('save-view-confirm')).toHaveAttribute(
'aria-disabled',
'true',
);
await user.type(
screen.getByTestId('save-view-name-field'),
' Prod {enter}',
);
expect(onViewSaved).toHaveBeenCalledWith('new');
expect(props.onSave).toHaveBeenCalledWith('Prod');
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
);
});
it('opens the save dialog empty again after a cancel', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const props = renderRail({ isModified: true });
await user.click(screen.getByTestId(`${PREFIX}-view-save-as-new`));
await user.type(await screen.findByTestId('save-view-name-field'), 'Draft');
await user.click(screen.getByTestId('save-view-cancel'));
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
);
await user.click(screen.getByTestId(`${PREFIX}-view-save-trigger`));
await expect(
screen.findByTestId('save-view-name-field'),
).resolves.toHaveValue('');
expect(props.onSave).not.toHaveBeenCalled();
});
it('renames a custom view from a dialog filled with its name', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onViewRenamed = jest.fn();
const props = renderRail({ onViewRenamed });
await user.click(screen.getByTestId(`${PREFIX}-view-rename-view-2`));
await expect(
screen.findByTestId('rename-view-dialog'),
).resolves.toHaveAttribute('aria-modal', 'true');
const input = screen.getByTestId('rename-view-name-field');
expect(input).toHaveValue('Staging things');
await user.clear(input);
await user.type(input, ' QA things {enter}');
expect(onViewRenamed).toHaveBeenCalledTimes(1);
expect(props.onRename).toHaveBeenCalledWith('view-2', 'QA things');
expect(props.onSelect).not.toHaveBeenCalled();
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
);
});
it('refills the rename dialog with the name of the view it opens for', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const props = renderRail();
await user.click(screen.getByTestId(`${PREFIX}-view-rename-view-1`));
await user.type(await screen.findByTestId('rename-view-name-field'), ' v2');
await user.click(screen.getByTestId('rename-view-cancel'));
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
);
await user.click(screen.getByTestId(`${PREFIX}-view-rename-view-2`));
await expect(
screen.findByTestId('rename-view-name-field'),
).resolves.toHaveValue('Staging things');
expect(props.onRename).not.toHaveBeenCalled();
});
it('wraps its body in the guard it is given', () => {
renderRail({
guard: (content): JSX.Element => (

View File

@@ -1 +0,0 @@
export const VIEW_NAME_MAX_LENGTH = 64;

View File

@@ -1,13 +1,14 @@
import { MouseEventHandler } from 'react';
import { DrawerProps } from 'antd';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { VIEWS } from './constants';
import { MouseEventHandler } from 'react';
export type LogDetailProps = {
log: ILog | null;
selectedTab?: VIEWS;
selectedTab: VIEWS;
handleChangeSelectedView?: ChangeViewFunctionType;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
@@ -15,19 +16,10 @@ export type LogDetailProps = {
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (logId: string) => void;
handleOpenInExplorer?: MouseEventHandler;
getContainer?: DrawerProps['getContainer'];
onApplyLogFilter?: (expression: string) => void;
onClose: () => void;
};
} & Pick<DrawerProps, 'onClose'>;
export type LogDetailBodyProps = Pick<
LogDetailProps,
| 'selectedTab'
| 'isListViewPanel'
| 'handleChangeSelectedView'
| 'onApplyLogFilter'
| 'onClose'
> & {
log: ILog;
onPrev: () => void;
onNext: () => void;
export type LogDetailInnerProps = LogDetailProps & {
log: NonNullable<LogDetailProps['log']>;
};

View File

@@ -1,364 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import { Tooltip } from 'antd';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
import Overview from 'container/LogDetailedView/Overview';
import {
getSanitizedLogBody,
removeEscapeCharacters,
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { useArrowKeyNavigation } from 'container/TracesExplorer/SpanDrawer/useArrowKeyNavigation';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getBodyMessage } from 'lib/logs/logBody';
import { cloneDeep } from 'lodash-es';
import {
ArrowDown,
ArrowUp,
Filter,
Histogram,
Table,
TextSelect,
} from '@signozhq/icons';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailBodyProps } from './LogDetail.interfaces';
import LogHighlights from './LogHighlights/LogHighlights';
/* eslint-disable-next-line sonarjs/cognitive-complexity */
function LogDetailBody({
log,
onClose,
selectedTab = VIEW_TYPES.OVERVIEW,
isListViewPanel = false,
handleChangeSelectedView,
onApplyLogFilter,
onPrev,
onNext,
}: LogDetailBodyProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
initialContextQuery,
);
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
const [isFilterVisible, setIsFilterVisible] = useState<boolean>(false);
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery } = useQueryBuilder();
// Handle clicks outside to close drawer, except on explicitly ignored regions
useEffect(() => {
const handleClickOutside = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
// Don't close if clicking on drawer content, overlays, or portal elements
if (
target.closest('[data-log-detail-ignore="true"]') ||
target.closest('.cm-tooltip-autocomplete') ||
target.closest('.drawer-popover') ||
target.closest('.query-status-popover') ||
target.closest('[data-radix-popper-content-wrapper]') ||
target.closest('[data-base-ui-portal]')
) {
return;
}
// Close the drawer for any other outside click
onClose();
};
document.addEventListener('mousedown', handleClickOutside);
return (): void => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
// Only listen in the OVERVIEW tab so we don't hijack arrow keys from the
// JSON editor / context view.
useArrowKeyNavigation({
enabled: selectedView === VIEW_TYPES.OVERVIEW,
onPrev,
onNext,
});
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
return null;
}
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
}, [stagedQuery]);
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const handleModeChange = (value: string): void => {
setSelectedView(value as VIEWS);
setIsEdit(false);
setIsFilterVisible(false);
};
const handleFilterVisible = (): void => {
setIsFilterVisible(!isFilterVisible);
setIsEdit(!isEdit);
};
const logBody = useMemo(() => getBodyMessage(log?.body), [log?.body]);
const htmlBody = useMemo(
() => ({
__html: getSanitizedLogBody(logBody || '', { shouldEscapeHtml: true }),
}),
[logBody],
);
const handleQueryExpressionChange = useCallback(
(value: string, queryIndex: number) => {
// update the query at the given index
setContextQuery((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
builder: {
...prev.builder,
queryData: prev.builder.queryData.map((query, idx) =>
idx === queryIndex
? {
...query,
filter: {
...query.filter,
expression: value,
},
}
: query,
),
},
};
});
},
[],
);
const handleRunQuery = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
if (!updatedContextQuery || !updatedContextQuery.builder) {
return;
}
const newFilters: TagFilter = {
items: expression ? convertExpressionToFilters(expression) : [],
op: 'AND',
};
updatedContextQuery = {
...updatedContextQuery,
builder: {
...updatedContextQuery?.builder,
queryData: updatedContextQuery?.builder.queryData.map((queryData) => ({
...queryData,
filter: {
...queryData.filter,
expression,
},
filters: {
...queryData.filters,
...newFilters,
op: queryData.filters?.op ?? 'AND',
},
})),
},
};
setContextQuery(updatedContextQuery);
if (newFilters) {
setFilters(newFilters);
}
};
return (
<OverlayScrollbar style={{ height: '100%' }}>
<div className="log-detail-drawer__content">
<div className="log-detail-drawer__log">
<LogStateIndicator
severityText={log.severity_text}
severityNumber={log.severity_number}
fontSize={options?.fontSize ?? FontSize.MEDIUM}
/>
<Tooltip
title={removeEscapeCharacters(logBody)}
placement="left"
mouseLeaveDelay={0}
>
<div
className="log-body"
data-testid="log-detail-body"
dangerouslySetInnerHTML={htmlBody}
/>
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>
</div>
<LogHighlights log={log} />
<div className="log-detail-drawer__section-divider" />
<div className="tabs-and-search">
<ToggleGroup
variant="outlined"
color="secondary"
size="sm"
type="single"
testId="log-detail-views-tabs"
onChange={handleModeChange}
value={selectedView}
items={[
{
value: VIEW_TYPES.OVERVIEW,
label: (
<div className="view-title">
<Table size={14} />
Overview
</div>
),
},
{
value: VIEW_TYPES.CONTEXT,
label: (
<div className="view-title">
<TextSelect size={14} />
Context
</div>
),
},
{
value: VIEW_TYPES.INFRAMETRICS,
label: (
<div className="view-title">
<Histogram size="md" />
Metrics
</div>
),
},
]}
/>
<div className="log-detail-drawer__actions">
{selectedView === VIEW_TYPES.CONTEXT && (
<Tooltip
title="Show Filters"
placement="topLeft"
aria-label="Show Filters"
mouseLeaveDelay={0}
>
<Button
variant="link"
color="secondary"
size="sm"
icon
aria-label="Show Filters"
onClick={handleFilterVisible}
>
<Filter size="lg" />
</Button>
</Tooltip>
)}
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}
/>
</div>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<Overview
logData={log}
isListViewPanel={isListViewPanel}
handleChangeSelectedView={handleChangeSelectedView}
onApplyLogFilter={onApplyLogFilter}
/>
)}
{selectedView === VIEW_TYPES.CONTEXT && (
<ContextView
log={log}
filters={filters}
contextQuery={contextQuery}
isEdit={isEdit}
/>
)}
{selectedView === VIEW_TYPES.INFRAMETRICS && (
<InfraMetrics
clusterName={log.resources_string?.[RESOURCE_KEYS.CLUSTER_NAME] || ''}
podName={log.resources_string?.[RESOURCE_KEYS.POD_NAME] || ''}
nodeName={log.resources_string?.[RESOURCE_KEYS.NODE_NAME] || ''}
hostName={log.resources_string?.[RESOURCE_KEYS.HOST_NAME] || ''}
timestamp={log.timestamp.toString()}
dataSource={DataSource.LOGS}
/>
)}
{selectedView === VIEW_TYPES.OVERVIEW && (
<div className="log-detail-drawer__footer-hint">
<div className="log-detail-drawer__footer-hint-content">
<Typography
variant="paragraph"
size="base"
weight="regular"
color="secondary"
as="span"
textOverflow="wrap"
>
Use
</Typography>
<ArrowUp size={14} className="log-detail-drawer__footer-hint-icon" />
<span>/</span>
<ArrowDown size={14} className="log-detail-drawer__footer-hint-icon" />
<Typography
variant="paragraph"
size="base"
weight="regular"
color="secondary"
as="span"
textOverflow="wrap"
>
to view previous/next log
</Typography>
</div>
</div>
)}
</div>
</OverlayScrollbar>
);
}
export default LogDetailBody;

View File

@@ -1,17 +1,79 @@
[data-testid='log-detail-drawer'] {
--drawer-extra-wide-max-inline-size: 60vw;
--drawer-body-padding: 0 var(--spacing-8);
// OverlayScrollbar owns the scroll. A native scrollbar here changes the
// body width when it toggles and shifts the header grid.
--drawer-body-overflow-y: hidden;
}
.log-detail-drawer {
border-left: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.log-detail-drawer-query-container {
margin-bottom: 16px;
}
.log-detail-drawer__title {
display: flex;
justify-content: space-between;
align-items: center;
.log-detail-drawer__content {
min-height: 100%;
.log-detail-drawer__title-left {
display: flex;
align-items: center;
gap: 8px;
}
.log-detail-drawer__title-right {
display: flex;
align-items: center;
.ant-btn {
display: flex;
align-items: center;
}
}
}
.ant-drawer-header {
padding: 8px 16px;
border-bottom: none;
align-items: stretch;
border-bottom: 1px solid var(--l1-border);
background: var(--l2-background);
}
.ant-drawer-close {
margin-inline-end: 0px;
}
.ant-drawer-body {
display: flex;
flex-direction: column;
padding: 0 16px;
// OverlayScrollbar owns the scroll. A native scrollbar here changes the
// body width when it toggles and shifts the header grid.
overflow: hidden;
}
.title {
color: var(--l2-foreground);
font-family: Inter;
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.radio-button {
display: flex;
align-items: center;
justify-content: center;
padding-top: var(--padding-1);
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
&-query-container {
margin-bottom: 16px;
}
.log-detail-drawer__content {
min-height: 100%;
}
.log-detail-drawer__log {
width: 100%;
@@ -23,9 +85,10 @@
.log-body {
font-family: 'SF Mono';
font-family: var(--code-base-400-font-family);
font-family: 'Geist Mono';
font-size: var(--code-base-400-font-size);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
line-height: 18px;
letter-spacing: -0.07px;
white-space: nowrap;
@@ -55,11 +118,7 @@
}
.log-overflow-shadow {
background: linear-gradient(
270deg,
var(--drawer-background) 10.4%,
transparent 100%
);
background: linear-gradient(270deg, var(--card) 10.4%, transparent 100%);
width: 196px;
position: absolute;
@@ -86,6 +145,16 @@
align-items: center;
margin: 16px 0;
.action-btn {
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.log-detail-drawer__actions {
display: flex;
gap: 4px;
@@ -97,9 +166,23 @@
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--label-small-400-font-size);
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.search-input {
margin-top: var(--margin-2);
border: 1px solid var(--l1-border);
height: 46px;
padding: var(--padding-1) var(--padding-2);
box-shadow: none;
border-radius: 0;
}
.ant-drawer-close {
padding: 0px;
}
.log-detail-drawer__footer-hint {
position: sticky;
bottom: 0;
@@ -108,7 +191,7 @@
padding: 8px 16px;
text-align: left;
color: var(--l3-foreground);
background: var(--drawer-background);
background: var(--l2-background);
z-index: 10;
.log-detail-drawer__footer-hint-content {
@@ -122,5 +205,18 @@
vertical-align: middle;
color: var(--l3-foreground);
}
.log-detail-drawer__footer-hint-text {
font-size: 13px;
margin: 0;
}
}
.log-arrows {
display: flex;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 2px 6px;
align-items: center;
margin-left: 8px;
}
}

View File

@@ -1,83 +0,0 @@
import { MouseEventHandler } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Compass, Copy, Ellipsis, Link } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
import { toast } from '@signozhq/ui/toast';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { ILog } from 'types/api/logs/log';
interface LogActionsProps {
log: ILog;
showOpenInExplorer?: boolean;
onOpenInExplorer?: MouseEventHandler;
}
function LogActions({
log,
showOpenInExplorer = false,
onOpenInExplorer,
}: LogActionsProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const { onLogCopy } = useCopyLogLink(log?.id);
const handleCopyLog = (): void => {
copyToClipboard(aggregateAttributesResourcesToString(log));
toast.success('Copied to clipboard', { position: 'bottom-right' });
};
const menuItems: DropdownItemType[] = [
{
type: 'item',
value: 'copy-log',
label: 'Copy log',
prefix: <Copy size={14} />,
onClick: handleCopyLog,
},
{
type: 'item',
value: 'copy-link',
label: 'Copy link to log',
prefix: <Link size={14} />,
onClick: (): void => onLogCopy(),
},
];
return (
<>
{showOpenInExplorer && (
<Button
size="sm"
variant="outlined"
color="secondary"
prefix={<Compass size={14} />}
onClick={onOpenInExplorer}
testId="log-details-header-open-in-explorer"
>
Open in Explorer
</Button>
)}
<Dropdown items={menuItems} nativeButton align="end" side="bottom">
<Button
size="sm"
variant="link"
color="secondary"
icon
aria-label="Log actions"
testId="log-details-header-menu"
>
<Ellipsis size={16} />
</Button>
</Dropdown>
</>
);
}
LogActions.defaultProps = {
showOpenInExplorer: false,
onOpenInExplorer: undefined,
};
export default LogActions;

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