mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-12 19:31:13 +00:00
Compare commits
96 Commits
feat/audit
...
feat/input
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
566f0233fe | ||
|
|
acc9d77bb3 | ||
|
|
bd2090ba1a | ||
|
|
a5708d751f | ||
|
|
c35c26ec58 | ||
|
|
83216b9e48 | ||
|
|
fccc0c50a6 | ||
|
|
f1727d5473 | ||
|
|
b0c17eb8a1 | ||
|
|
3a66cc4d25 | ||
|
|
577ce733b5 | ||
|
|
ac631df197 | ||
|
|
b0b140fb5c | ||
|
|
bf3b648449 | ||
|
|
6484d70bc4 | ||
|
|
f1b20eaee2 | ||
|
|
9c544bf1eb | ||
|
|
e18cc3e9a5 | ||
|
|
11ab477d5a | ||
|
|
1bd9168720 | ||
|
|
30e6becb8c | ||
|
|
b6618dea5b | ||
|
|
44c0eae502 | ||
|
|
ead817101a | ||
|
|
5b4d72ab8d | ||
|
|
5926e28906 | ||
|
|
46e6b771dd | ||
|
|
5b4da56230 | ||
|
|
88c6c6d37f | ||
|
|
fe743dfb55 | ||
|
|
e9692f5a23 | ||
|
|
972b4bd17a | ||
|
|
e36cfe3924 | ||
|
|
2b88514afe | ||
|
|
319095fe36 | ||
|
|
90651539ea | ||
|
|
f0f7353953 | ||
|
|
da6307976c | ||
|
|
c0fdc268a3 | ||
|
|
943b983edd | ||
|
|
964c7d673a | ||
|
|
fc3ef17512 | ||
|
|
52c20eef19 | ||
|
|
54fe74b26c | ||
|
|
e91b587291 | ||
|
|
c58bc111b7 | ||
|
|
61327e4360 | ||
|
|
6f16cf02fc | ||
|
|
09440b8916 | ||
|
|
69706cdbc1 | ||
|
|
aa749e45c5 | ||
|
|
08d62740aa | ||
|
|
c3f15f9d83 | ||
|
|
8394a8ccb0 | ||
|
|
6fe29bf7ca | ||
|
|
8645231cb2 | ||
|
|
86f078a8e5 | ||
|
|
e4a689bd40 | ||
|
|
91b937358f | ||
|
|
6d84c9cfbb | ||
|
|
f4c165fc69 | ||
|
|
8adbe65c53 | ||
|
|
b16484a05e | ||
|
|
f5500429ad | ||
|
|
81a2a864bd | ||
|
|
45fe5962f6 | ||
|
|
8c7a99d060 | ||
|
|
169ad9f71f | ||
|
|
6e321c3103 | ||
|
|
6f7de010cb | ||
|
|
ba3dda191a | ||
|
|
84de4507b8 | ||
|
|
7a63f67d1e | ||
|
|
e1a23faab4 | ||
|
|
ac23f30d68 | ||
|
|
759664cb6a | ||
|
|
b7ace137f6 | ||
|
|
ee3bc2cf23 | ||
|
|
2adea9264d | ||
|
|
a560e3997f | ||
|
|
73433bb15b | ||
|
|
8768c36a09 | ||
|
|
5b25e7dd1f | ||
|
|
a6b40611f2 | ||
|
|
c131295ba9 | ||
|
|
0ee7dabe48 | ||
|
|
774b97aa78 | ||
|
|
1d88653fd3 | ||
|
|
e81ee9a2ec | ||
|
|
9071d9a1cb | ||
|
|
d154a6ea03 | ||
|
|
2ff370cf8c | ||
|
|
e1de5c9d20 | ||
|
|
4ae337b8a6 | ||
|
|
eac4b21de4 | ||
|
|
22b4b1c5ee |
2
.github/workflows/e2eci.yaml
vendored
2
.github/workflows/e2eci.yaml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
postgres:15 \
|
||||
clickhouse/clickhouse-server:25.12.5 \
|
||||
clickhouse/clickhouse-keeper:25.12.5 \
|
||||
signoz/signoz-otel-collector:v0.144.16-rc.1 \
|
||||
signoz/signoz-schema-migrator:v0.144.9 \
|
||||
wiremock/wiremock:2.35.1-1 \
|
||||
| xargs -P 0 -n 1 docker pull -q
|
||||
- name: python
|
||||
|
||||
7
.github/workflows/integrationci.yaml
vendored
7
.github/workflows/integrationci.yaml
vendored
@@ -40,7 +40,6 @@ jobs:
|
||||
suite:
|
||||
- alertmanager
|
||||
- alertmanagerrotation
|
||||
- auditquerier
|
||||
- basepath
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
@@ -84,8 +83,8 @@ jobs:
|
||||
- wal
|
||||
clickhouse-version:
|
||||
- 25.12.5
|
||||
signoz-otel-collector-version:
|
||||
- v0.144.16-rc.1
|
||||
schema-migrator-version:
|
||||
- v0.144.6
|
||||
postgres-version:
|
||||
- 15
|
||||
if: |
|
||||
@@ -156,4 +155,4 @@ jobs:
|
||||
--sqlite-mode ${{matrix.sqlite-mode}} \
|
||||
--postgres-version ${{matrix.postgres-version}} \
|
||||
--clickhouse-version ${{matrix.clickhouse-version}} \
|
||||
--signoz-otel-collector-version ${{matrix.signoz-otel-collector-version}}
|
||||
--schema-migrator-version ${{matrix.schema-migrator-version}}
|
||||
|
||||
@@ -256,7 +256,7 @@ Tests can be configured using pytest options:
|
||||
- `--sqlite-mode` — SQLite journal mode: `delete` or `wal` (default: `delete`). Only relevant when `--sqlstore-provider=sqlite`.
|
||||
- `--postgres-version` — PostgreSQL version (default: `15`)
|
||||
- `--clickhouse-version` — ClickHouse version, also used for ClickHouse Keeper (default: `25.12.5`)
|
||||
- `--signoz-otel-collector-version`: SigNoz OTel collector version, used to run the ClickHouse migrations (default: `v0.144.16-rc.1`)
|
||||
- `--schema-migrator-version` — SigNoz schema migrator version (default: `v0.144.9`)
|
||||
- `--with-web` — Build the frontend into the SigNoz image (required for e2e)
|
||||
|
||||
Example:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-lg);
|
||||
font-size: var(--label-large-400-font-size);
|
||||
}
|
||||
|
||||
@@ -497,7 +497,11 @@
|
||||
// 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-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
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"src/parser/*.ts",
|
||||
|
||||
@@ -45,8 +45,8 @@ const mockAliases = [
|
||||
},
|
||||
{
|
||||
// jest: not replaced, a test asserts on the toast it fires.
|
||||
find: /^@signozhq\/ui\/sonner$/,
|
||||
replacement: `${srcPath}/storybook/mocks/sonner.mock.tsx`,
|
||||
find: /^@signozhq\/ui\/toast$/,
|
||||
replacement: `${srcPath}/storybook/mocks/toast.mock.tsx`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<!--
|
||||
`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.
|
||||
Every Google Fonts request the app makes is answered with an empty
|
||||
stylesheet; the local faces stand in for them.
|
||||
-->
|
||||
<link rel="stylesheet" href="storybook-fonts.css" />
|
||||
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
/*
|
||||
* The five families the app pulls from Google Fonts, served from the files in
|
||||
* The four families the app pulls from Google Fonts, served from the files in
|
||||
* `public/fonts` instead. `msw/appShellHandlers.ts` answers the CDN with an
|
||||
* empty stylesheet so no request leaves the iframe, which without this left
|
||||
* every story on a fallback for the four families `src/styles.scss` imports at
|
||||
* runtime, and on whatever the Inter link in `index.html` happened to fetch
|
||||
* before the worker had started.
|
||||
* runtime. Inter comes from `src/styles.scss` itself.
|
||||
*/
|
||||
|
||||
@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');
|
||||
|
||||
@@ -14,9 +14,23 @@ 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: [
|
||||
{
|
||||
|
||||
@@ -191,34 +191,30 @@ 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-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
|
||||
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);
|
||||
}
|
||||
|
||||
// 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
|
||||
.hint {
|
||||
font-size: var(--paragraph-small-400-font-size); // 11px
|
||||
line-height: var(--paragraph-small-400-line-height); // 18px
|
||||
}
|
||||
|
||||
// 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
|
||||
.id {
|
||||
font-family: var(--code-base-400-font-family);
|
||||
font-size: var(--code-base-400-font-size); // 13px
|
||||
}
|
||||
```
|
||||
|
||||
@@ -312,7 +308,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(--periscope-font-size-base);
|
||||
--button-font-size: var(--label-base-400-font-size);
|
||||
}
|
||||
|
||||
// Override Input
|
||||
@@ -342,7 +338,7 @@ Components use data attributes for variants/states. Target them for state-specif
|
||||
|
||||
// Target size
|
||||
.wrapper :global([data-size="sm"]) {
|
||||
--button-font-size: var(--periscope-font-size-small);
|
||||
--button-font-size: var(--label-small-400-font-size);
|
||||
}
|
||||
|
||||
// Target color
|
||||
|
||||
191
frontend/docs/popover-migration-analysis.md
Normal file
191
frontend/docs/popover-migration-analysis.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 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`.
|
||||
@@ -14,11 +14,6 @@
|
||||
<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>
|
||||
|
||||
@@ -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.2.1",
|
||||
"@signozhq/design-tokens": "2.6.0",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/ui": "0.2.0",
|
||||
"@signozhq/ui": "0.4.0-rc.2",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
170
frontend/plugins/__tests__/no-text-css.test.mjs
Normal file
170
frontend/plugins/__tests__/no-text-css.test.mjs
Normal file
@@ -0,0 +1,170 @@
|
||||
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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
49
frontend/plugins/__tests__/require-disable-reason.test.mjs
Normal file
49
frontend/plugins/__tests__/require-disable-reason.test.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
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 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
324
frontend/plugins/rules/no-text-css.mjs
Normal file
324
frontend/plugins/rules/no-text-css.mjs
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
38
frontend/plugins/rules/require-disable-reason.mjs
Normal file
38
frontend/plugins/rules/require-disable-reason.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 \`--\`.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -17,6 +17,8 @@ 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: {
|
||||
@@ -35,5 +37,7 @@ 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,
|
||||
},
|
||||
};
|
||||
|
||||
1127
frontend/pnpm-lock.yaml
generated
1127
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
92
frontend/public/fonts/GeistMono-LICENSE.txt
Normal file
92
frontend/public/fonts/GeistMono-LICENSE.txt
Normal file
@@ -0,0 +1,92 @@
|
||||
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.
@@ -54,6 +54,9 @@ function AddCreditCardModal({
|
||||
updateCreditCard({ url: getBaseUrl() });
|
||||
};
|
||||
|
||||
// TODO: antd stacks at 1000, under `@signozhq/ui` dialogs (1001). Opened from
|
||||
// CustomDomainEditModal's "Contact Support" it renders behind that dialog.
|
||||
// Moving to the `@signozhq/ui` Dialog fixes it.
|
||||
return (
|
||||
<Modal
|
||||
className="add-credit-card-modal"
|
||||
@@ -90,10 +93,17 @@ function AddCreditCardModal({
|
||||
</AuthZTooltip>,
|
||||
]}
|
||||
>
|
||||
<Typography.Text className="add-credit-card-text">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
You're currently on <span className="highlight-text">Trial plan</span>.
|
||||
Add a credit card to access SigNoz chat support to your workspace.
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
.breadcrumb {
|
||||
padding: var(--spacing-4) 16px var(--spacing-2);
|
||||
|
||||
// The antd breadcrumb provides the item spacing.
|
||||
--button-padding: 0;
|
||||
|
||||
ol {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -8,13 +11,4 @@
|
||||
: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;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ function AlertBreadcrumb({
|
||||
items={breadcrumbItems}
|
||||
data-testid={testId}
|
||||
/>
|
||||
{showDivider && <Divider className={styles.divider} />}
|
||||
{showDivider && <Divider spacing="10px 16px" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Copy } from '@signozhq/icons';
|
||||
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import cx from 'classnames';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Copy } from '@signozhq/icons';
|
||||
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--l1-foreground); // Primary text
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -28,15 +28,29 @@ function AppLoading(): JSX.Element {
|
||||
<div className="brand">
|
||||
<img src={signozBrandLogoUrl} alt="SigNoz" className="brand-logo" />
|
||||
|
||||
<Typography.Title level={2} className="brand-title">
|
||||
<Typography
|
||||
variant="label"
|
||||
size="large"
|
||||
weight="bold"
|
||||
color="main"
|
||||
as="h2"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
SigNoz
|
||||
</Typography.Title>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="brand-tagline">
|
||||
<Typography.Text>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
OpenTelemetry-Native Logs, Metrics and Traces in a single pane
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="loader" />
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// 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);
|
||||
}
|
||||
20
frontend/src/components/AppToaster/AppToaster.tsx
Normal file
20
frontend/src/components/AppToaster/AppToaster.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Toaster } from '@signozhq/ui/toast';
|
||||
|
||||
import styles from './AppToaster.module.scss';
|
||||
|
||||
// The bottom clears the page-size select of a pagination bar, 83px above the bottom strip, and
|
||||
// the 56px footer of a drawer or dialog panel, which sits 16px from the edge of the window.
|
||||
const OFFSET = {
|
||||
top: 24,
|
||||
right: 24,
|
||||
bottom: 'calc(var(--bottom-strip-height, 0px) + 92px)',
|
||||
left: 24,
|
||||
};
|
||||
|
||||
function AppToaster(): JSX.Element {
|
||||
return (
|
||||
<Toaster position="bottom-right" offset={OFFSET} className={styles.toaster} />
|
||||
);
|
||||
}
|
||||
|
||||
export default AppToaster;
|
||||
@@ -35,7 +35,6 @@
|
||||
&__error-code {
|
||||
color: var(--bg-cherry-100);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
}
|
||||
@@ -43,8 +42,6 @@
|
||||
&__error-message {
|
||||
color: var(--bg-cherry-300);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
}
|
||||
|
||||
@@ -75,7 +72,6 @@
|
||||
&__message-item {
|
||||
color: var(--bg-cherry-300);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.065px;
|
||||
|
||||
|
||||
@@ -45,9 +45,8 @@
|
||||
}
|
||||
|
||||
.auth-footer-text {
|
||||
font-family: var(--font-family-inter, Inter, sans-serif);
|
||||
font-family: var(--label-small-400-font-family);
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color: var(--l2-foreground);
|
||||
text-align: center;
|
||||
@@ -57,7 +56,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4.9px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-header-logo-icon {
|
||||
@@ -26,7 +25,7 @@
|
||||
}
|
||||
|
||||
.auth-header-logo-text {
|
||||
font-family: Satoshi, var(--font-family-inter, Inter), sans-serif;
|
||||
font-family: Satoshi, var(--label-medium-500-font-family), sans-serif;
|
||||
font-size: 15.4px;
|
||||
font-weight: 500;
|
||||
line-height: 17.5px;
|
||||
|
||||
@@ -17,8 +17,3 @@
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.divider {
|
||||
--divider-color: var(--l1-border);
|
||||
--divider-margin: 10px 0 16px 0;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ function BackHeader({
|
||||
</Button>
|
||||
<div className={styles.title}>{title}</div>
|
||||
</div>
|
||||
<Divider className={styles.divider} />
|
||||
<Divider spacing="10px 16px" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
@@ -31,10 +30,7 @@
|
||||
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;
|
||||
|
||||
@@ -45,7 +41,6 @@
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l1-background);
|
||||
@@ -67,12 +62,6 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,27 @@ const INITIAL_PAGE_SIZE = 20;
|
||||
|
||||
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
|
||||
<>
|
||||
<Typography.Text className="numbers">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
{range[0]} — {range[1]}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="total"> of {total}</Typography.Text>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
{' '}
|
||||
of {total}
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -56,22 +73,15 @@ function ProgressRender(item: string | number): JSX.Element {
|
||||
const percent = Number(Number(item).toFixed(1));
|
||||
return (
|
||||
<div className="progress-container">
|
||||
<Progress
|
||||
percent={percent}
|
||||
strokeLinecap="butt"
|
||||
showInfo
|
||||
strokeColor={((): string => {
|
||||
const cpuPercent = percent;
|
||||
if (cpuPercent >= 90) {
|
||||
return Color.BG_SAKURA_500;
|
||||
<div className="progress-bar">
|
||||
<Progress
|
||||
percent={percent}
|
||||
showInfo
|
||||
color={
|
||||
percent >= 90 ? 'highlight-danger' : percent >= 60 ? 'warning' : 'success'
|
||||
}
|
||||
if (cpuPercent >= 60) {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
return Color.BG_FOREST_500;
|
||||
})()}
|
||||
className="progress-bar"
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -526,7 +536,18 @@ export default function CeleryOverviewTable({
|
||||
),
|
||||
}}
|
||||
locale={{
|
||||
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,
|
||||
emptyText: isLoading ? null : (
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
No data
|
||||
</Typography>
|
||||
),
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
showSorterTooltip
|
||||
|
||||
@@ -24,9 +24,16 @@ function CeleryTaskConfigOptions(): JSX.Element {
|
||||
return (
|
||||
<div className="celery-task-filters">
|
||||
<div className="celery-filters">
|
||||
<Typography.Text style={{ whiteSpace: 'nowrap' }}>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="visible"
|
||||
>
|
||||
Task Name
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
<Select
|
||||
data-testid="celery-task-name-select"
|
||||
placeholder="Task Name"
|
||||
|
||||
@@ -49,11 +49,7 @@
|
||||
|
||||
.subtitle {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,11 +60,6 @@
|
||||
|
||||
.footer-text {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +65,38 @@ export default function CeleryTaskDetail({
|
||||
width="45%"
|
||||
title={
|
||||
<div>
|
||||
<Typography.Text className="title">{`Details - ${taskData.entity}`}</Typography.Text>
|
||||
<Typography
|
||||
variant="label"
|
||||
size="large"
|
||||
weight="bold"
|
||||
color="main"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>{`Details - ${taskData.entity}`}</Typography>
|
||||
<div>
|
||||
<Typography.Text className="subtitle">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="regular"
|
||||
color="secondary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
{`${formatTimestamp(startTime)} ${
|
||||
endTime ? `- ${formatTimestamp(endTime)}` : ''
|
||||
}`}
|
||||
</Typography.Text>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="subtitle">{taskData.value}</Typography.Text>
|
||||
</Typography>
|
||||
<Divider orientation="vertical" />
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="regular"
|
||||
color="secondary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
{taskData.value}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -88,7 +111,14 @@ export default function CeleryTaskDetail({
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
footer={
|
||||
<Typography.Text className="footer-text">{`Total Task: ${totalTask}`}</Typography.Text>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="secondary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>{`Total Task: ${totalTask}`}</Typography>
|
||||
}
|
||||
>
|
||||
<CeleryTaskGraph
|
||||
|
||||
@@ -52,11 +52,6 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.celery-task-graph-worker-count-text {
|
||||
font-size: 2.5vw;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.worker-count-header {
|
||||
@@ -148,14 +143,12 @@
|
||||
|
||||
.configure-option-Info-text {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.row-panel {
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--card) 40%, transparent);
|
||||
background: color-mix(in srgb, var(--surface-2) 40%, transparent);
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
@@ -217,7 +210,6 @@
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-family: 'Inter';
|
||||
font-size: 14px;
|
||||
color: var(--l2-foreground);
|
||||
line-height: 20px;
|
||||
@@ -225,7 +217,7 @@
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-family: 'Geist Mono';
|
||||
font-family: var(--code-base-400-font-family);
|
||||
font-size: 24px;
|
||||
color: var(--l1-foreground);
|
||||
line-height: 32px;
|
||||
|
||||
@@ -113,9 +113,16 @@ export default function CeleryTaskGraphGrid({
|
||||
<CardContainer className="row-card" isDarkMode={isDarkMode}>
|
||||
<div className="row-panel">
|
||||
<div className="row-panel-section">
|
||||
<Typography.Text className="section-title">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="secondary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Flower Metrics
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
{collapsedSections.metricBasedGraphs ? (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
@@ -146,14 +153,30 @@ export default function CeleryTaskGraphGrid({
|
||||
/>
|
||||
<Card className="celery-task-graph-worker-count">
|
||||
<div className="worker-count-header">
|
||||
<Typography.Text className="worker-count-header-text">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Worker Online
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="worker-count-text-container">
|
||||
<Typography.Text className="celery-task-graph-worker-count-text">
|
||||
<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"
|
||||
>
|
||||
{options.filter((option) => option.value).length}
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -164,9 +187,16 @@ export default function CeleryTaskGraphGrid({
|
||||
<CardContainer className="row-card" isDarkMode={isDarkMode}>
|
||||
<div className="row-panel">
|
||||
<div className="row-panel-section">
|
||||
<Typography.Text className="section-title">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="secondary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Span Based Stats
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
{collapsedSections.traceBasedGraphs ? (
|
||||
<ChevronDown
|
||||
size={14}
|
||||
@@ -185,9 +215,16 @@ export default function CeleryTaskGraphGrid({
|
||||
</CardContainer>
|
||||
<div className="configure-option-Info">
|
||||
{configureOptionComponent}
|
||||
<Typography.Text className="configure-option-Info-text">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="regular"
|
||||
color="secondary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Click on a graph co-ordinate to see more details
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
{!collapsedSections.traceBasedGraphs && (
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
justify-content: center;
|
||||
}
|
||||
&__label {
|
||||
font-family: 'Geist Mono';
|
||||
font-size: 12px;
|
||||
line-height: normal;
|
||||
font-family: var(--code-base-400-font-family);
|
||||
}
|
||||
&--positive {
|
||||
.change-percentage-pill {
|
||||
|
||||
@@ -97,6 +97,10 @@ function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: antd stacks at 1000, under `@signozhq/ui` dialogs (1001). It opens on
|
||||
// its own a second after load, so a dialog opened from a deep link
|
||||
// (`?add-key`, `?invite=true`) covers it. Moving to the `@signozhq/ui` Dialog
|
||||
// fixes it.
|
||||
return (
|
||||
<Modal
|
||||
className={cx('changelog-modal')}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { createAppContextMock } from 'tests/fixtures/appContextMock';
|
||||
import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
import { storyChangelog } from './__story_mockdata__/changelog';
|
||||
import {
|
||||
showsCloudChangelog,
|
||||
showsSelfHostedChangelog,
|
||||
} from './__story_mockdata__/changelogModal.play';
|
||||
|
||||
import ChangelogModal from '../ChangelogModal';
|
||||
|
||||
const { activeLicense } = createAppContextMock(USER_ROLES.ADMIN);
|
||||
|
||||
/** `AppLayout` opens it one second after load, for a version the user has not seen. */
|
||||
const meta = {
|
||||
title: 'Components/Changelog Modal',
|
||||
component: ChangelogModal,
|
||||
tags: ['play'],
|
||||
parameters: { signoz: {} },
|
||||
args: {
|
||||
changelog: storyChangelog,
|
||||
onClose: fn(),
|
||||
},
|
||||
} satisfies Meta<typeof ChangelogModal>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/** Cloud workspaces update on their own, so the footer has no update actions. */
|
||||
export const Cloud: Story = {
|
||||
play: showsCloudChangelog,
|
||||
};
|
||||
|
||||
/** Self-hosted workspaces get "Skip for now" and "Update my workspace". */
|
||||
export const SelfHosted: Story = {
|
||||
parameters: {
|
||||
signoz: {
|
||||
appContext: {
|
||||
activeLicense: activeLicense && {
|
||||
...activeLicense,
|
||||
platform: LicensePlatform.SELF_HOSTED,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
play: showsSelfHostedChangelog,
|
||||
};
|
||||
62
frontend/src/components/ChangelogModal/stories/__story_mockdata__/changelog.ts
generated
Normal file
62
frontend/src/components/ChangelogModal/stories/__story_mockdata__/changelog.ts
generated
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
ChangelogSchema,
|
||||
DeploymentType,
|
||||
} from 'types/api/changelog/getChangelogByVersion';
|
||||
|
||||
const TIMESTAMP = '2026-11-03T12:00:00Z';
|
||||
|
||||
function feature(
|
||||
id: number,
|
||||
title: string,
|
||||
description: string,
|
||||
): ChangelogSchema['features'][number] {
|
||||
return {
|
||||
id,
|
||||
documentId: `feature-${id}`,
|
||||
title,
|
||||
description,
|
||||
media: null,
|
||||
sort_order: id,
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
publishedAt: TIMESTAMP,
|
||||
deployment_type: DeploymentType.ALL,
|
||||
};
|
||||
}
|
||||
|
||||
/** Long enough to scroll, so the "Scroll for more" button shows. */
|
||||
export const storyChangelog: ChangelogSchema = {
|
||||
id: 1,
|
||||
documentId: 'changelog-v0.99.0',
|
||||
version: 'v0.99.0',
|
||||
release_date: '2026-11-03',
|
||||
createdAt: TIMESTAMP,
|
||||
updatedAt: TIMESTAMP,
|
||||
publishedAt: TIMESTAMP,
|
||||
features: [
|
||||
feature(
|
||||
1,
|
||||
'Dashboard sections',
|
||||
'Group panels into **sections** you can collapse, rename and drag. Existing dashboards move into a single section the first time you edit them.',
|
||||
),
|
||||
feature(
|
||||
2,
|
||||
'Service accounts',
|
||||
'Create service accounts with their own API keys and roles. Keys can expire, and you can revoke them without touching the account. See the [docs](https://signoz.io/docs/) for the API.',
|
||||
),
|
||||
feature(
|
||||
3,
|
||||
'Faster log search',
|
||||
'Full-text search over log bodies now uses a token index:\n\n- queries over a day of logs return up to 4x faster\n- `has()` and `hasToken()` use the same index\n- the old behaviour stays behind a setting for a release',
|
||||
),
|
||||
feature(
|
||||
4,
|
||||
'Routing policies',
|
||||
'Send an alert to a channel based on its labels instead of picking the channel in every rule. Policies are evaluated in order and the first match wins.',
|
||||
),
|
||||
],
|
||||
bug_fixes:
|
||||
'- Fixed the time picker losing the selected range after a refresh.\n- Fixed trace waterfall rows overlapping when a span name wraps.\n- Fixed CSV export dropping columns with dots in their names.',
|
||||
maintenance:
|
||||
'- Upgraded the OpenTelemetry collector to the latest release.\n- Reduced the memory used by the query service on large dashboards.',
|
||||
};
|
||||
19
frontend/src/components/ChangelogModal/stories/__story_mockdata__/changelogModal.play.ts
generated
Normal file
19
frontend/src/components/ChangelogModal/stories/__story_mockdata__/changelogModal.play.ts
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
import { expect, screen, waitFor } from 'storybook/test';
|
||||
|
||||
const TITLE = /What’s New ⎯ Changelog : November 3, 2026/;
|
||||
|
||||
export const showsCloudChangelog = async (): Promise<void> => {
|
||||
await screen.findByRole('dialog', { name: TITLE });
|
||||
await expect(
|
||||
screen.queryByRole('button', { name: /Skip for now/ }),
|
||||
).not.toBeInTheDocument();
|
||||
};
|
||||
|
||||
export const showsSelfHostedChangelog = async (): Promise<void> => {
|
||||
await screen.findByRole('dialog', { name: TITLE });
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('button', { name: /Update my workspace/ }),
|
||||
).toBeVisible(),
|
||||
);
|
||||
};
|
||||
@@ -17,10 +17,6 @@
|
||||
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;
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
|
||||
pre {
|
||||
color: var(--l2-foreground) !important;
|
||||
font-family: 'Geist Mono' !important;
|
||||
font-family: var(--code-small-400-font-family) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--l1-foreground) !important;
|
||||
font-family: 'Geist Mono' !important;
|
||||
font-family: var(--code-small-400-font-family) !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,3 @@
|
||||
.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;
|
||||
@@ -43,32 +10,11 @@
|
||||
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);
|
||||
@@ -77,22 +23,7 @@
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
|
||||
@@ -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 { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Dialog } from '@signozhq/ui/dialog';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
invalidateListServiceAccounts,
|
||||
@@ -35,7 +35,7 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
);
|
||||
const [, setSelectedAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT);
|
||||
|
||||
const { showErrorModal, isErrorModalVisible } = useErrorModal();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -82,84 +82,75 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
<Dialog
|
||||
title="New Service Account"
|
||||
open={isOpen}
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
showCloseButton
|
||||
width="narrow"
|
||||
className="create-sa-modal"
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
onClose={handleClose}
|
||||
width="base"
|
||||
testId="create-service-account-modal"
|
||||
>
|
||||
<div className="create-sa-modal__content">
|
||||
<AuthZGuardContent
|
||||
checks={[SACreatePermission]}
|
||||
fallbackOnLoading={<Skeleton active paragraph={{ rows: 1 }} />}
|
||||
>
|
||||
<form
|
||||
id="create-sa-form"
|
||||
className="create-sa-form"
|
||||
onSubmit={handleSubmit(handleCreate)}
|
||||
footerSuffix={
|
||||
<>
|
||||
<Button
|
||||
size="md"
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
testId="create-sa-cancel-btn"
|
||||
prefix={<X size={12} />}
|
||||
>
|
||||
<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>
|
||||
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-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"
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="create-sa-form__error">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</AuthZGuardContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import {
|
||||
setupAuthzAdmin,
|
||||
setupAuthzDenyAll,
|
||||
@@ -10,9 +10,9 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import CreateServiceAccountModal from '../CreateServiceAccountModal';
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
...jest.requireActual('@signozhq/ui/toast'),
|
||||
toast: { success: jest.fn(), danger: jest.fn() },
|
||||
}));
|
||||
|
||||
const mockToast = jest.mocked(toast);
|
||||
@@ -23,7 +23,6 @@ jest.mock('providers/ErrorModalProvider', () => ({
|
||||
...jest.requireActual('providers/ErrorModalProvider'),
|
||||
useErrorModal: jest.fn(() => ({
|
||||
showErrorModal,
|
||||
isErrorModalVisible: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -58,7 +57,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');
|
||||
await screen.findByTestId('create-sa-name-input-field');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('create-sa-submit-btn')).toHaveAttribute(
|
||||
@@ -72,7 +71,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input');
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input-field');
|
||||
const submitBtn = await screen.findByTestId('create-sa-submit-btn');
|
||||
|
||||
await user.type(nameInput, 'test');
|
||||
@@ -90,7 +89,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input');
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input-field');
|
||||
await user.type(nameInput, 'Deploy Bot');
|
||||
|
||||
const submitBtn = screen.getByTestId('create-sa-submit-btn');
|
||||
@@ -128,7 +127,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId<HTMLInputElement>(
|
||||
'create-sa-name-input',
|
||||
'create-sa-name-input-field',
|
||||
);
|
||||
await user.type(nameInput, 'Deploy Bot');
|
||||
const submitBtn = screen.getByTestId('create-sa-submit-btn');
|
||||
@@ -160,7 +159,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input');
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input-field');
|
||||
await user.type(nameInput, 'Dupe Bot');
|
||||
|
||||
const submitBtn = screen.getByTestId('create-sa-submit-btn');
|
||||
@@ -209,7 +208,9 @@ describe('CreateServiceAccountModal', () => {
|
||||
screen.findByText(/is not authorized to perform/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId('create-sa-name-input')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('create-sa-name-input-field'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('create-service-account-modal'),
|
||||
).toBeInTheDocument();
|
||||
@@ -244,7 +245,7 @@ describe('CreateServiceAccountModal', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderModal();
|
||||
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input');
|
||||
const nameInput = await screen.findByTestId('create-sa-name-input-field');
|
||||
await user.type(nameInput, 'Bot');
|
||||
await user.clear(nameInput);
|
||||
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
.valid-format-error {
|
||||
margin-top: 4px;
|
||||
color: var(--bg-cherry-400) !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
@@ -76,11 +74,6 @@
|
||||
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 {
|
||||
@@ -104,15 +97,11 @@
|
||||
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;
|
||||
}
|
||||
@@ -228,10 +217,6 @@
|
||||
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;
|
||||
|
||||
@@ -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) {
|
||||
if (inputElement && !document.activeElement?.closest('.timezone-picker')) {
|
||||
inputElement.focus();
|
||||
inputElement.select();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ $item-spacing: 8px;
|
||||
.timezone-picker {
|
||||
width: 532px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: $font-family;
|
||||
|
||||
&__search {
|
||||
@include flex-center;
|
||||
@@ -36,34 +35,6 @@ $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;
|
||||
|
||||
@@ -65,7 +65,8 @@ function SearchBar({
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
className="timezone-picker__input"
|
||||
variant="unstyled"
|
||||
noFocusRing
|
||||
placeholder="Search timezones..."
|
||||
value={value}
|
||||
onChange={(e): void => onChange(e.target.value)}
|
||||
|
||||
@@ -12,5 +12,4 @@
|
||||
|
||||
.name {
|
||||
color: var(--danger-background);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
.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;
|
||||
@@ -12,7 +9,6 @@
|
||||
background: var(--l2-background);
|
||||
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--l1-foreground);
|
||||
flex: 1;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
.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%;
|
||||
}
|
||||
@@ -1,56 +1,49 @@
|
||||
import { Drawer, DrawerContent } from '@signozhq/ui/drawer';
|
||||
import cx from 'classnames';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
|
||||
import styles from './DetailsPanelDrawer.module.scss';
|
||||
import { ReactNode } from 'react';
|
||||
import { Drawer } from '@signozhq/ui/drawer';
|
||||
|
||||
interface DetailsPanelDrawerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
headerPrefix?: ReactNode;
|
||||
headerSuffix?: ReactNode;
|
||||
loading?: boolean;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
function DetailsPanelDrawer({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
headerPrefix,
|
||||
headerSuffix,
|
||||
loading,
|
||||
testId,
|
||||
}: DetailsPanelDrawerProps): JSX.Element {
|
||||
return (
|
||||
<Drawer
|
||||
type="inspector"
|
||||
stack="overlay"
|
||||
open={isOpen}
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
modal={false}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
width="extra-wide"
|
||||
headerPrefix={headerPrefix}
|
||||
headerSuffix={headerSuffix}
|
||||
loading={loading}
|
||||
testId={testId}
|
||||
>
|
||||
<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>
|
||||
{children}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
DetailsPanelDrawer.defaultProps = {
|
||||
className: undefined,
|
||||
headerPrefix: undefined,
|
||||
headerSuffix: undefined,
|
||||
loading: false,
|
||||
testId: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -68,7 +68,17 @@ export default function DownloadOptionsMenu({
|
||||
aria-modal="true"
|
||||
>
|
||||
<div className="export-format">
|
||||
<Typography.Text className="title">FORMAT</Typography.Text>
|
||||
<Typography
|
||||
variant="uppercase"
|
||||
size="small"
|
||||
weight="medium"
|
||||
color="main"
|
||||
as="p"
|
||||
marginBottom={4}
|
||||
textOverflow="wrap"
|
||||
>
|
||||
FORMAT
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
color="primary"
|
||||
value={exportFormat}
|
||||
@@ -83,7 +93,17 @@ export default function DownloadOptionsMenu({
|
||||
<div className="horizontal-line" />
|
||||
|
||||
<div className="row-limit">
|
||||
<Typography.Text className="title">Number of Rows</Typography.Text>
|
||||
<Typography
|
||||
variant="uppercase"
|
||||
size="small"
|
||||
weight="medium"
|
||||
color="main"
|
||||
as="p"
|
||||
marginBottom={4}
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Number of Rows
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
color="primary"
|
||||
value={String(rowLimit)}
|
||||
@@ -101,7 +121,17 @@ export default function DownloadOptionsMenu({
|
||||
<div className="horizontal-line" />
|
||||
|
||||
<div className="columns-scope">
|
||||
<Typography.Text className="title">Columns</Typography.Text>
|
||||
<Typography
|
||||
variant="uppercase"
|
||||
size="small"
|
||||
weight="medium"
|
||||
color="main"
|
||||
as="p"
|
||||
marginBottom={4}
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Columns
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
color="primary"
|
||||
value={columnsScope}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Trash2, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { AlertDialog } from '@signozhq/ui/alert-dialog';
|
||||
import { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
|
||||
interface DeleteMemberDialogProps {
|
||||
@@ -9,7 +8,7 @@ interface DeleteMemberDialogProps {
|
||||
member: MemberRow | null;
|
||||
isDeleting: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
onConfirm: () => Promise<void>;
|
||||
}
|
||||
|
||||
function DeleteMemberDialog({
|
||||
@@ -36,47 +35,20 @@ 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 (
|
||||
<DialogWrapper
|
||||
<AlertDialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title={title}
|
||||
width="narrow"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
footer={footer}
|
||||
color="danger"
|
||||
confirmText={isDeleting ? 'Processing...' : title}
|
||||
confirmIcon={<Trash2 size={12} />}
|
||||
cancelIcon={<X size={12} />}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onClose}
|
||||
testId="member-delete"
|
||||
>
|
||||
{body}
|
||||
</DialogWrapper>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-20);
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
letter-spacing: -0.07px;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -53,10 +52,8 @@
|
||||
}
|
||||
|
||||
&__email-text {
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
line-height: var(--paragraph-small-400-line-height);
|
||||
letter-spacing: -0.07px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -86,41 +83,26 @@
|
||||
[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(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--uppercase-small-500-font-size);
|
||||
font-weight: var(--uppercase-small-500-font-weight);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-20);
|
||||
line-height: var(--uppercase-small-500-line-height);
|
||||
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;
|
||||
@@ -163,14 +145,6 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -178,8 +152,6 @@
|
||||
}
|
||||
|
||||
&__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;
|
||||
@@ -211,10 +183,8 @@
|
||||
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(--line-height-18);
|
||||
line-height: var(--paragraph-small-400-line-height);
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { Drawer } from '@signozhq/ui/drawer';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
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 { mutate: deleteUser, isLoading: isDeleting } = useDeleteUser({
|
||||
const { mutateAsync: deleteUser, isLoading: isDeleting } = useDeleteUser({
|
||||
mutation: {
|
||||
onSuccess: (): void => {
|
||||
toast.success(
|
||||
@@ -378,11 +378,15 @@ function EditMemberDrawer({
|
||||
onComplete,
|
||||
]);
|
||||
|
||||
const handleDelete = useCallback((): void => {
|
||||
const handleDelete = useCallback(async (): Promise<void> => {
|
||||
if (!member) {
|
||||
return;
|
||||
}
|
||||
deleteUser({ pathParams: { id: member.id } });
|
||||
try {
|
||||
await deleteUser({ pathParams: { id: member.id } });
|
||||
} catch {
|
||||
// `onError` already shows the error modal, and the confirmation stays open.
|
||||
}
|
||||
}, [member, deleteUser]);
|
||||
|
||||
const handleGenerateResetLink = useCallback(async (): Promise<void> => {
|
||||
@@ -409,7 +413,8 @@ function EditMemberDrawer({
|
||||
setShowResetLinkDialog(true);
|
||||
onClose();
|
||||
} else {
|
||||
toast.error('Failed to generate password reset link', {
|
||||
toast.danger('Failed to generate password reset link', {
|
||||
action: { label: 'Dismiss' },
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
@@ -438,7 +443,7 @@ function EditMemberDrawer({
|
||||
|
||||
useEffect(() => {
|
||||
if (copyState.error) {
|
||||
toast.error('Failed to copy link');
|
||||
toast.danger('Failed to copy link', { action: { label: 'Dismiss' } });
|
||||
}
|
||||
}, [copyState.error]);
|
||||
|
||||
@@ -471,20 +476,19 @@ function EditMemberDrawer({
|
||||
<label className="edit-member-drawer__label" htmlFor="member-name">
|
||||
Name
|
||||
</label>
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__field">
|
||||
@@ -540,6 +544,7 @@ function EditMemberDrawer({
|
||||
<RolesSelect
|
||||
id="member-role"
|
||||
mode="multiple"
|
||||
testId="member-roles-select"
|
||||
roles={availableRoles}
|
||||
loading={rolesLoading}
|
||||
isError={rolesError}
|
||||
@@ -615,96 +620,88 @@ function EditMemberDrawer({
|
||||
</div>
|
||||
);
|
||||
|
||||
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>
|
||||
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>
|
||||
|
||||
<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 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>
|
||||
);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<DrawerWrapper
|
||||
<Drawer
|
||||
type="task"
|
||||
stack="overlay"
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
showOverlay={false}
|
||||
onClose={handleClose}
|
||||
title="Member Details"
|
||||
footer={footer}
|
||||
width="wide"
|
||||
footerPrefix={footerStart}
|
||||
footerSuffix={footerActions}
|
||||
>
|
||||
{drawerContent}
|
||||
</DrawerWrapper>
|
||||
</Drawer>
|
||||
|
||||
<ResetLinkDialog
|
||||
open={showResetLinkDialog}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Dialog } from '@signozhq/ui/dialog';
|
||||
|
||||
interface ResetLinkDialogProps {
|
||||
open: boolean;
|
||||
@@ -22,17 +22,11 @@ function ResetLinkDialog({
|
||||
onCopy,
|
||||
}: ResetLinkDialogProps): JSX.Element {
|
||||
return (
|
||||
<DialogWrapper
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
onClose={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">
|
||||
@@ -65,7 +59,7 @@ function ResetLinkDialog({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</DialogWrapper>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
useCreateResetPasswordToken,
|
||||
@@ -41,52 +41,31 @@ jest.mock('api/ErrorResponseHandlerForGeneratedAPIs', () => ({
|
||||
|
||||
jest.mock('@signozhq/ui/drawer', () => ({
|
||||
...jest.requireActual('@signozhq/ui/drawer'),
|
||||
DrawerWrapper: ({
|
||||
Drawer: ({
|
||||
children,
|
||||
footer,
|
||||
footerPrefix,
|
||||
footerSuffix,
|
||||
open,
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
footerPrefix?: ReactNode;
|
||||
footerSuffix?: ReactNode;
|
||||
open: boolean;
|
||||
}): JSX.Element | null =>
|
||||
open ? (
|
||||
<div>
|
||||
{children}
|
||||
{footer}
|
||||
{footerPrefix}
|
||||
{footerSuffix}
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
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'),
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
...jest.requireActual('@signozhq/ui/toast'),
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
danger: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -209,7 +188,7 @@ describe('EditMemberDrawer', () => {
|
||||
isLoading: false,
|
||||
});
|
||||
(useDeleteUser as jest.Mock).mockReturnValue({
|
||||
mutate: mockDeleteMutate,
|
||||
mutateAsync: mockDeleteMutate,
|
||||
isLoading: false,
|
||||
});
|
||||
// Token query: valid token for invited members
|
||||
@@ -323,8 +302,11 @@ describe('EditMemberDrawer', () => {
|
||||
renderDrawer({ onComplete });
|
||||
|
||||
// signoz-admin is already selected; add signoz-editor on top
|
||||
await user.click(screen.getByLabelText('Roles'));
|
||||
await user.click(await screen.findByTitle('signoz-editor'));
|
||||
await user.click(screen.getByTestId('member-roles-select'));
|
||||
await user.type(
|
||||
await screen.findByTestId('member-roles-select-search'),
|
||||
`${managedRoles[1].name}{Enter}`,
|
||||
);
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||
await waitFor(() =>
|
||||
@@ -348,11 +330,11 @@ describe('EditMemberDrawer', () => {
|
||||
renderDrawer({ onComplete });
|
||||
|
||||
// signoz-admin appears as a selected tag — click its remove button to deselect
|
||||
const adminTag = await screen.findByTitle('signoz-admin');
|
||||
const removeBtn = adminTag.querySelector(
|
||||
'.ant-select-selection-item-remove',
|
||||
) as Element;
|
||||
await user.click(removeBtn);
|
||||
await user.click(
|
||||
await screen.findByTestId(
|
||||
`member-roles-select-chip-${managedRoles[0].id}-remove`,
|
||||
),
|
||||
);
|
||||
|
||||
const saveBtn = screen.getByRole('button', { name: /save member details/i });
|
||||
await waitFor(() =>
|
||||
@@ -373,7 +355,7 @@ describe('EditMemberDrawer', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockDeleteMutate.mockImplementation(() => {
|
||||
mutateAsync: mockDeleteMutate.mockImplementation(() => {
|
||||
options?.mutation?.onSuccess?.();
|
||||
}),
|
||||
isLoading: false,
|
||||
@@ -453,7 +435,7 @@ describe('EditMemberDrawer', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockDeleteMutate.mockImplementation(() => {
|
||||
mutateAsync: mockDeleteMutate.mockImplementation(() => {
|
||||
options?.mutation?.onSuccess?.();
|
||||
}),
|
||||
isLoading: false,
|
||||
@@ -567,7 +549,7 @@ describe('EditMemberDrawer', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockDeleteMutate.mockImplementation(() => {
|
||||
mutateAsync: mockDeleteMutate.mockImplementation(() => {
|
||||
options?.mutation?.onError?.({});
|
||||
}),
|
||||
isLoading: false,
|
||||
@@ -598,7 +580,7 @@ describe('EditMemberDrawer', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
(useDeleteUser as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockDeleteMutate.mockImplementation(() => {
|
||||
mutateAsync: mockDeleteMutate.mockImplementation(() => {
|
||||
options?.mutation?.onError?.({});
|
||||
}),
|
||||
isLoading: false,
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
// The mask of the error modal blurs the page and any dialog under it, so a dialog's
|
||||
// own backdrop would dim the page a second time.
|
||||
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(
|
||||
@@ -65,7 +59,6 @@
|
||||
&__value {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.48px;
|
||||
|
||||
@@ -18,6 +18,12 @@ type Props = {
|
||||
open?: boolean;
|
||||
};
|
||||
|
||||
// Above every `@signozhq/ui` dialog (`--dialog-z-index: 1001`, plus one per
|
||||
// nested layer), so the error shows over the dialog whose action failed.
|
||||
// TODO: temporary. Drop it once this moves to the `@signozhq/ui` Dialog, which
|
||||
// stacks above the layer that opened it.
|
||||
const ERROR_MODAL_Z_INDEX = 2000;
|
||||
|
||||
const classNames = {
|
||||
body: 'error-modal__body',
|
||||
mask: 'error-modal__mask',
|
||||
@@ -53,8 +59,13 @@ function ErrorModal({
|
||||
onClick={(): void => setVisible(true)}
|
||||
onKeyDown={undefined}
|
||||
>
|
||||
<Badge variant="solid" color="danger">
|
||||
<CircleAlert size={14} color={Color.BG_CHERRY_500} /> error
|
||||
<Badge
|
||||
variant="solid"
|
||||
color="danger"
|
||||
textTransform="uppercase"
|
||||
prefix={<CircleAlert size={14} />}
|
||||
>
|
||||
error
|
||||
</Badge>
|
||||
</span>
|
||||
) : (
|
||||
@@ -90,6 +101,7 @@ function ErrorModal({
|
||||
closeIcon={false}
|
||||
classNames={classNames}
|
||||
wrapClassName="error-modal__wrap"
|
||||
zIndex={ERROR_MODAL_Z_INDEX}
|
||||
>
|
||||
<ErrorContent error={error} />
|
||||
</Modal>
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
margin: 0;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
@@ -49,8 +48,6 @@
|
||||
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;
|
||||
@@ -79,7 +76,6 @@
|
||||
color: var(--l2-foreground);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: 0.48px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -98,7 +94,6 @@
|
||||
color: var(--l1-foreground);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 180% */
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
@@ -133,16 +128,12 @@
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Geist Mono;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
font-family: var(--code-base-400-font-family);
|
||||
padding: 3px 12px;
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
&__message-item::before {
|
||||
font-family: unset;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.export-menu-popover {
|
||||
width: 240px;
|
||||
padding: 0 12px 12px 12px;
|
||||
// The popover shell carries no padding of its own, so the content owns it.
|
||||
padding: var(--spacing-8);
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--periscope-font-size-small);
|
||||
font-size: var(--uppercase-small-500-font-size);
|
||||
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(--periscope-font-size-base);
|
||||
font-size: var(--label-base-400-font-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Download } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
|
||||
import { Popover, usePopoverHandle } from '@signozhq/ui/popover';
|
||||
import { RadioGroup } from '@signozhq/ui/radio-group';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
@@ -36,6 +36,10 @@ 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,
|
||||
@@ -45,53 +49,72 @@ export default function ExportMenu({
|
||||
});
|
||||
|
||||
const handleExport = useCallback((): void => {
|
||||
setIsPopoverOpen(false);
|
||||
menu.close();
|
||||
handleClientExport({ format: exportFormat as ExportFormat });
|
||||
}, [exportFormat, handleClientExport]);
|
||||
}, [menu, exportFormat, handleClientExport]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<>
|
||||
<Tooltip title="Download">
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
aria-label="Download"
|
||||
testId={`export-menu-${dataSource}`}
|
||||
loading={isExporting}
|
||||
>
|
||||
<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>
|
||||
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
width="100%"
|
||||
onClick={handleExport}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon
|
||||
aria-label="Download"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={isPopoverOpen}
|
||||
testId={`export-menu-${dataSource}`}
|
||||
loading={isExporting}
|
||||
prefix={<Download size={16} />}
|
||||
onClick={(event): void => menu.showAt(event.currentTarget)}
|
||||
>
|
||||
Export
|
||||
<Download size={14} />
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</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>
|
||||
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
width="100%"
|
||||
onClick={handleExport}
|
||||
loading={isExporting}
|
||||
prefix={<Download size={16} />}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,9 +117,16 @@ function AddedFields({
|
||||
<div className={styles.sectionHeader}>
|
||||
<span>ADDED FIELDS</span>
|
||||
{maxFields !== undefined && (
|
||||
<Typography.Text size="sm" weight="medium" color="muted">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="medium"
|
||||
color="secondary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Max Allowed: {maxFields}
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.addedList}>
|
||||
|
||||
@@ -32,12 +32,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Full-bleed search bar: only top/bottom borders, square corners.
|
||||
// Per-side widths cannot be expressed via --input-border-width.
|
||||
.searchInput {
|
||||
background-color: var(--l1-background);
|
||||
height: 40px;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
--input-border-radius: 0;
|
||||
|
||||
[data-slot='input'] {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
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>
|
||||
<section className={styles.searchInput}>
|
||||
<Input
|
||||
className={styles.searchInput}
|
||||
type="text"
|
||||
size="md"
|
||||
value={inputValue}
|
||||
placeholder="Search for a field..."
|
||||
onChange={handleInputChange}
|
||||
|
||||
@@ -13,9 +13,9 @@ jest.mock('hooks/querySuggestions/useFieldKeysSuggestion', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
...jest.requireActual('@signozhq/ui/toast'),
|
||||
toast: { success: jest.fn(), danger: jest.fn() },
|
||||
}));
|
||||
|
||||
// FloatingPanel is a react-rnd/portal shell — presentation only. Render its
|
||||
|
||||
@@ -4,9 +4,16 @@ function AnnouncementsModal(): JSX.Element {
|
||||
return (
|
||||
<div className="announcements-modal-container">
|
||||
<div className="announcements-modal-container-header">
|
||||
<Typography.Text className="announcements-modal-title">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Announcements
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
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';
|
||||
@@ -39,7 +40,8 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
})
|
||||
.catch(() => {
|
||||
console.error(`Failed to submit ${entityName}`);
|
||||
toast.error(`Failed to submit ${entityName}`, {
|
||||
toast.danger(`Failed to submit ${entityName}`, {
|
||||
action: { label: 'Dismiss' },
|
||||
position: 'top-right',
|
||||
});
|
||||
})
|
||||
@@ -136,14 +138,23 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
Submit
|
||||
</Button>
|
||||
<div className="feedback-modal-content-footer-info-text">
|
||||
<Typography.Text>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Have a specific issue?{' '}
|
||||
<Typography.Link
|
||||
className="contact-support-link"
|
||||
<SignozButton
|
||||
variant="link"
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={handleContactSupportClick}
|
||||
>
|
||||
Contact Support{' '}
|
||||
</Typography.Link>
|
||||
</SignozButton>
|
||||
or{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/introduction/"
|
||||
@@ -153,7 +164,7 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,11 +68,6 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,16 @@ function HeaderRightSection({
|
||||
}
|
||||
prefix={<Noz size={20} />}
|
||||
>
|
||||
<Typography.Text>Noz</Typography.Text>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Noz
|
||||
</Typography>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
@@ -141,9 +141,16 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
{(shareURLWithTime || isRouteToBeSharedWithTime) && (
|
||||
<>
|
||||
<div className="absolute-relative-time-toggler-container">
|
||||
<Typography.Text className="absolute-relative-time-toggler-label">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="medium"
|
||||
color="main"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Enable absolute time
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
|
||||
<div className="absolute-relative-time-toggler">
|
||||
{!isValidateRelativeTime && (
|
||||
@@ -172,9 +179,16 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
|
||||
{extraOption && (
|
||||
<div className="absolute-relative-time-toggler-container">
|
||||
<Typography.Text className="absolute-relative-time-toggler-label">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="medium"
|
||||
color="main"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
{extraOption.label}
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
<div className="absolute-relative-time-toggler">
|
||||
<Switch
|
||||
color="primary"
|
||||
@@ -189,12 +203,26 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
<div className="share-link">
|
||||
<div className="url-share-container">
|
||||
<div className="url-share-container-header">
|
||||
<Typography.Text className="url-share-title">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="medium"
|
||||
color="main"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Share page link
|
||||
</Typography.Text>
|
||||
<Typography.Text className="url-share-sub-title">
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="regular"
|
||||
color="tertiary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Share the current page link with your team member
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Mock dependencies before imports
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { toast } from '@signozhq/ui/toast';
|
||||
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/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
jest.mock('@signozhq/ui/toast', () => ({
|
||||
...jest.requireActual('@signozhq/ui/toast'),
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
danger: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('FeedbackModal', () => {
|
||||
isCloudUser: false,
|
||||
});
|
||||
mockToast.success.mockClear();
|
||||
mockToast.error.mockClear();
|
||||
mockToast.danger.mockClear();
|
||||
});
|
||||
|
||||
it('should render feedback modal with all tabs', () => {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
.infra-container-card-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
color: var(--l2-foreground);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
.infra-container-card-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
color: var(--l2-foreground);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
@@ -52,9 +52,16 @@ export default function WaitlistFragment({
|
||||
|
||||
return (
|
||||
<div className="wait-list-container">
|
||||
<Typography.Text className="wait-list-text">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="base"
|
||||
weight="regular"
|
||||
color="inherit"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
{t('waitlist_message')}
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
className="periscope-btn join-waitlist-btn"
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
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.
|
||||
@@ -31,66 +34,41 @@
|
||||
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 {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
position: relative;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.ant-select:hover .ant-select-selector,
|
||||
.ant-select-focused .ant-select-selector {
|
||||
&:focus-within {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&::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 {
|
||||
// The Input has no token for the native number spinners.
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
@@ -114,16 +92,7 @@
|
||||
|
||||
&.labelAfter {
|
||||
.input {
|
||||
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;
|
||||
}
|
||||
--input-border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
@@ -49,21 +49,51 @@ function InputWithLabel({
|
||||
labelAfter,
|
||||
})}
|
||||
>
|
||||
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||
<Input
|
||||
{!labelAfter && (
|
||||
<div className="label">
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="regular"
|
||||
color="secondary"
|
||||
as="p"
|
||||
textOverflow="ellipsis"
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cx('input', {
|
||||
'input__has-label-after': !labelAfter,
|
||||
'input__has-close-button': !!onClose,
|
||||
})}
|
||||
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>}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
{onClose && (
|
||||
<Button
|
||||
className="periscope-btn ghost close-btn"
|
||||
|
||||
@@ -55,26 +55,12 @@
|
||||
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 {
|
||||
@@ -83,17 +69,6 @@
|
||||
gap: var(--spacing-2);
|
||||
flex: 0 0 160px;
|
||||
width: 160px;
|
||||
|
||||
:global(.roles-single-select) {
|
||||
width: 100%;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
background-color: var(
|
||||
--invite-members-field-background,
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cellAction {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CircleAlert, Plus, Trash2 } from '@signozhq/icons';
|
||||
import { CircleAlert, Plus, SolidCheckCircle2, Trash2 } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
@@ -68,20 +68,30 @@ function InviteMembers({
|
||||
<div className={styles.table}>
|
||||
{showHeader && (
|
||||
<div className={styles.header}>
|
||||
<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>
|
||||
<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>
|
||||
<div className={styles.headerCellAction} />
|
||||
</div>
|
||||
)}
|
||||
@@ -97,12 +107,19 @@ function InviteMembers({
|
||||
onChange={(e): void => updateEmail(row.id, e.target.value)}
|
||||
name={`invite-email-${row.id}`}
|
||||
autoComplete="email"
|
||||
data-testid={`invite-email-${row.id}`}
|
||||
testId={`invite-email-${row.id}`}
|
||||
/>
|
||||
{emailValidity[row.id] === false && row.email.trim() !== '' && (
|
||||
<Typography.Text size="small" className={styles.errorText}>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="regular"
|
||||
color="danger"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Invalid email address
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -113,6 +130,7 @@ function InviteMembers({
|
||||
onChange={(roleIds): void => updateRole(row.id, roleIds)}
|
||||
placeholder="Select roles"
|
||||
id={`invite-role-${row.id}`}
|
||||
testId={`invite-role-${row.id}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -152,61 +170,90 @@ function InviteMembers({
|
||||
</div>
|
||||
|
||||
{hasValidationErrors && (
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
icon={<CircleAlert size={12} />}
|
||||
className={styles.callout}
|
||||
data-testid="invite-validation-error"
|
||||
>
|
||||
{getValidationErrorMessage()}
|
||||
</Callout>
|
||||
<div className={styles.callout}>
|
||||
<Callout
|
||||
color="danger"
|
||||
size="sm"
|
||||
icon={<CircleAlert />}
|
||||
testId="invite-validation-error"
|
||||
>
|
||||
{getValidationErrorMessage()}
|
||||
</Callout>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasResults && hasFailures && (
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{hasResults && !hasFailures && hasSuccesses && (
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{renderFooter?.({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import InviteMembers from '../InviteMembers';
|
||||
|
||||
import { pickRole } from './roleSelectUtils';
|
||||
import {
|
||||
CREATE_USER_ENDPOINT,
|
||||
createErrorHandler,
|
||||
@@ -68,8 +69,7 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
@@ -100,8 +100,7 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
@@ -132,18 +131,14 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
await expect(
|
||||
screen.findByTestId('invite-api-error'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
const viewerElements = screen.getAllByTitle('Viewer');
|
||||
await user.click(viewerElements[0]);
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
await pickRole(user, 0, 'role-editor');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('invite-api-error')).not.toBeInTheDocument();
|
||||
@@ -189,8 +184,7 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
const submitBtn = screen.getByTestId('submit-btn');
|
||||
await user.click(submitBtn);
|
||||
@@ -226,8 +220,7 @@ describe('InviteMembers - Edge Cases', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], ' alice@signoz.io ');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import InviteMembers from '../InviteMembers';
|
||||
|
||||
import { pickRole } from './roleSelectUtils';
|
||||
import {
|
||||
CREATE_USER_ENDPOINT,
|
||||
createErrorHandler,
|
||||
@@ -40,8 +41,7 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -73,18 +73,13 @@ describe('InviteMembers - Submission', () => {
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.type(emailInputs[1], 'bob@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
await pickRole(user, 1, 'role-editor');
|
||||
|
||||
await user.type(emailInputs[2], 'charlie@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const adminOptions = await screen.findAllByTitle('Admin');
|
||||
await user.click(adminOptions[adminOptions.length - 1]);
|
||||
await pickRole(user, 2, 'role-admin');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -125,8 +120,7 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -154,8 +148,7 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -218,13 +211,10 @@ describe('InviteMembers - Submission', () => {
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.type(emailInputs[1], 'bob@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const editorOptions = await screen.findAllByTitle('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
await pickRole(user, 1, 'role-editor');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -276,8 +266,7 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -303,8 +292,7 @@ describe('InviteMembers - Submission', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import InviteMembers from '../InviteMembers';
|
||||
|
||||
import { pickRole } from './roleSelectUtils';
|
||||
import {
|
||||
createRolesHandler,
|
||||
createSuccessHandler,
|
||||
@@ -35,8 +36,7 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -60,8 +60,7 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
@@ -85,8 +84,7 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], INVALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
await expect(
|
||||
@@ -149,8 +147,7 @@ describe('InviteMembers - Validation', () => {
|
||||
screen.findByText('Please select roles for team members'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@@ -204,8 +201,7 @@ describe('InviteMembers - Validation', () => {
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
|
||||
await user.type(emailInputs[0], VALID_EMAIL);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByTitle('Viewer'));
|
||||
await pickRole(user, 0, 'role-viewer');
|
||||
|
||||
await user.click(screen.getByTestId('submit-btn'));
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
const ROLE_TRIGGER_TEST_ID = /^invite-role-[0-9a-f-]{36}$/;
|
||||
|
||||
type User = ReturnType<typeof userEvent.setup>;
|
||||
|
||||
export function getRoleTrigger(rowIndex: number): HTMLElement {
|
||||
return screen.getAllByTestId(ROLE_TRIGGER_TEST_ID)[rowIndex];
|
||||
}
|
||||
|
||||
// jsdom cannot deliver the pointer click Base UI expects on an option, so the
|
||||
// option is reached with the arrow keys and picked with Enter.
|
||||
export async function pickRole(
|
||||
user: User,
|
||||
rowIndex: number,
|
||||
roleId: string,
|
||||
): Promise<void> {
|
||||
const triggerTestId = getRoleTrigger(rowIndex).getAttribute('data-testid');
|
||||
const itemTestId = `${triggerTestId}-item-${roleId}`;
|
||||
|
||||
await user.click(getRoleTrigger(rowIndex));
|
||||
|
||||
const item = await screen.findByTestId(itemTestId);
|
||||
const index = screen
|
||||
.getAllByTestId(new RegExp(`^${triggerTestId}-item-`))
|
||||
.indexOf(item);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId(`${triggerTestId}-search`)).toHaveFocus(),
|
||||
);
|
||||
await user.keyboard(`${'{ArrowDown}'.repeat(index + 1)}{Enter}`);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId(itemTestId)).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
),
|
||||
);
|
||||
|
||||
await user.keyboard('{Escape}');
|
||||
await waitFor(() =>
|
||||
expect(getRoleTrigger(rowIndex)).toHaveAttribute('aria-expanded', 'false'),
|
||||
);
|
||||
}
|
||||
@@ -25,10 +25,6 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -107,6 +103,6 @@
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--label-small-400-font-size);
|
||||
color: var(--danger-foreground);
|
||||
}
|
||||
|
||||
@@ -116,15 +116,31 @@ function FilterZone({
|
||||
{rightSlot}
|
||||
</div>
|
||||
{error && (
|
||||
<Typography.Text
|
||||
className={styles.error}
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="regular"
|
||||
color="danger"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
testId={`${testIdPrefix}-query-error`}
|
||||
>
|
||||
{error}
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
)}
|
||||
<div className={styles.filtersRow}>
|
||||
<Typography.Text className={styles.filtersLabel}>Filters</Typography.Text>
|
||||
<Typography
|
||||
variant="uppercase"
|
||||
size="small"
|
||||
weight="medium"
|
||||
color="secondary"
|
||||
as="span"
|
||||
display="inline-block"
|
||||
marginRight={1}
|
||||
textOverflow="wrap"
|
||||
>
|
||||
Filters
|
||||
</Typography>
|
||||
{wrapControl(
|
||||
<div
|
||||
className={styles.filterControls}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
:global(.cm-dsl-logic) {
|
||||
color: var(--bg-sakura-400);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--label-base-500-font-weight);
|
||||
}
|
||||
|
||||
:global(.cm-dsl-string) {
|
||||
@@ -59,7 +59,7 @@
|
||||
content: 'SUGGESTIONS';
|
||||
padding: 12px 14px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--uppercase-small-500-font-weight);
|
||||
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(--font-size-xs);
|
||||
font-size: var(--label-small-400-font-size);
|
||||
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(--font-size-xs);
|
||||
font-size: var(--label-small-400-font-size);
|
||||
color: var(--l2-foreground);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -23,21 +25,11 @@
|
||||
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(--font-size-lg);
|
||||
font-size: var(--label-large-500-font-size);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-weight: var(--label-large-500-font-weight);
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.09px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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';
|
||||
@@ -7,6 +9,7 @@ interface Props {
|
||||
label: string;
|
||||
count: number;
|
||||
isModified: boolean;
|
||||
isCountLoading?: boolean;
|
||||
action?: ReactNode;
|
||||
// Ids: `${testIdPrefix}-list-heading`.
|
||||
testIdPrefix?: string;
|
||||
@@ -16,20 +19,33 @@ function ListCommandHeader({
|
||||
label,
|
||||
count,
|
||||
isModified,
|
||||
isCountLoading,
|
||||
action,
|
||||
testIdPrefix,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.commandHeader}>
|
||||
<div className={styles.headingBlock}>
|
||||
<Typography.Title
|
||||
className={styles.title}
|
||||
<Typography
|
||||
variant="label"
|
||||
size="large"
|
||||
weight="medium"
|
||||
color="main"
|
||||
as="h1"
|
||||
marginY={4}
|
||||
textOverflow="ellipsis"
|
||||
testId={testIdPrefix && `${testIdPrefix}-list-heading`}
|
||||
>
|
||||
{label}
|
||||
</Typography.Title>
|
||||
</Typography>
|
||||
{isModified && <span className={styles.dirtyDot} title="Unsaved changes" />}
|
||||
<span className={styles.countPill}>{count}</span>
|
||||
{isCountLoading ? (
|
||||
<Skeleton.Input size="sm" width={24} />
|
||||
) : (
|
||||
<Badge variant="solid" color="secondary">
|
||||
{count}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.grow} />
|
||||
{action}
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--label-small-400-font-size);
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
@@ -36,9 +36,16 @@ function ListStatusBar({
|
||||
>
|
||||
{collapsed ? 'Expand' : 'Collapse'}
|
||||
</Button>
|
||||
<Typography.Text className={styles.count}>
|
||||
<Typography
|
||||
variant="paragraph"
|
||||
size="small"
|
||||
weight="regular"
|
||||
color="tertiary"
|
||||
as="span"
|
||||
textOverflow="wrap"
|
||||
>
|
||||
{count} of {total} {noun}
|
||||
</Typography.Text>
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user