Compare commits

...

137 Commits

Author SHA1 Message Date
Vinícius Lourenço
6f4dd57e43 chore(deps): bump @signozhq/ui to 0.4.0-rc.3 2026-11-13 20:33:52 -03:00
Vinícius Lourenço
3699101218 fix(dashboard): make the add panel section chevron a solid primary button 2026-11-13 20:27:02 -03:00
Vinícius Lourenço
ada1d12f62 fix(dashboard): keep the panel header search input at the sm size 2026-11-13 20:26:54 -03:00
Vinícius Lourenço
13ba955b18 fix(dashboard): use the l2 surface for the variable impact rows 2026-11-13 20:26:51 -03:00
Vinícius Lourenço
d6359d6592 fix(dashboard): scope the compact badge sizes to the variable type tab items 2026-11-13 20:26:48 -03:00
Vinícius Lourenço
5268cb1cd3 fix(query-builder): pin the labelled and bucket inputs to the sm size 2026-11-13 20:12:51 -03:00
Vinícius Lourenço
32096b9b92 fix(dashboard): let the view panel modal show through the query builder 2026-11-13 20:12:48 -03:00
Vinícius Lourenço
bb00052a8a fix(members): use l3 surface for the invite link row in the edit member drawer 2026-11-13 18:57:02 -03:00
Vinícius Lourenço
70ca09c313 chore(app-layout): drop the unused slow api warning banner styles 2026-11-13 18:57:02 -03:00
Vinícius Lourenço
15e0f859f8 fix(sso): drop the native input overrides from the attribute and claim mappings 2026-11-13 18:57:02 -03:00
Vinícius Lourenço
a1b99e733c fix(dashboard): keep the variable type tab badges compact 2026-11-13 18:57:01 -03:00
Vinícius Lourenço
adc4602337 fix(traces): put the span drawer close at the start and the span nav at the end 2026-11-13 18:57:01 -03:00
Vinícius Lourenço
07b3c5a5a5 fix(dashboard): render panel type tiles with Typography and restyle the modal actions 2026-11-13 18:57:01 -03:00
Vinícius Lourenço
78749045fc fix(alerts): size the channel name in the delete dialog 2026-11-13 18:57:01 -03:00
Vinícius Lourenço
b0184a68ef fix(bottom-strip): show the no-auth banner above the bottom strip 2026-11-13 18:57:01 -03:00
Vinícius Lourenço
1a8bad7594 fix(query-builder): match the labelled inputs to the group by row 2026-11-13 18:57:00 -03:00
Vinícius Lourenço
cd7d514a39 fix(query-builder): restore the 13px size on the every and by labels 2026-11-13 18:57:00 -03:00
Vinícius Lourenço
d64ed13e00 fix(dashboard): render hidden variable values as list items 2026-11-13 18:57:00 -03:00
Vinícius Lourenço
df81a5f53d chore(typography): drop the unsupported italic TODOs 2026-11-13 18:57:00 -03:00
Vinícius Lourenço
4411f56bea chore(query-builder): drop the dead y-axis unit label rule and its TODO 2026-11-13 18:57:00 -03:00
Vinícius Lourenço
43ecb7ffdd fix(onboarding): restore the hover colour on the data source filters 2026-11-13 18:56:59 -03:00
Vinícius Lourenço
fe04c20005 fix(alerts): restore the medium weight on the alert not found support line 2026-11-13 18:56:59 -03:00
Vinícius Lourenço
9a0abba30c fix(gcp): render the read-only connection values in mono 2026-11-13 18:56:59 -03:00
Vinícius Lourenço
66ccccfa1b fix(settings): restore the license key code box 2026-11-13 18:56:59 -03:00
Vinícius Lourenço
275ffe4b48 fix(ingestion): restore copy buttons on the ingestion settings values 2026-11-13 18:56:59 -03:00
Vinícius Lourenço
3849b50868 refactor(logs-settings): render the tab label as plain text 2026-11-13 18:56:58 -03:00
Vinícius Lourenço
d4aa9969c1 fix(infra-monitoring): open the k8s events docs through a real link 2026-11-13 18:56:58 -03:00
Vinícius Lourenço
3115db5fa5 fix(ui): run the contact support links through Typography.Link 2026-11-13 18:56:58 -03:00
Vinícius Lourenço
950701ab62 fix(celery): restore the worker count font size 2026-11-13 18:56:58 -03:00
Vinícius Lourenço
8176e4d1ab fix(dashboard): render the panel search clear button as a link 2026-11-13 18:56:58 -03:00
Vinícius Lourenço
3a51eb5a06 fix(dashboard): match the dashboard name size to the delete confirmation text 2026-11-13 18:56:57 -03:00
Vinícius Lourenço
fb69d378bb fix(workspace): keep the Typography title size and center the locked page description 2026-11-13 18:56:57 -03:00
Vinícius Lourenço
0db7eafd4b fix(workspace): restore the hero title size on the locked and suspended pages 2026-11-13 18:56:57 -03:00
Vinícius Lourenço
91e85ee2f9 fix(signup): render the password fields with the same Input as the email 2026-11-13 18:56:57 -03:00
Vinícius Lourenço
3768dd8e2f fix(sso): drop the native input overrides that drew a second frame inside Input 2026-11-13 18:56:56 -03:00
Vinícius Lourenço
3e3b879cfb chore(deps): bump @signozhq/ui to 0.4.0-rc.2 2026-11-13 18:56:56 -03:00
Vinícius Lourenço
09f2cff332 refactor(styles): replace the legacy card tokens with surface-2 and l1-foreground 2026-11-13 18:56:56 -03:00
Vinícius Lourenço
4756e87600 fix(alerts): pad the top of the downtime, routing policy and channel headers 2026-11-13 18:56:56 -03:00
Vinícius Lourenço
df7ba563f3 fix(metrics-explorer): give the Where and From labels one fixed width 2026-11-13 18:56:55 -03:00
Vinícius Lourenço
953d513112 style(service-accounts): match the key field to the l3 surface and border 2026-11-13 18:56:55 -03:00
Vinícius Lourenço
b40bb46419 refactor(fields-selector): use the md input size 2026-11-13 18:56:55 -03:00
Vinícius Lourenço
9c1cd873b7 fix(toaster): clear the pagination page-size select with the bottom offset 2026-11-13 18:56:55 -03:00
Manika Malhotra
5efa9ff48d fix: adopt @signozhq/ui 0.4.0-rc.1 in the endpoint picker and query suggestions added on main 2026-11-13 18:56:54 -03:00
Vinícius Lourenço
b2240fd469 test(storybook): cover workspace domain conflict and onboarding facing issues 2026-11-13 18:56:54 -03:00
Vinícius Lourenço
e2bab7506c test(storybook): cover invite roles and move members and service account plays to play files 2026-11-13 18:56:54 -03:00
Vinícius Lourenço
357b1b7eb3 refactor(roles-select): render the roles select with Combobox 2026-11-13 18:56:54 -03:00
Vinícius Lourenço
ad44c4a7ca docs(modals): note the antd modals that open under ui dialogs 2026-11-13 18:56:54 -03:00
Vinícius Lourenço
85d4428864 fix(error-modal): show the error modal above the dialog that raised it 2026-11-13 18:56:53 -03:00
Vinícius Lourenço
c574e13e3a fix(query-builder): restore the case of the suggestion type tag and the suggested filters heading 2026-11-13 18:56:53 -03:00
Vinícius Lourenço
1579519ea9 fix(billing): run the past-due update action through Callout.Link 2026-11-13 18:56:53 -03:00
Vinícius Lourenço
5aa274048f fix(callout): draw links inside callouts with Callout.Link 2026-11-13 18:56:53 -03:00
Vinícius Lourenço
1663a5a225 test(storybook): wait on danger toasts through their visible title and action 2026-11-13 18:56:53 -03:00
Vinícius Lourenço
bacd493f82 fix(triggered-alerts): let the search input fill the filter row again 2026-11-13 18:56:52 -03:00
Vinícius Lourenço
77412f2dfa fix(authz): forward the ref through AuthZButton so a tooltip can anchor to it 2026-11-13 18:56:52 -03:00
Vinícius Lourenço
cfe53393e4 refactor(list-views): rename a view from a dialog instead of a popover 2026-11-13 18:56:52 -03:00
Vinícius Lourenço
dd40a8f2fe fix(channels): pass read-only to every channel setting instead of a fieldset 2026-11-13 18:56:52 -03:00
Vinícius Lourenço
473ada70dd fix(channels): disable read-only channel fields through the form 2026-11-13 18:56:52 -03:00
Vinícius Lourenço
02accc8e44 fix(gcp): space the setup drawer footer buttons 2026-11-13 18:56:51 -03:00
Vinícius Lourenço
f6587f7b17 fix(infra-monitoring): move the drawer close button to the start of the header 2026-11-13 18:56:51 -03:00
Vinícius Lourenço
a470d90a33 fix(callout): use primary instead of info for informational callouts 2026-11-13 18:56:51 -03:00
Vinícius Lourenço
e514ac102a fix(messaging-queues): align the custom interval input with its old place 2026-11-13 18:56:51 -03:00
Vinícius Lourenço
edfa99dbbf fix(logs): keep the log navigation arrows at the end of the drawer header 2026-11-13 18:56:51 -03:00
Vinícius Lourenço
ee4383e10d refactor(list-views): save a view from a dialog instead of a popover 2026-11-13 18:56:50 -03:00
Vinícius Lourenço
bcfe40fab5 revert(button): drop the geometricPrecision text rendering on buttons 2026-11-13 18:56:50 -03:00
Vinícius Lourenço
170787f117 fix(quick-filters): restore the square sync tag for the query name 2026-11-13 18:56:50 -03:00
Vinícius Lourenço
2c785f06c3 fix(list-views): size the count skeleton to the Badge 2026-11-13 18:56:50 -03:00
Vinícius Lourenço
7e4b09cddf refactor(list-views): show header counts in Badge with a skeleton while loading 2026-11-13 18:56:50 -03:00
Vinícius Lourenço
d280958de3 refactor(fonts): load Inter from design-tokens and drop the subset script 2026-11-13 18:56:49 -03:00
Vinícius Lourenço
30036f7088 fix(logs): animate the log details drawer open and close 2026-11-13 18:56:49 -03:00
Vinícius Lourenço
6a9a69f791 feat(logs): move the log details drawer to the new Drawer 2026-11-13 18:56:49 -03:00
Vinícius Lourenço
2cd0997874 fix(dialog): keep dialogs above antd error modals like main did 2026-11-13 18:56:49 -03:00
Vinícius Lourenço
e554f2e3a9 fix(custom-domain): show the domain update as a library toast with an action 2026-11-13 18:56:49 -03:00
Vinícius Lourenço
f116962b67 test(stories): wait for the GCP combobox to open before typing 2026-11-13 18:56:48 -03:00
Vinícius Lourenço
48fa440a9e test(stories): drop the feedback fixture toaster, the providers mount one 2026-11-13 18:56:48 -03:00
Vinícius Lourenço
210cb7e97d test(stories): give the overview drawer story the entity width and open its time picker 2026-11-13 18:56:48 -03:00
Vinícius Lourenço
3d2861d0f0 feat(k8s): step back through the entity drawer with the breadcrumb trail 2026-11-13 18:56:48 -03:00
Vinícius Lourenço
19270f8663 refactor(drawer): set stack on every drawer for the trail-capable Drawer 2026-11-13 18:56:48 -03:00
Vinícius Lourenço
987c0dd873 test(stories): drop the Home feedback story, Home has no Feedback button 2026-11-13 18:56:47 -03:00
Vinícius Lourenço
766e812b78 fix(button): render labels with fractional advances to keep word gaps 2026-11-13 18:56:47 -03:00
Vinícius Lourenço
7865602243 fix(fonts): mark the Inter subsets unhinted so FreeType keeps word spaces 2026-11-13 18:56:47 -03:00
Vinícius Lourenço
b52f139465 fix(ui): restore list heading spacing, muted and inline links, and quick filter height 2026-11-13 18:56:47 -03:00
Vinícius Lourenço
5288635d2c fix(button): keep the Run Query and Create new funnel labels whole 2026-11-13 18:56:47 -03:00
Vinícius Lourenço
cbcc02171d fix(k8s): restore the instrumentation checks header and the 70vw entity drawer 2026-11-13 18:56:46 -03:00
Vinícius Lourenço
e5f1f4fce8 test(stories): type into Input fields and let the modal settle before the nested select 2026-11-13 18:56:46 -03:00
Vinícius Lourenço
bdda0efdde test(stories): type into the Input field and scope picks so plays reach their end state 2026-11-13 18:56:46 -03:00
Vinícius Lourenço
396e09041b fix(input): size number inputs, label chips and the integrations search through the new Input frame 2026-11-13 18:56:46 -03:00
Vinícius Lourenço
7a31af2590 fix(toast): keep toasts clear of drawer, dialog and page controls 2026-11-13 18:56:46 -03:00
Vinícius Lourenço
f832717f26 test: pick combobox and command items through the search row 2026-11-13 18:56:45 -03:00
Vinícius Lourenço
d6dfbde482 test(drawer): follow the new close button and back control 2026-11-13 18:56:45 -03:00
Vinícius Lourenço
15852e694d fix(traces): step through spans with the arrow keys inside the new drawer 2026-11-13 18:56:45 -03:00
Vinícius Lourenço
fbc8c61527 test(gcp): pick the deployment region through the combobox search row 2026-11-13 18:56:45 -03:00
Vinícius Lourenço
336f4f08b5 test: update snapshots for the new component class hashes and lazy tooltip 2026-11-13 18:56:45 -03:00
Vinícius Lourenço
e5bfe1c5b7 refactor(styles): replace the removed design tokens main added since 2026-11-13 18:56:45 -03:00
Vinícius Lourenço
9d78da163a docs(css-modules): describe the Periscope text style tokens 2026-11-13 18:56:44 -03:00
Vinícius Lourenço
b692e47207 refactor(styles): switch mono text to the code font family 2026-11-13 18:56:44 -03:00
Vinícius Lourenço
22ae891541 refactor(styles): replace removed design tokens with Periscope text styles 2026-11-13 18:56:44 -03:00
Vinícius Lourenço
b1c36e264a refactor(styles): drop font and text CSS that renders nothing 2026-11-13 18:56:44 -03:00
Vinícius Lourenço
cbc6df2fc3 refactor(typography): migrate the files main added since the first pass 2026-11-13 18:56:44 -03:00
Vinícius Lourenço
8d9e01f4b3 chore(lint): let font-face files set font descriptors 2026-11-13 18:56:43 -03:00
Vinícius Lourenço
d035887c34 perf(fonts): pin Inter at opsz 14 and split it by unicode-range 2026-11-13 18:56:43 -03:00
Vinícius Lourenço
1a3def98e1 fix(fonts): ship the OFL license with Geist Mono 2026-11-13 18:56:43 -03:00
Vinícius Lourenço
ab5028d8bc feat(fonts): self-host Inter 4.1 so ss01, zero and case work 2026-11-13 18:56:43 -03:00
Vinícius Lourenço
d21d8b467b feat(styles): align root text rendering with the Periscope type tokens page 2026-11-13 18:56:43 -03:00
Vinícius Lourenço
21e8453b23 fix(styles): stop font synthesis and set font smoothing once on the root 2026-11-13 18:56:42 -03:00
Vinícius Lourenço
cdec644bbb chore(lint): warn on font and text CSS outside Typography 2026-11-13 18:56:42 -03:00
Vinícius Lourenço
a486df69b3 refactor(typography): migrate to the reworked Typography 2026-11-13 18:56:42 -03:00
Vinícius Lourenço
5735b4f00f refactor(ui): port drawers to the reworked Drawer 2026-11-13 18:56:42 -03:00
Vinícius Lourenço
5cd6fa4dad refactor(ui): port dialogs and confirmations to the reworked Dialog and AlertDialog 2026-11-13 18:56:41 -03:00
Vinícius Lourenço
ef58a2afa0 refactor(ui): port breadcrumb, pagination, avatar, kbd and skeleton to the reworked components 2026-11-13 18:56:41 -03:00
Vinícius Lourenço
a796053a23 feat(popover): bring back the handle-based popover now that the library ships it 2026-11-13 18:56:41 -03:00
Vinícius Lourenço
7231cd1801 test(panel-editor): pick multi-select options through the combobox search row 2026-11-13 18:56:41 -03:00
Vinícius Lourenço
bc46e60147 test(tanstack-table): pick the page size through the combobox search row 2026-11-13 18:56:40 -03:00
Vinícius Lourenço
c7b6b5562e test: use the 0.3.0 input and combobox test ids in tests added on main 2026-11-13 18:56:40 -03:00
Vinícius Lourenço
ff95c1ab15 fix: drop styles and story helpers the old merges brought back 2026-11-13 18:56:40 -03:00
Vinícius Lourenço
db6a6cadf2 fix: adopt @signozhq/ui 0.3.0 in the alert rules list and saved views tests 2026-11-13 18:56:40 -03:00
Vinícius Lourenço
263e84bbb4 fix(popover): use the @signozhq/ui 0.3.0 popover API 2026-11-13 18:56:39 -03:00
Vinícius Lourenço
16c8398b21 fix: adopt reworked @signozhq/ui in code added on main 2026-11-13 18:56:39 -03:00
Manika Malhotra
6a114bd31e fix(popover): drop nativeButton from areas where handle is passed 2026-11-13 18:56:39 -03:00
Manika Malhotra
0662fee1af fix: tooltip mock and simplify variables popover 2026-11-13 18:56:39 -03:00
Manika Malhotra
39d836f98c feat(popover): integrate popover component 2026-11-13 18:56:39 -03:00
Manika Malhotra
4e48328c2d fix: cleanup conflicting css 2026-11-13 18:56:38 -03:00
Manika Malhotra
0b295340a7 fix: remove sonner shim and use toast 2026-11-13 18:56:38 -03:00
Manika Malhotra
b22f07cd9d chore: bump packages, integrate alert-strip and resizable 2026-11-13 18:56:28 -03:00
Manika Malhotra
f14de10d84 feat(frontend): adopt reworked @signozhq/ui components across remaining areas 2026-11-13 18:56:27 -03:00
Vinícius Lourenço
aab0d86f24 test(storybook): move the remaining logs, traces and pods plays into their play files 2026-11-13 18:56:17 -03:00
Vinícius Lourenço
27f7e4ef2d test(stories): add sso attribute and claim mapping expanded stories 2026-11-13 18:50:42 -03:00
Vinícius Lourenço
7acebd8e4e test(storybook): collect page banner and notice stories under foundations 2026-11-13 18:50:42 -03:00
Vinícius Lourenço
101dc51eeb test(storybook): add shell banner host stories for fixed footers, drawers and dialogs 2026-11-13 18:50:41 -03:00
Vinícius Lourenço
2de8bc7e66 test(storybook): add shell banner stories for every license, trial and no-auth state 2026-11-13 18:49:42 -03:00
Vinícius Lourenço
542e0bfdd9 test(stories): add query builder add-on stories with few and many values 2026-11-13 18:49:42 -03:00
Vinícius Lourenço
90fdef2991 test(storybook): cover the trace details thread tab 2026-11-13 18:49:42 -03:00
Vinícius Lourenço
0f75302284 test(storybook): cover workspace domain conflict and onboarding facing issues 2026-11-13 18:49:42 -03:00
Vinícius Lourenço
f7e17e55fb test(storybook): cover invite roles and move members plays to a play file 2026-11-13 18:49:41 -03:00
Vinícius Lourenço
d88f8e9b2f test(storybook): add changelog modal stories for cloud and self-hosted 2026-11-13 18:49:41 -03:00
Vinícius Lourenço
0c069c71e7 test(stories): cover the channel edit form, editable and read-only 2026-11-13 18:49:41 -03:00
Vinícius Lourenço
80232c03d6 test(stories): add K8s drawer depth and pods log details stories 2026-11-13 18:49:41 -03:00
Manika Malhotra
b56c2c6350 test(stories): cover the states the component migration changes 2026-11-13 18:49:41 -03:00
1339 changed files with 23357 additions and 16611 deletions

View File

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

View File

@@ -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",

View File

@@ -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`,
},
];

View File

@@ -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" />

View File

@@ -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');

View File

@@ -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: [
{

View File

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

View 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`.

View File

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

View File

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

View 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 },
],
},
],
});

View 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 }],
},
],
});

View 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,
};
},
};

View 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 \`--\`.`,
});
}
}
},
};
},
};

View File

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

File diff suppressed because it is too large Load Diff

View 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.

View File

@@ -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&apos;re currently on <span className="highlight-text">Trial plan</span>.
Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Typography>
</Modal>
);
}

View File

@@ -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;
}

View File

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

View File

@@ -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);
}

View File

@@ -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';

View File

@@ -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';

View File

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

View File

@@ -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" />

View File

@@ -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);
}

View 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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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]} &#8212; {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

View File

@@ -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"

View File

@@ -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;
}
}
}

View File

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

View File

@@ -48,15 +48,11 @@
}
.worker-count-text-container {
--typography-font-size: 2.5vw;
height: 100%;
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 +144,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 +211,6 @@
}
&__label {
font-family: 'Inter';
font-size: 14px;
color: var(--l2-foreground);
line-height: 20px;
@@ -225,7 +218,7 @@
}
&__value {
font-family: 'Geist Mono';
font-family: var(--code-base-400-font-family);
font-size: 24px;
color: var(--l1-foreground);
line-height: 32px;

View File

@@ -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
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 && (

View File

@@ -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 {

View File

@@ -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')}

View File

@@ -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,
};

View 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.',
};

View 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(),
);
};

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -5,9 +5,9 @@ import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { 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>
);
}

View File

@@ -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);

View File

@@ -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;

View File

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

View File

@@ -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;

View File

@@ -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)}

View File

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

View File

@@ -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;

View File

@@ -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%;
}

View File

@@ -1,56 +1,46 @@
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;
headerSuffix?: ReactNode;
loading?: boolean;
testId?: string;
}
function DetailsPanelDrawer({
isOpen,
onClose,
title,
children,
className,
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"
closePosition="start"
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,
headerSuffix: undefined,
loading: false,
testId: undefined,
};

View File

@@ -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}

View File

@@ -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>
);
}

View File

@@ -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;
@@ -193,8 +165,8 @@
align-items: center;
height: 32px;
overflow: hidden;
background: var(--l2-background);
border: 1px solid var(--l1-border);
background: var(--l3-background);
border: 1px solid var(--l3-border);
border-radius: 2px;
padding-right: var(--spacing-4);
}
@@ -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;
}
}

View File

@@ -3,9 +3,9 @@ import { useCopyToClipboard } from 'react-use';
import { LockKeyhole, RefreshCw, Trash2, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { 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}

View File

@@ -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>
);
}

View File

@@ -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,

View File

@@ -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;

View File

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

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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>
</>
);
}

View File

@@ -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}>

View File

@@ -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 {

View File

@@ -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}

View File

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

View File

@@ -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>
);

View File

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

View File

@@ -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;
}
}

View File

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

View File

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

View File

@@ -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', () => {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"

View File

@@ -4,14 +4,13 @@
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.
--typography-text-display: flex;
font-size: 12px;
font-style: normal;
font-weight: 500;
font-weight: 300;
line-height: 18px; /* 128.571% */
letter-spacing: 0.56px;
@@ -31,66 +30,45 @@
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-sm-height: 36px;
--input-padding: 0 12px;
--input-font-family: 'Space Mono', monospace;
--input-font-size: 12px;
--input-letter-spacing: normal;
--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 {

View File

@@ -1,7 +1,6 @@
import { useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { X } from '@signozhq/icons';
@@ -49,21 +48,26 @@ function InputWithLabel({
labelAfter,
})}
>
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
{!labelAfter && <div className="label">{label}</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
size="sm"
placeholder={placeholder}
type={type}
value={inputValue}
onChange={handleChange}
disabled={disabled}
disabledTooltip={undefined}
name={label.toLowerCase()}
testId={`input-${label}`}
/>
</div>
{labelAfter && <div className="label">{label}</div>}
{onClose && (
<Button
className="periscope-btn ghost close-btn"

View File

@@ -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 {

View File

@@ -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?.({

View File

@@ -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'));

View File

@@ -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'));

View File

@@ -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'));

View File

@@ -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'),
);
}

View File

@@ -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);
}

View File

@@ -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}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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}

View File

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

View File

@@ -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>
);
}

View File

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

View File

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

View File

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

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