Compare commits

..

43 Commits

Author SHA1 Message Date
Vinícius Lourenço
0c0bea2e7b test(e2e): update selectors for reworked @signozhq/ui components 2026-10-01 00:35:07 -03:00
Vinícius Lourenço
b6dec98f75 feat(events-exceptions-apis-queues): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:33 -03:00
Vinícius Lourenço
e779e9115e feat(events-traces): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:33 -03:00
Vinícius Lourenço
b8a302c70c feat(events-logs): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:33 -03:00
Vinícius Lourenço
a302957d1b feat(pulse-infra-monitoring): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:32 -03:00
Vinícius Lourenço
51babc6378 feat(pulse-alerts): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:32 -03:00
Vinícius Lourenço
9f23ad3334 feat(pulse-dashboards-list): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:32 -03:00
Vinícius Lourenço
1d81dce137 feat(pulse-dashboards): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:32 -03:00
Vinícius Lourenço
a8eafa8130 feat(metrics-apm): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:32 -03:00
Vinícius Lourenço
537eab1ec7 feat(llm-observability): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:32 -03:00
Vinícius Lourenço
9b07d061ce feat(ai-assistant): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:31 -03:00
Vinícius Lourenço
93484b7160 feat(integrations): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:31 -03:00
Vinícius Lourenço
4e6653008a feat(auth-onboarding): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:31 -03:00
Vinícius Lourenço
b10c3b7cd3 feat(settings): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:31 -03:00
Vinícius Lourenço
5c266e0076 feat(navigation): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:31 -03:00
Vinícius Lourenço
63b41db67a feat(query-builder): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:30 -03:00
Vinícius Lourenço
bb5bef3825 feat(shared-components): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:30 -03:00
Vinícius Lourenço
a5dd792c70 feat(members-roles-service-accounts): adopt reworked @signozhq/ui components
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:30 -03:00
Vinícius Lourenço
0cc1471ce2 feat(authz): build AuthZ components on reworked @signozhq/ui
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:30 -03:00
Vinícius Lourenço
098f8cce91 fix(api): treat requests cancelled before dispatch as cancellations
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:29 -03:00
Vinícius Lourenço
6fb5f09905 test(storybook): adopt reworked @signozhq/ui in fixtures and mocks
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:29 -03:00
Vinícius Lourenço
6d511729f8 chore(deps): bump @signozhq/ui to 0.2.0-rc.2 and design-tokens to 2.2.1
Co-authored-by: Manika Malhotra <manika@signoz.io>
2026-09-30 23:23:29 -03:00
Vinícius Lourenço
a26834de01 test(storybook): hold Noz click-opened state past the thread remount 2026-09-30 21:41:08 -03:00
Vinícius Lourenço
7446669d07 test(storybook): drop the fixed pause between filter keystrokes 2026-09-30 21:01:14 -03:00
Vinícius Lourenço
0ad99f97bd test(storybook): find dashboard settings and panel editor controls by test id 2026-09-30 21:01:10 -03:00
Vinícius Lourenço
a3706e0656 fix(tanstack-table): keep cells mounted when inline row callbacks change 2026-09-30 21:01:06 -03:00
Vinícius Lourenço
c3792e43e1 test(storybook): cover panel editor chart options, thresholds and links 2026-09-30 18:33:47 -03:00
Vinícius Lourenço
2ed614e7ac test(storybook): cover dashboard settings, variables and authz states 2026-09-30 18:33:47 -03:00
Vinícius Lourenço
2f18d2121c fix(storybook): attribute failed responses to the story that sent them 2026-09-30 18:33:47 -03:00
Vinícius Lourenço
3e002ba7e4 test(storybook): cover Noz voice recording states 2026-09-30 18:33:46 -03:00
Vinícius Lourenço
8b26e6a24b test(storybook): assert the in-flight create key button is disabled 2026-09-30 18:33:46 -03:00
Vinícius Lourenço
399a3d5dec test(storybook): cover ingestion create key validation, expiry and submit states 2026-09-30 18:33:46 -03:00
Vinícius Lourenço
65fd2d70d2 test(storybook): cover ingestion key tag adding and added states 2026-09-30 18:33:46 -03:00
Vinícius Lourenço
e4beea5667 test(storybook): click exceptions filter suggestions as they are found 2026-09-30 18:33:46 -03:00
Vinícius Lourenço
ea24ebe176 test(storybook): hold the pod filter stories at their opening height 2026-09-30 18:33:46 -03:00
Vinícius Lourenço
652b1d5f7b test(storybook): wait for the filter's scrollbars before focusing it 2026-09-30 18:33:45 -03:00
Vinícius Lourenço
b651994926 test(storybook): keep filter focused while scrollbars initialise 2026-09-30 18:33:45 -03:00
Vinícius Lourenço
51aee52d53 test(storybook): add query builder filter interaction stories 2026-09-30 18:33:45 -03:00
Vinícius Lourenço
499908b95c test(storybook): fix interaction stories against current main 2026-09-30 18:33:45 -03:00
Vinícius Lourenço
3b6f2fa234 test(storybook): add interaction stories for menus, drawers and selects 2026-09-30 18:33:45 -03:00
Vikrant Gupta
e374d03e54 revert(authz): restore gjson-based body extraction in resource middleware (#13024)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Reverts #13014 and #13015. The resource middleware goes back to
reading body-derived resource ids with `BodyJSONPath` / `BodyJSONArray`
over the raw body, and handlers decode their own request bodies again.
- Authz should not own request decoding; that ownership stays with the
handlers.

#### Additional Information

- Contributes to: https://github.com/SigNoz/keystone-pod/issues/37
2026-09-30 13:53:21 +00:00
Swapnil Nakade
d445b6c296 chore: bumping cloud integration agent version to v0.0.15 (#13023)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
Bumping the cloud integration agent's version to latest v0.0.15

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Contributes to https://github.com/SigNoz/keystone-pod/issues/101
2026-09-30 12:38:24 +00:00
Swapnil Nakade
fd8aaac300 feat: adding sync state in cloud integration (#12991)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
The agent only saw the current list of enabled regions, so it couldn’t
tell which regions had been removed. To find stacks to clean up, it
checked unrelated AWS regions, causing unnecessary calls and permission
errors. Sync state keeps track of regions sent to the agent and pending
removals until the agent acknowledges cleanup.

Please check
[comment](https://github.com/SigNoz/keystone-pod/issues/101#issuecomment-5832865898)
for approach

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Contributes to https://github.com/SigNoz/keystone-pod/issues/101

<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information
This PR should be merged before changes for cloud-integration repo.

<!--Please delete paragraphs that you did not use before submitting.-->
2026-09-30 11:39:46 +00:00
910 changed files with 12518 additions and 16177 deletions

View File

@@ -104,7 +104,6 @@ func runGenerateAuthz(_ context.Context) error {
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMetrics).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMeterMetrics).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceQuickFilter).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceSavedView).String(): true,
}
allowedTypes := map[string]bool{}

View File

@@ -1771,12 +1771,15 @@ components:
additionalProperties: {}
nullable: true
type: object
syncState:
$ref: '#/components/schemas/CloudintegrationtypesSyncState'
timestampMillis:
format: int64
type: integer
required:
- timestampMillis
- data
- syncState
type: object
CloudintegrationtypesAzureAccountConfig:
properties:
@@ -2021,6 +2024,8 @@ components:
format: date-time
nullable: true
type: string
syncState:
$ref: '#/components/schemas/CloudintegrationtypesSyncState'
required:
- account_id
- cloud_account_id
@@ -2030,6 +2035,7 @@ components:
- providerAccountId
- integrationConfig
- removedAt
- syncState
type: object
CloudintegrationtypesGettableServicesMetadata:
properties:
@@ -2129,6 +2135,9 @@ components:
type: object
providerAccountId:
type: string
syncedVersion:
nullable: true
type: integer
required:
- data
type: object
@@ -2141,6 +2150,18 @@ components:
gcp:
$ref: '#/components/schemas/CloudintegrationtypesGCPIntegrationConfig'
type: object
CloudintegrationtypesRegionState:
enum:
- enabled
- disabled
type: string
CloudintegrationtypesRegionSyncState:
properties:
state:
$ref: '#/components/schemas/CloudintegrationtypesRegionState'
required:
- state
type: object
CloudintegrationtypesService:
properties:
assets:
@@ -2282,6 +2303,23 @@ components:
metrics:
type: boolean
type: object
CloudintegrationtypesSyncState:
nullable: true
properties:
inSync:
type: boolean
regions:
additionalProperties:
$ref: '#/components/schemas/CloudintegrationtypesRegionSyncState'
type: object
version:
format: int64
type: integer
required:
- version
- inSync
- regions
type: object
CloudintegrationtypesUpdatableAccount:
properties:
config:

View File

@@ -118,7 +118,7 @@ router.Handle("/api/v1/service_accounts", handler.New(
The pieces:
- **`CheckResources(handlerFn, roles...)`** — the resource-aware authorization wrapper from [pkg/http/middleware/authz.go](/pkg/http/middleware/authz.go). The role list is the community-edition fallback: which managed roles may call this route when per-resource checks are unavailable.
- **`ResourceDef`** — declares the resource, verb, audit category, how to extract the instance ID, and how to turn that ID into selectors. ID extractors live in [pkg/types/coretypes/extractor.go](/pkg/types/coretypes/extractor.go): `PathParam("id")`, `BodyField(func(req *T) string)` / `BodyFields(func(req *T) []string)` reading the request body the resource middleware decoded into the route's `OpenAPIDef.Request` type `T`, and `ResponseJSONPath("data.id")` for IDs only known after the handler runs (e.g. `create`). A handler on such a route reads the same decoded value with `coretypes.BodyFromContext[T](r.Context())`.
- **`ResourceDef`** — declares the resource, verb, audit category, how to extract the instance ID, and how to turn that ID into selectors. ID extractors live in [pkg/types/coretypes/extractor.go](/pkg/types/coretypes/extractor.go): `PathParam("id")`, `BodyJSONPath("data.id")`, `BodyJSONArray("ids")`, and `ResponseJSONPath("data.id")` for IDs only known after the handler runs (e.g. `create`).
- **`SecuritySchemes`** — advertises the required scope (`resource.Scope(verb)`, e.g. `serviceaccount:create`) in the OpenAPI spec.
For routes that link two resources, use `AttachDetachSiblingResourceDef` (both sides are authz-checked, e.g. attaching a role to a service account requires `attach` on **both** the service account and the role). For parent-child routes (e.g. creating an API key under a service account), both sides are checked too, but with different verbs: declare a `BasicResourceDef` checking the child with `create`/`delete`, alongside an `AttachDetachParentChildResourceDef` checking the parent with `attach`/`detach` (within that def the child is only recorded for audit) — see the `/api/v1/service_accounts/{id}/keys` route in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).

View File

@@ -183,32 +183,52 @@ func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provi
return nil, errors.New(errors.TypeAlreadyExists, cloudintegrationtypes.ErrCodeCloudIntegrationAlreadyConnected, errMessage)
}
account, err := module.store.GetAccountByID(ctx, orgID, req.CloudIntegrationID, provider)
storableAccount, err := module.store.GetAccountByID(ctx, orgID, req.CloudIntegrationID, provider)
if err != nil {
return nil, err
}
account, err := cloudintegrationtypes.NewAccountFromStorable(storableAccount)
if err != nil {
return nil, err
}
syncState := account.NextSyncState(req.SyncedVersion)
// If account has been removed (disconnected), return a minimal response with empty integration config.
// The agent uses this response to clean up resources
if account.RemovedAt != nil {
// Heartbeat stays frozen after removal, only the sync state is updated.
if account.AgentReport != nil && syncState != nil {
account.UpdateSyncState(syncState)
storableAccount, err = cloudintegrationtypes.NewStorableCloudIntegration(account)
if err != nil {
return nil, err
}
err = module.store.UpdateAgentReport(ctx, storableAccount)
if err != nil {
return nil, err
}
}
return cloudintegrationtypes.NewAgentCheckInResponse(
req.ProviderAccountID,
account.ID.StringValue(),
new(cloudintegrationtypes.ProviderIntegrationConfig),
account.RemovedAt,
syncState,
), nil
}
// update account with cloud provider account id and agent report (heartbeat)
account.Update(&req.ProviderAccountID, cloudintegrationtypes.NewAgentReport(req.Data))
account.UpdateAgentReport(&req.ProviderAccountID, cloudintegrationtypes.NewAgentReport(req.Data, syncState))
err = module.store.UpdateAccount(ctx, account)
storableAccount, err = cloudintegrationtypes.NewStorableCloudIntegration(account)
if err != nil {
return nil, err
}
// Get account as domain object for config access (enabled regions, etc.)
domainAccount, err := cloudintegrationtypes.NewAccountFromStorable(account)
err = module.store.UpdateAgentReport(ctx, storableAccount)
if err != nil {
return nil, err
}
@@ -223,8 +243,7 @@ func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provi
return nil, err
}
// Delegate integration config building entirely to the provider module
integrationConfig, err := cloudProvider.BuildIntegrationConfig(ctx, domainAccount, storedServices)
integrationConfig, err := cloudProvider.BuildIntegrationConfig(ctx, account, storedServices)
if err != nil {
return nil, err
}
@@ -234,6 +253,7 @@ func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provi
account.ID.StringValue(),
integrationConfig,
account.RemovedAt,
syncState,
), nil
}

View File

@@ -70,3 +70,49 @@
Object.defineProperty(window, 'Date', { value: FrozenDate, writable: true });
})();
</script>
<script>
// Storybook has no microphone. Chromium's recognizer asks for one and for
// Google's speech service, and the `not-allowed` or `network` it reports makes
// the assistant hide its mic for the rest of the tab. This one starts
// listening at once and hears only what a story passes to `hear`.
(() => {
class StorySpeechRecognition extends EventTarget {
static listening = null;
lang = 'en-US';
continuous = false;
interimResults = false;
onstart = null;
onresult = null;
onerror = null;
onend = null;
start() {
StorySpeechRecognition.listening = this;
setTimeout(() => this.onstart?.());
}
stop() {
if (StorySpeechRecognition.listening === this) {
StorySpeechRecognition.listening = null;
}
setTimeout(() => this.onend?.());
}
abort() {
this.stop();
}
/** An interim result: the words so far, still listening. */
hear(transcript) {
const result = Object.assign([{ transcript, confidence: 1 }], {
isFinal: false,
});
this.onresult?.({ resultIndex: 0, results: [result] });
}
}
window.SpeechRecognition = StorySpeechRecognition;
window.webkitSpeechRecognition = StorySpeechRecognition;
})();
</script>

View File

@@ -122,7 +122,21 @@ const preview: Preview = {
['List', 'New', 'Edit'],
],
'Dashboards',
['List', 'Detail', 'Panel Editor', 'Public'],
[
'List',
'Detail',
[
'Overview',
'Settings',
'Variables',
'New Variable',
'Edit Variable',
'Authz',
],
'Panel Editor',
['Overview', 'Chart Options', 'Thresholds and Links'],
'Public',
],
'Services',
['List', 'Detail', 'Top Level Operations', 'Service Map'],
'Logs',

View File

@@ -23,6 +23,13 @@ const IGNORED_MESSAGES = [
// (YouTube embeds, the docs pane) so they hit the real network instead of
// an unanswered msw request; the block is the point, not a bug.
/violates the following Content Security Policy directive/,
// The filter editor's ANTLR parser reports every syntax error through
// `console.error` (`line 1:14 missing ...`), so each partial expression
// typed into it logs one; the editor shows the same errors on screen.
/^line \d+:\d+ /,
// Chromium's echo of a 4xx/5xx response, which the `[response]` entry below
// already reports with its URL and the time its request left.
/^Failed to load resource: the server responded with a status of /,
];
interface CapturedMessage {
@@ -86,7 +93,9 @@ const config: TestRunnerConfig = {
page.on('response', (response) => {
if (response.status() >= 400) {
messages.push({
at: Date.now(),
// The browser's clock, when the request left: a busy runner receives
// the outgoing story's responses after the next story has started.
at: response.request().timing().startTime,
text: `[response] ${response.status()} ${response.url()}`,
});
}

View File

@@ -25,6 +25,7 @@ You are operating within a constrained context window and strict system prompts.
- Never create barrel files.
- When writing new css, prefer CSS Modules
- Use ./docs/css-modules-guide.md as reference on how to write good CSS Modules.
- Before styling a `@signozhq/ui` component, check its props in `node_modules/@signozhq/ui/dist/<component>/*.d.ts` for `className`/`style` support; some omit them. Avoid overriding the component's CSS vars unless explicitly needed.
- When writing code that could need authorization checks, read ./src/lib/authz/README.md
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:

View File

@@ -41,6 +41,20 @@ if (!HTMLElement.prototype.releasePointerCapture) {
HTMLElement.prototype.releasePointerCapture = function (): void {};
}
// jsdom has no PointerEvent; Base UI Switch constructs one on click.
if (typeof window.PointerEvent === 'undefined') {
class PointerEventMock extends MouseEvent {
pointerId: number;
pointerType: string;
constructor(type: string, init: PointerEventInit = {}) {
super(type, init);
this.pointerId = init.pointerId ?? 0;
this.pointerType = init.pointerType ?? '';
}
}
(window as any).PointerEvent = PointerEventMock;
}
if (typeof window.IntersectionObserver === 'undefined') {
class IntersectionObserverMock {
observe(): void {}

View File

@@ -49,13 +49,12 @@
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.6.15",
"@lezer/highlight": "1.2.1",
"@monaco-editor/react": "^4.7.0",
"@sentry/react": "10.57.0",
"@sentry/vite-plugin": "5.3.0",
"@signozhq/design-tokens": "2.1.6",
"@signozhq/design-tokens": "2.2.1",
"@signozhq/icons": "0.4.0",
"@signozhq/ui": "0.1.0",
"@signozhq/ui": "0.2.0-rc.2",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
"@uiw/codemirror-theme-copilot": "4.23.11",

441
frontend/pnpm-lock.yaml generated
View File

@@ -62,9 +62,6 @@ importers:
'@grafana/data':
specifier: ^11.6.15
version: 11.6.15(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@lezer/highlight':
specifier: 1.2.1
version: 1.2.1
'@monaco-editor/react':
specifier: ^4.7.0
version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -75,14 +72,14 @@ importers:
specifier: 5.3.0
version: 5.3.0
'@signozhq/design-tokens':
specifier: 2.1.6
version: 2.1.6
specifier: 2.2.1
version: 2.2.1
'@signozhq/icons':
specifier: 0.4.0
version: 0.4.0
'@signozhq/ui':
specifier: 0.1.0
version: 0.1.0(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
specifier: 0.2.0-rc.2
version: 0.2.0-rc.2(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
'@tanstack/react-table':
specifier: 8.21.3
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -1287,6 +1284,33 @@ packages:
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
'@base-ui/react@1.7.0':
resolution: {integrity: sha512-j+8QjX44C32jrXD/qyEAGpFr70FRpGL2CY61mQd9nBPWN737CK0xxD1ceJ055rW4RtdvFDT1e7otzdlfxvsYug==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@date-fns/tz': ^1.2.0
'@types/react': ^17 || ^18 || ^19
date-fns: ^4.0.0
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@date-fns/tz':
optional: true
'@types/react':
optional: true
date-fns:
optional: true
'@base-ui/utils@0.3.2':
resolution: {integrity: sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
@@ -1729,20 +1753,20 @@ packages:
resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@floating-ui/core@1.5.2':
resolution: {integrity: sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==}
'@floating-ui/core@1.8.0':
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
'@floating-ui/dom@1.5.3':
resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==}
'@floating-ui/dom@1.8.0':
resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
'@floating-ui/react-dom@2.0.4':
resolution: {integrity: sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==}
'@floating-ui/react-dom@2.1.9':
resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.1.6':
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@gerrit0/mini-shiki@3.23.0':
resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==}
@@ -2923,19 +2947,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-checkbox@1.3.11':
resolution: {integrity: sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.15':
resolution: {integrity: sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==}
peerDependencies:
@@ -2949,15 +2960,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-compose-refs@1.1.5':
resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==}
peerDependencies:
@@ -3011,19 +3013,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-dropdown-menu@2.1.24':
resolution: {integrity: sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-focus-guards@1.1.6':
resolution: {integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==}
peerDependencies:
@@ -3051,15 +3040,6 @@ packages:
peerDependencies:
react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-id@1.1.4':
resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==}
peerDependencies:
@@ -3069,19 +3049,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-menu@2.1.24':
resolution: {integrity: sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-popover@1.1.23':
resolution: {integrity: sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==}
peerDependencies:
@@ -3147,19 +3114,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.4':
resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-progress@1.1.16':
resolution: {integrity: sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==}
peerDependencies:
@@ -3225,15 +3179,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.2.4':
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-slot@1.3.3':
resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==}
peerDependencies:
@@ -3243,19 +3188,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-switch@1.3.7':
resolution: {integrity: sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-tabs@1.1.21':
resolution: {integrity: sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==}
peerDependencies:
@@ -3269,45 +3201,6 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-toggle-group@1.1.19':
resolution: {integrity: sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-toggle@1.1.18':
resolution: {integrity: sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-tooltip@1.2.16':
resolution: {integrity: sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.4':
resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==}
peerDependencies:
@@ -3344,15 +3237,6 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.4':
resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==}
peerDependencies:
@@ -3724,14 +3608,14 @@ packages:
'@sideway/pinpoint@2.0.0':
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
'@signozhq/design-tokens@2.1.6':
resolution: {integrity: sha512-n0cH4TRS1Xj7BR38HGiwIBDekIOAnDgl2JyYmAyUvws0L1nlE0txSHO5NiOBJ22Dm8uMva0v5/rOHxpKsGQbsw==}
'@signozhq/design-tokens@2.2.1':
resolution: {integrity: sha512-ulf6lhWUP6qxqsyemQySUMfOIKRvefvZ5rBRv+wlhOpPfYPk3uatPmPkKEhAjISpX8f9g1Gg8wgRZxUCysf20Q==}
'@signozhq/icons@0.4.0':
resolution: {integrity: sha512-QctNvNPlQlsefFdIVmSy/P/7a1IcQ3IryKP0KPMqtoqew3q89FGUmFa5IfEJ7Z3CHhtG1zaXwBKqP72QkLyA3g==}
'@signozhq/ui@0.1.0':
resolution: {integrity: sha512-FjqmQhz+WtpGl29gPGZey5YwNe4p++TCyX4jaGUhAgvlBGRmxmAKW0lFRrSOYTPbgFL5UaDCW0wR62aiGHZ+FA==}
'@signozhq/ui@0.2.0-rc.2':
resolution: {integrity: sha512-JYZmJJYmaBFfiArPe4cE5epQ6afv8quHQ7YZqq6/+5ugdUmEBWdkoi3kJh9577DzAGiMocKCic15wI/eiHf1MQ==}
peerDependencies:
'@signozhq/icons': 0.3.0
react: ^18.2.0
@@ -8523,6 +8407,11 @@ packages:
react: ^16.8.5 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0
react-compiler-runtime@1.0.0:
resolution: {integrity: sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental
react-day-picker@9.9.0:
resolution: {integrity: sha512-NtkJbuX6cl/VaGNb3sVVhmMA6LSMnL5G3xNL+61IyoZj0mUZFWTg4hmj7PHjIQ8MXN9dHWhUHFoJWG6y60DKSg==}
engines: {node: '>=18'}
@@ -8910,6 +8799,9 @@ packages:
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
reselect@5.3.0:
resolution: {integrity: sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==}
reserved-words@0.1.2:
resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==}
@@ -11157,6 +11049,31 @@ snapshots:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@base-ui/react@1.7.0(@date-fns/tz@1.4.1)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.29.2
'@base-ui/utils': 0.3.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@floating-ui/react-dom': 2.1.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@floating-ui/utils': 0.2.12
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
use-sync-external-store: 1.6.0(react@18.2.0)
optionalDependencies:
'@date-fns/tz': 1.4.1
'@types/react': 18.0.26
date-fns: 4.1.0
'@base-ui/utils@0.3.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.29.2
'@floating-ui/utils': 0.2.12
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
reselect: 5.3.0
use-sync-external-store: 1.6.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@bcoe/v8-coverage@0.2.3': {}
'@braintree/sanitize-url@7.0.1': {}
@@ -11601,22 +11518,22 @@ snapshots:
'@eslint/core': 1.2.1
levn: 0.4.1
'@floating-ui/core@1.5.2':
'@floating-ui/core@1.8.0':
dependencies:
'@floating-ui/utils': 0.1.6
'@floating-ui/utils': 0.2.12
'@floating-ui/dom@1.5.3':
'@floating-ui/dom@1.8.0':
dependencies:
'@floating-ui/core': 1.5.2
'@floating-ui/utils': 0.1.6
'@floating-ui/core': 1.8.0
'@floating-ui/utils': 0.2.12
'@floating-ui/react-dom@2.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
'@floating-ui/react-dom@2.1.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@floating-ui/dom': 1.5.3
'@floating-ui/dom': 1.8.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@floating-ui/utils@0.1.6': {}
'@floating-ui/utils@0.2.12': {}
'@gerrit0/mini-shiki@3.23.0':
dependencies:
@@ -12766,21 +12683,6 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-checkbox@1.3.11(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-size': 1.1.4(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-collection@1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
@@ -12793,12 +12695,6 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-compose-refs@1.1.2(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-compose-refs@1.1.5(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -12853,21 +12749,6 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-dropdown-menu@2.1.24(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-menu': 2.1.24(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-focus-guards@1.1.6(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -12889,13 +12770,6 @@ snapshots:
dependencies:
react: 18.2.0
'@radix-ui/react-id@1.1.1(@types/react@18.0.26)(react@18.2.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-id@1.1.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.0.26)(react@18.2.0)
@@ -12903,32 +12777,6 @@ snapshots:
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-menu@2.1.24(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-collection': 1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-focus-guards': 1.1.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-popper': 1.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-portal': 1.1.17(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.1.4(@types/react@18.0.26)(react@18.2.0)
aria-hidden: 1.2.6
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.7.2(@types/react@18.0.26)(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-popover@1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
@@ -12954,7 +12802,7 @@ snapshots:
'@radix-ui/react-popper@1.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@floating-ui/react-dom': 2.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@floating-ui/react-dom': 2.1.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-arrow': 1.1.15(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
@@ -12998,15 +12846,6 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-primitive@2.1.4(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-slot': 1.2.4(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-progress@1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
@@ -13102,13 +12941,6 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-slot@1.2.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-slot@1.3.3(@types/react@18.0.26)(react@18.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
@@ -13116,20 +12948,6 @@ snapshots:
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-switch@1.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-size': 1.1.4(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-tabs@1.1.21(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
@@ -13146,53 +12964,6 @@ snapshots:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-toggle-group@1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-direction': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-toggle': 1.1.18(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-toggle@1.1.18(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-tooltip@1.2.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.7
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-context': 1.2.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-popper': 1.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-portal': 1.1.17(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-presence': 1.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.2.6(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
optionalDependencies:
'@types/react': 18.0.26
'@types/react-dom': 18.0.10
'@radix-ui/react-use-callback-ref@1.1.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -13221,12 +12992,6 @@ snapshots:
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
optionalDependencies:
'@types/react': 18.0.26
'@radix-ui/react-use-layout-effect@1.1.4(@types/react@18.0.26)(react@18.2.0)':
dependencies:
react: 18.2.0
@@ -13275,14 +13040,14 @@ snapshots:
'@rc-component/context@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.28.2
'@babel/runtime': 7.29.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
'@rc-component/mini-decimal@1.0.1':
dependencies:
'@babel/runtime': 7.28.2
'@babel/runtime': 7.29.2
'@rc-component/mutate-observer@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
@@ -13294,7 +13059,7 @@ snapshots:
'@rc-component/portal@1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
dependencies:
'@babel/runtime': 7.28.2
'@babel/runtime': 7.29.2
classnames: 2.3.2
rc-util: 5.38.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
@@ -13562,16 +13327,15 @@ snapshots:
'@sideway/pinpoint@2.0.0': {}
'@signozhq/design-tokens@2.1.6': {}
'@signozhq/design-tokens@2.2.1': {}
'@signozhq/icons@0.4.0': {}
'@signozhq/ui@0.1.0(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)':
'@signozhq/ui@0.2.0-rc.2(@date-fns/tz@1.4.1)(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)':
dependencies:
'@base-ui/react': 1.7.0(@date-fns/tz@1.4.1)(@types/react@18.0.26)(date-fns@4.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@chenglou/pretext': 0.0.8
'@radix-ui/react-checkbox': 1.3.11(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-dialog': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-dropdown-menu': 2.1.24(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-icons': 1.3.2(react@18.2.0)
'@radix-ui/react-popover': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-progress': 1.1.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -13579,11 +13343,7 @@ snapshots:
'@radix-ui/react-select': 2.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slider': 1.4.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-slot': 1.3.3(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-switch': 1.3.7(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-tabs': 1.1.21(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-toggle': 1.1.18(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-tooltip': 1.2.16(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@signozhq/icons': 0.4.0
'@tanstack/react-table': 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@tanstack/react-virtual': 3.14.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -13597,16 +13357,19 @@ snapshots:
next-themes: 0.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nuqs: 2.9.6(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.6(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-compiler-runtime: 1.0.0(react@18.2.0)
react-day-picker: 9.9.0(react@18.2.0)
react-dom: 18.2.0(react@18.2.0)
react-resizable-panels: 4.12.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
sonner: 2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
transitivePeerDependencies:
- '@date-fns/tz'
- '@emotion/is-prop-valid'
- '@remix-run/react'
- '@tanstack/react-router'
- '@types/react'
- '@types/react-dom'
- date-fns
- next
- react-router
- react-router-dom
@@ -15132,10 +14895,10 @@ snapshots:
cmdk@1.1.1(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.1.5(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-dialog': 1.1.23(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.1.1(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@radix-ui/react-id': 1.1.4(@types/react@18.0.26)(react@18.2.0)
'@radix-ui/react-primitive': 2.1.10(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
@@ -19280,6 +19043,10 @@ snapshots:
transitivePeerDependencies:
- react-native
react-compiler-runtime@1.0.0(react@18.2.0):
dependencies:
react: 18.2.0
react-day-picker@9.9.0(react@18.2.0):
dependencies:
'@date-fns/tz': 1.4.1
@@ -19765,6 +19532,8 @@ snapshots:
requires-port@1.0.0: {}
reselect@5.3.0: {}
reserved-words@0.1.2: {}
resize-observer-polyfill@1.5.1: {}
@@ -19870,7 +19639,7 @@ snapshots:
rtl-css-js@1.16.1:
dependencies:
'@babel/runtime': 7.28.2
'@babel/runtime': 7.29.2
run-applescript@7.1.0: {}
@@ -20682,7 +20451,7 @@ snapshots:
unload@2.2.0:
dependencies:
'@babel/runtime': 7.28.2
'@babel/runtime': 7.29.2
detect-node: 2.1.0
unplugin@2.3.11:

View File

@@ -41,17 +41,16 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
message: data?.error,
};
}
// Avoid logging error when the request was just cancelled for whatever reason
if (isCancel(error)) {
return {
statusCode: 500,
payload: null,
error: 'Something went wrong',
message: null,
};
}
if (request) {
// Avoid logging error when the request was just cancelled for whatever reason
if (isCancel(error)) {
return {
statusCode: 500,
payload: null,
error: 'Something went wrong',
message: null,
};
}
// client never received a response, or request never left
console.error('client never received a response, or request never left');

View File

@@ -0,0 +1,22 @@
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, CanceledError } from 'axios';
describe('ErrorResponseHandler', () => {
// axios aborts a request whose signal fired before it was sent without ever
// creating one, so the error carries neither `response` nor `request`.
it('treats a request cancelled before it was sent as a cancellation', () => {
const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => undefined);
expect(ErrorResponseHandler(new CanceledError() as AxiosError)).toEqual({
statusCode: 500,
payload: null,
error: 'Something went wrong',
message: null,
});
expect(consoleError).not.toHaveBeenCalled();
consoleError.mockRestore();
});
});

View File

@@ -3396,6 +3396,37 @@ export interface CloudintegrationtypesAWSServiceConfigDTO {
metrics?: CloudintegrationtypesAWSServiceMetricsConfigDTO;
}
export enum CloudintegrationtypesRegionStateDTO {
enabled = 'enabled',
disabled = 'disabled',
}
export interface CloudintegrationtypesRegionSyncStateDTO {
state: CloudintegrationtypesRegionStateDTO;
}
export type CloudintegrationtypesSyncStateDTORegions = {
[key: string]: CloudintegrationtypesRegionSyncStateDTO;
};
/**
* @nullable
*/
export type CloudintegrationtypesSyncStateDTO = {
/**
* @type boolean
*/
inSync: boolean;
/**
* @type object
*/
regions: CloudintegrationtypesSyncStateDTORegions;
/**
* @type integer
* @format int64
*/
version: number;
} | null;
export type CloudintegrationtypesAgentReportDTODataAnyOf = {
[key: string]: unknown;
};
@@ -3414,6 +3445,7 @@ export type CloudintegrationtypesAgentReportDTO = {
* @type object,null
*/
data: CloudintegrationtypesAgentReportDTOData;
syncState: CloudintegrationtypesSyncStateDTO | null;
/**
* @type integer
* @format int64
@@ -3842,6 +3874,7 @@ export interface CloudintegrationtypesGettableAgentCheckInDTO {
* @format date-time
*/
removedAt: string | null;
syncState: CloudintegrationtypesSyncStateDTO | null;
}
export interface CloudintegrationtypesServiceMetadataDTO {
@@ -3912,6 +3945,10 @@ export interface CloudintegrationtypesPostableAgentCheckInDTO {
* @type string
*/
providerAccountId?: string;
/**
* @type integer,null
*/
syncedVersion?: number | null;
}
export interface CloudintegrationtypesStorableIntegrationDashboardDTO {

View File

@@ -8,6 +8,10 @@
:global(.ant-breadcrumb-separator) {
color: var(--muted-foreground);
}
[data-slot='button'] {
padding: 0px;
}
}
.divider {

View File

@@ -1,9 +1,5 @@
.item {
--button-padding: 0;
--button-font-size: var(--periscope-font-size-base);
}
.itemLast {
color: var(--muted-foreground);
font-size: var(--periscope-font-size-base);
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

@@ -26,9 +26,9 @@ function BreadcrumbItem({
return (
<Button
variant="ghost"
size="md"
variant="link"
color="secondary"
className={styles.item}
onClick={(e: React.MouseEvent): void => {
if (!('route' in props) || !props.route) {
return;

View File

@@ -34,21 +34,23 @@ function ErrorEmptyState({
</div>
<div className={styles.actions}>
<Button
size="md"
variant="solid"
color="secondary"
prefix={<LifeBuoy size={14} />}
onClick={onContactSupport}
data-testid="error-contact-support-button"
testId="error-contact-support-button"
>
Contact Support
</Button>
{onRefresh && (
<Button
size="md"
variant="outlined"
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
data-testid="error-refresh-button"
testId="error-refresh-button"
>
Refresh
</Button>

View File

@@ -1,20 +1,32 @@
.labelColumn {
// Badges fill their wrapper; the wrapper, as the flex item, owns the cap.
--badge-max-width: 100%;
position: relative;
display: flex;
gap: 4px;
align-items: center;
overflow: hidden;
max-width: 100%;
width: 100%;
> * {
flex-shrink: 0;
max-width: 180px;
}
}
.labelBadge {
cursor: default;
font-size: 12px;
// Kept mounted so its width can still be measured.
.overflowed {
position: absolute;
visibility: hidden;
pointer-events: none;
}
--badge-display: inline;
max-width: 180px;
text-overflow: ellipsis;
// The only visible label truncates instead of pushing `+N` out of view.
.labelColumn > .shrinkable {
flex-shrink: 1;
min-width: 0;
}
.overflowTrigger {
@@ -22,11 +34,6 @@
cursor: pointer;
}
.overflowBadge {
cursor: pointer;
font-size: 12px;
}
.labelPopover {
display: flex;
flex-direction: column;

View File

@@ -1,13 +1,14 @@
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { act, render, screen } from '@testing-library/react';
import LabelColumn from './LabelColumn';
let resizeCallback: ResizeObserverCallback | null = null;
const BADGE_WIDTH = 60;
let resizeCallbacks: ResizeObserverCallback[] = [];
class MockResizeObserver {
constructor(callback: ResizeObserverCallback) {
resizeCallback = callback;
resizeCallbacks.push(callback);
}
observe = jest.fn();
@@ -15,15 +16,23 @@ class MockResizeObserver {
disconnect = jest.fn();
}
function triggerResize(width: number): void {
if (resizeCallback) {
act(() => {
resizeCallback?.(
[{ contentRect: { width } } as ResizeObserverEntry],
{} as ResizeObserver,
);
// jsdom has no layout: every badge measures BADGE_WIDTH, the column `containerWidth`.
function mockLayout(containerWidth: number): void {
jest
.spyOn(HTMLElement.prototype, 'clientWidth', 'get')
.mockImplementation(function getClientWidth(this: HTMLElement) {
return this.dataset.testid === 'label-column' ? containerWidth : 0;
});
}
jest
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockReturnValue({ width: BADGE_WIDTH } as DOMRect);
}
function triggerResize(containerWidth: number): void {
mockLayout(containerWidth);
act(() => {
resizeCallbacks.forEach((callback) => callback([], {} as ResizeObserver));
});
}
beforeAll(() => {
@@ -31,13 +40,14 @@ beforeAll(() => {
});
afterEach(() => {
resizeCallback = null;
resizeCallbacks = [];
jest.restoreAllMocks();
});
function renderWithProviders(
ui: React.ReactElement,
): ReturnType<typeof render> {
return render(<TooltipProvider>{ui}</TooltipProvider>);
return render(ui);
}
describe('LabelColumn', () => {
@@ -54,13 +64,10 @@ describe('LabelColumn', () => {
it('should truncate labels and show +N badge when container is narrow', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
// 3 badges (180px) + overflow reserve (40px)
mockLayout(220);
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate narrow container that fits ~3 badges
// Badge widths: env=37, service=65, region=58, team=44, owner=51, version=65
// 220px available = 3 badges (160px) + gaps (8px) + overflow (44px)
triggerResize(220);
// First 3 visible
expect(screen.getByTestId('label-tag-env')).toBeInTheDocument();
expect(screen.getByTestId('label-tag-service')).toBeInTheDocument();
@@ -93,19 +100,37 @@ describe('LabelColumn', () => {
expect(screen.getByTestId('label-tag-service')).toHaveTextContent('service');
});
it('should show overflow badge with remaining count when container is narrow', () => {
it('should update the overflow count when the container resizes', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
mockLayout(220);
renderWithProviders(<LabelColumn labels={labels} />);
expect(screen.getByTestId('label-overflow-badge')).toHaveTextContent('+3');
// 2 badges (120px) + overflow reserve (40px)
triggerResize(160);
expect(screen.getByTestId('label-overflow-badge')).toHaveTextContent('+4');
triggerResize(1000);
expect(screen.queryByTestId('label-overflow-badge')).not.toBeInTheDocument();
});
it('should skip the overflow reserve when every label fits', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
mockLayout(labels.length * BADGE_WIDTH);
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate narrow container to trigger overflow (shows 3 labels)
// 220px fits first 3 badges before overflow
triggerResize(220);
expect(screen.queryByTestId('label-overflow-badge')).not.toBeInTheDocument();
});
// Overflow badge shows +3 (remaining labels)
const overflowBadge = screen.getByTestId('label-overflow-badge');
expect(overflowBadge).toBeInTheDocument();
expect(overflowBadge).toHaveTextContent('+3');
it('should keep one label visible when even that one does not fit', () => {
const labels = ['env', 'service'];
mockLayout(50);
renderWithProviders(<LabelColumn labels={labels} />);
expect(screen.getByTestId('label-overflow-badge')).toHaveTextContent('+1');
});
it('should render empty when no labels provided', () => {
@@ -126,11 +151,9 @@ describe('LabelColumn', () => {
it('should show all labels when container is wide enough', () => {
const labels = ['env', 'service', 'region', 'team', 'owner', 'version'];
mockLayout(1000);
renderWithProviders(<LabelColumn labels={labels} />);
// Simulate wide container
triggerResize(1000);
// All labels visible
labels.forEach((label) => {
expect(screen.getByTestId(`label-tag-${label}`)).toBeInTheDocument();

View File

@@ -1,35 +1,19 @@
import { Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/sonner';
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Tooltip } from '@signozhq/ui/tooltip';
import cx from 'classnames';
import { useLayoutEffect, useRef, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import LabelTag from './LabelTag';
import styles from './LabelColumn.module.scss';
import { BADGE_GAP, estimateBadgeWidth, OVERFLOW_BADGE_WIDTH } from './utils';
import { getVisibleCount } from './utils';
export interface LabelColumnProps {
labels: string[];
color?:
| 'primary'
| 'secondary'
| 'success'
| 'error'
| 'warning'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua'
| 'vanilla';
color?: BadgeColorType;
value?: { [key: string]: string };
}
@@ -39,60 +23,37 @@ function LabelColumn({
color = 'primary',
}: LabelColumnProps): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
const [maxVisibleCount, setMaxVisibleCount] = useState(labels.length);
const [visibleCount, setVisibleCount] = useState(labels.length);
const [, copyToClipboard] = useCopyToClipboard();
const calculateMaxVisible = useCallback(
(width: number): number => {
if (width <= 0) {
return 1;
}
const availableWidth = width - OVERFLOW_BADGE_WIDTH - BADGE_GAP;
let usedWidth = 0;
let count = 0;
for (const label of labels) {
const badgeWidth = estimateBadgeWidth(label, value?.[label]) + BADGE_GAP;
if (usedWidth + badgeWidth > availableWidth && count > 0) {
break;
}
usedWidth += badgeWidth;
count++;
}
return Math.max(1, count);
},
[labels, value],
);
useEffect(() => {
useLayoutEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0) {
setMaxVisibleCount(calculateMaxVisible(entry.contentRect.width));
const measure = (): void => {
const available = container.clientWidth;
if (available <= 0) {
return;
}
});
const widths = Array.from(container.children)
.slice(0, labels.length)
.map((item) => item.getBoundingClientRect().width);
const gap = parseFloat(getComputedStyle(container).columnGap) || 0;
setVisibleCount(getVisibleCount(widths, available, gap));
};
const observer = new ResizeObserver(measure);
observer.observe(container);
if (container.clientWidth > 0) {
setMaxVisibleCount(calculateMaxVisible(container.clientWidth));
}
measure();
return (): void => observer.disconnect();
}, [calculateMaxVisible]);
}, [labels, value]);
const needsOverflow = labels.length > maxVisibleCount;
const visibleLabels = needsOverflow
? labels.slice(0, maxVisibleCount)
: labels;
const remainingLabels = needsOverflow ? labels.slice(maxVisibleCount) : [];
const remainingLabels = labels.slice(visibleCount);
return (
<div
@@ -100,24 +61,23 @@ function LabelColumn({
className={styles.labelColumn}
data-testid="label-column"
>
{visibleLabels.map((label) => (
<LabelTag key={label} label={label} color={color} value={value?.[label]} />
{labels.map((label, index) => (
<LabelTag
key={label}
label={label}
color={color}
value={value?.[label]}
className={cx({
[styles.overflowed]: index >= visibleCount,
[styles.shrinkable]: index === 0 && visibleCount === 1,
})}
/>
))}
{remainingLabels.length > 0 && (
<TooltipRoot>
<TooltipTrigger asChild>
<span>
<Badge
color={color}
className={styles.overflowBadge}
variant="outline"
data-testid="label-overflow-badge"
>
+{remainingLabels.length}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom" align="end">
<Tooltip
side="bottom"
align="end"
title={
<div className={styles.tooltipContent}>
<span>
{remainingLabels
@@ -140,8 +100,14 @@ function LabelColumn({
<Copy size={12} />
</button>
</div>
</TooltipContent>
</TooltipRoot>
}
>
<span>
<Badge color={color} variant="outlined" testId="label-overflow-badge">
+{remainingLabels.length}
</Badge>
</span>
</Tooltip>
)}
</div>
);

View File

@@ -1,11 +1,3 @@
.labelBadge {
cursor: default;
font-size: 12px;
max-width: 180px;
text-overflow: ellipsis;
}
.labelValue {
text-overflow: ellipsis;
overflow: hidden;

View File

@@ -1,35 +1,24 @@
import { Copy } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
import { toast } from '@signozhq/ui/sonner';
import {
TooltipContent,
TooltipRoot,
TooltipTrigger,
} from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { useCopyToClipboard } from 'react-use';
import styles from './LabelTag.module.scss';
export interface LabelTagProps {
label: string;
color?:
| 'primary'
| 'secondary'
| 'success'
| 'error'
| 'warning'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua'
| 'vanilla';
color?: BadgeColorType;
value?: string;
className?: string;
}
function LabelTag({ label, value, color }: LabelTagProps): JSX.Element {
function LabelTag({
label,
value,
color,
className,
}: LabelTagProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const displayText = value ? `${label}: ${value}` : label;
const searchFormat = value ? `${label} ${value}` : label;
@@ -41,20 +30,8 @@ function LabelTag({ label, value, color }: LabelTagProps): JSX.Element {
};
return (
<TooltipRoot>
<TooltipTrigger asChild>
<span>
<Badge
color={color}
className={styles.labelBadge}
variant="outline"
data-testid={`label-tag-${label}`}
>
<span className={styles.labelValue}>{displayText}</span>
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
<Tooltip
title={
<div className={styles.tooltipContent}>
<span>{displayText}</span>
<button
@@ -66,8 +43,19 @@ function LabelTag({ label, value, color }: LabelTagProps): JSX.Element {
<Copy size={12} />
</button>
</div>
</TooltipContent>
</TooltipRoot>
}
>
<span className={className}>
<Badge
textTransform="none"
color={color ?? 'secondary'}
variant="outlined"
testId={`label-tag-${label}`}
>
<span className={styles.labelValue}>{displayText}</span>
</Badge>
</span>
</Tooltip>
);
}

View File

@@ -1,14 +1,33 @@
export const BADGE_GAP = 4;
export const OVERFLOW_BADGE_WIDTH = 40;
export const BADGE_MAX_WIDTH = 180;
export const BADGE_PADDING = 16;
export const CHAR_WIDTH = 7;
export function estimateBadgeWidth(label: string, value?: string): number {
const displayText = value ? `${label}: ${value}` : label;
return Math.min(
displayText.length * CHAR_WIDTH + BADGE_PADDING,
BADGE_MAX_WIDTH,
/**
* How many leading labels fit in `available` px, reserving room for the `+N` badge
* whenever some are left out. At least one label is always shown.
*/
export function getVisibleCount(
widths: number[],
available: number,
gap: number,
): number {
const total = widths.reduce(
(sum, width, index) => sum + width + (index > 0 ? gap : 0),
0,
);
if (total <= available) {
return widths.length;
}
const limit = available - OVERFLOW_BADGE_WIDTH - gap;
let used = 0;
let count = 0;
for (const width of widths) {
used += width + (count > 0 ? gap : 0);
if (used > limit) {
break;
}
count++;
}
return Math.max(1, count);
}

View File

@@ -30,21 +30,23 @@ function NoResultsEmptyState({
<div className={styles.actions}>
{onClear && (
<Button
size="md"
variant="outlined"
color="secondary"
onClick={onClear}
data-testid="no-results-clear-button"
testId="no-results-clear-button"
>
{clearButtonText}
</Button>
)}
{onRefresh && (
<Button
size="md"
variant="outlined"
color="secondary"
prefix={<RefreshCw size={14} />}
onClick={onRefresh}
data-testid="no-results-refresh-button"
testId="no-results-refresh-button"
>
Refresh
</Button>

View File

@@ -1,4 +1,4 @@
import type { BadgeColor } from '@signozhq/ui/badge';
import type { BadgeColorType } from '@signozhq/ui/badge';
export const STATE_ORDER = ['firing', 'pending', 'inactive', 'disabled'];
export const SEVERITY_ORDER = ['critical', 'error', 'warning', 'info'];
@@ -24,9 +24,9 @@ export const SEVERITY_COLORS: Record<string, string> = {
info: 'var(--bg-robin-500)',
};
export const SEVERITY_BADGE_COLORS: Record<string, BadgeColor> = {
critical: 'error',
error: 'error',
export const SEVERITY_BADGE_COLORS: Record<string, BadgeColorType> = {
critical: 'danger',
error: 'danger',
warning: 'warning',
info: 'primary',
};

View File

@@ -33,36 +33,3 @@
color: var(--l1-foreground);
white-space: nowrap;
}
.auth-header-help-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 32px;
padding: 10px 16px;
background: var(--l2-background);
color: var(--l2-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
transition: opacity 0.2s ease;
span {
font-family: var(--font-family-inter, Inter, sans-serif);
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--l2-foreground);
text-align: center;
}
svg {
flex-shrink: 0;
color: var(--l2-foreground);
}
&:hover {
opacity: 0.8;
}
}

View File

@@ -22,11 +22,11 @@ function AuthHeader(): JSX.Element {
<span className="auth-header-logo-text">SigNoz</span>
</div>
<Button
className="auth-header-help-button"
size="md"
prefix={<LifeBuoy size={12} />}
onClick={handleGetHelp}
variant="solid"
color="none"
color="secondary"
>
Get Help
</Button>

View File

@@ -1,7 +1,7 @@
import React, { Dispatch, SetStateAction, useState } from 'react';
import { Check, Plus, X } from '@signozhq/icons';
import { Button, Flex } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Pill } from '@signozhq/ui/pill';
import Input from 'components/Input';
import './Badges.styles.scss';
@@ -47,18 +47,13 @@ function Badges({ tags, setTags }: AddTagsProps): JSX.Element {
return (
<div className="tags-container">
{tags.map<React.ReactNode>((tag) => (
<Badge
<Pill.Closeable
textTransform="none"
key={tag}
color="vanilla"
style={{ userSelect: 'none' }}
closable
onClose={(e): void => {
e.preventDefault();
handleClose(tag);
}}
onClose={() => handleClose(tag)}
>
{tag}
</Badge>
</Pill.Closeable>
))}
{inputVisible && (

View File

@@ -39,9 +39,3 @@
}
}
}
.cloud-service-data-collected-table-tooltip {
max-width: 280px;
white-space: normal;
word-break: break-word;
}

View File

@@ -4,7 +4,7 @@ import {
CloudintegrationtypesCollectedMetricDTO,
} from 'api/generated/services/sigNoz.schemas';
import { BarChart, Info, ScrollText } from '@signozhq/icons';
import { TooltipProvider, TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import './CloudServiceDataCollected.styles.scss';
@@ -88,23 +88,15 @@ function CloudServiceDataCollected({
<BarChart size={14} />
Metrics
{metricsInfoTooltip && (
<TooltipProvider>
<TooltipSimple
title={metricsInfoTooltip}
side="top"
tooltipContentProps={{
className: 'cloud-service-data-collected-table-tooltip',
}}
<Tooltip title={metricsInfoTooltip} side="top">
<span
className="cloud-service-data-collected-table-heading-info"
aria-label="About the metrics listed below"
data-testid="data-collected-metrics-info"
>
<span
className="cloud-service-data-collected-table-heading-info"
aria-label="About the metrics listed below"
data-testid="data-collected-metrics-info"
>
<Info size={12} />
</span>
</TooltipSimple>
</TooltipProvider>
<Info size={12} />
</span>
</Tooltip>
)}
</div>
<Table

View File

@@ -2,6 +2,13 @@
position: relative;
}
.copyButton {
position: absolute;
right: 8px;
top: 8px;
z-index: 1;
}
.codeBlockSyntaxHighlighter {
background-color: var(--l2-background) !important;
border-radius: 4px !important;

View File

@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Check, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import SyntaxHighlighter, {
a11yDark,
} from 'components/MarkdownRenderer/syntaxHighlighter';
@@ -52,16 +53,20 @@ function CodeBlock({
data-testid="code-block-container"
>
{showCopyButton ? (
<Button
variant="ghost"
color="secondary"
size="sm"
onClick={handleCopy}
prefix={isCopied ? <Check size={14} /> : <Copy size={14} />}
aria-label="Copy code"
title={isCopied ? 'Copied' : 'Copy'}
style={{ position: 'absolute', right: 8, top: 8, zIndex: 1 }}
/>
<div className={styles.copyButton}>
<Tooltip title={isCopied ? 'Copied' : 'Copy'}>
<Button
variant="ghost"
color="secondary"
size="sm"
icon
onClick={handleCopy}
aria-label="Copy code"
>
{isCopied ? <Check size={14} /> : <Copy size={14} />}
</Button>
</Tooltip>
</div>
) : null}
<SyntaxHighlighter
style={a11yDark}

View File

@@ -134,26 +134,27 @@ function CreateServiceAccountModal(): JSX.Element {
<DialogFooter className="create-sa-modal__footer">
<Button
size="md"
type="button"
variant="solid"
color="secondary"
onClick={handleClose}
data-testid="create-sa-cancel-btn"
testId="create-sa-cancel-btn"
prefix={<X size={12} />}
>
<X size={12} />
Cancel
</Button>
<AuthZButton
size="md"
checks={[SACreatePermission]}
withPortal={false}
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
data-testid="create-sa-submit-btn"
testId="create-sa-submit-btn"
>
Create Service Account
</AuthZButton>

View File

@@ -5,6 +5,7 @@ import {
} from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { submitImplicitly } from 'tests/submitImplicitly';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
@@ -60,7 +61,10 @@ describe('CreateServiceAccountModal', () => {
await screen.findByTestId('create-sa-name-input');
await waitFor(() =>
expect(screen.getByTestId('create-sa-submit-btn')).toBeDisabled(),
expect(screen.getByTestId('create-sa-submit-btn')).toHaveAttribute(
'aria-disabled',
'true',
),
);
});
@@ -72,10 +76,14 @@ describe('CreateServiceAccountModal', () => {
const submitBtn = await screen.findByTestId('create-sa-submit-btn');
await user.type(nameInput, 'test');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await waitFor(() =>
expect(submitBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.clear(nameInput);
await waitFor(() => expect(submitBtn).toBeDisabled());
await waitFor(() =>
expect(submitBtn).toHaveAttribute('aria-disabled', 'true'),
);
});
it('successful submit shows toast.success and closes modal', async () => {
@@ -86,7 +94,9 @@ describe('CreateServiceAccountModal', () => {
await user.type(nameInput, 'Deploy Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await waitFor(() =>
expect(submitBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(submitBtn);
await waitFor(() => {
@@ -102,6 +112,40 @@ describe('CreateServiceAccountModal', () => {
});
});
it('pressing Enter again while creating sends one request', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
let requests = 0;
server.use(
rest.post(SERVICE_ACCOUNTS_ENDPOINT, (_, res, ctx) => {
requests += 1;
return res(
ctx.delay(100),
ctx.status(201),
ctx.json({ status: 'success', data: {} }),
);
}),
);
renderModal();
const nameInput = await screen.findByTestId<HTMLInputElement>(
'create-sa-name-input',
);
await user.type(nameInput, 'Deploy Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
await waitFor(() =>
expect(submitBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
submitImplicitly(nameInput);
await waitFor(() => expect(submitBtn).toHaveAttribute('aria-busy', 'true'));
submitImplicitly(nameInput);
await waitFor(() => {
expect(mockToast.success).toHaveBeenCalled();
});
expect(requests).toBe(1);
});
it('shows toast.error on API error and keeps modal open', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -120,7 +164,9 @@ describe('CreateServiceAccountModal', () => {
await user.type(nameInput, 'Dupe Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await waitFor(() =>
expect(submitBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(submitBtn);
await waitFor(() => {
@@ -180,7 +226,10 @@ describe('CreateServiceAccountModal', () => {
).resolves.toBeInTheDocument();
// The footer lives outside the guard: submit is gated, Cancel still works.
expect(screen.getByTestId('create-sa-submit-btn')).toBeDisabled();
expect(screen.getByTestId('create-sa-submit-btn')).toHaveAttribute(
'aria-disabled',
'true',
);
await user.click(screen.getByTestId('create-sa-cancel-btn'));

View File

@@ -4,30 +4,6 @@
align-items: center;
gap: 4px;
.zoom-out-btn {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--secondary-foreground);
background-color: var(--secondary-background);
border: 1px solid var(--secondary-border);
border-radius: 2px;
box-shadow: none;
padding: 10px;
height: 33px;
&:hover:not(:disabled) {
color: var(--primary-foreground);
background: var(--primary-background);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.timeSelection-input {
&:hover {
border-color: var(--l1-border) !important;

View File

@@ -650,20 +650,20 @@ function CustomTimePicker({
</Popover>
</Tooltip>
{!showLiveLogs && !isModalTimeSelection && (
<Tooltip
title={
zoomOutDisabled ? 'Zoom out time range is limited to 1 month' : 'Zoom out'
}
>
<Tooltip title={zoomOutDisabled ? undefined : 'Zoom out'}>
<Button
className="zoom-out-btn"
disabledTooltip="Zoom out time range is limited to 1 month"
size="md"
onClick={handleZoomOut}
disabled={zoomOutDisabled}
data-testid="zoom-out-btn"
prefix={<ZoomOut size={14} />}
testId="zoom-out-btn"
icon
aria-label="Zoom out"
variant="solid"
color="none"
/>
color="secondary"
>
<ZoomOut size={14} />
</Button>
</Tooltip>
)}
</div>

View File

@@ -164,6 +164,6 @@ describe('CustomTimePicker - zoom out button', () => {
);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
expect(zoomOutBtn).toBeDisabled();
expect(zoomOutBtn).toHaveAttribute('aria-disabled', 'true');
});
});

View File

@@ -27,12 +27,14 @@ function DetailsHeader({
const closeButton = (
<Button
variant="ghost"
size="icon"
size="sm"
icon
color="secondary"
onClick={onClose}
aria-label="Close"
prefix={<X size={14} />}
></Button>
>
<X size={14} />
</Button>
);
return (

View File

@@ -42,12 +42,5 @@
height: 1px;
background: var(--l1-border);
}
.export-button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}

View File

@@ -308,13 +308,16 @@ describe.each([
fireEvent.click(screen.getByText('Export'));
expect(screen.getByTestId(testId)).toBeDisabled();
expect(screen.getByTestId(testId)).toHaveAttribute('aria-disabled', 'true');
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
resolveDownload!();
await waitFor(() => {
expect(screen.getByTestId(testId)).not.toBeDisabled();
expect(screen.getByTestId(testId)).not.toHaveAttribute(
'aria-disabled',
'true',
);
});
});
});

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import { Popover, Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { RadioGroup } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -69,10 +69,15 @@ export default function DownloadOptionsMenu({
>
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={DownloadFormats.CSV}>csv</RadioGroupItem>
<RadioGroupItem value={DownloadFormats.JSONL}>jsonl</RadioGroupItem>
</RadioGroup>
<RadioGroup
color="primary"
value={exportFormat}
onChange={setExportFormat}
items={[
{ value: DownloadFormats.CSV, label: 'csv' },
{ value: DownloadFormats.JSONL, label: 'jsonl' },
]}
/>
</div>
<div className="horizontal-line" />
@@ -80,19 +85,15 @@ export default function DownloadOptionsMenu({
<div className="row-limit">
<Typography.Text className="title">Number of Rows</Typography.Text>
<RadioGroup
color="primary"
value={String(rowLimit)}
onChange={(value): void => setRowLimit(Number(value))}
>
<RadioGroupItem value={String(DownloadRowCounts.TEN_K)}>
10k
</RadioGroupItem>
<RadioGroupItem value={String(DownloadRowCounts.THIRTY_K)}>
30k
</RadioGroupItem>
<RadioGroupItem value={String(DownloadRowCounts.FIFTY_K)}>
50k
</RadioGroupItem>
</RadioGroup>
items={[
{ value: String(DownloadRowCounts.TEN_K), label: '10k' },
{ value: String(DownloadRowCounts.THIRTY_K), label: '30k' },
{ value: String(DownloadRowCounts.FIFTY_K), label: '50k' },
]}
/>
</div>
{dataSource !== DataSource.TRACES && (
@@ -101,12 +102,15 @@ export default function DownloadOptionsMenu({
<div className="columns-scope">
<Typography.Text className="title">Columns</Typography.Text>
<RadioGroup value={columnsScope} onChange={setColumnsScope}>
<RadioGroupItem value={DownloadColumnsScopes.ALL}>All</RadioGroupItem>
<RadioGroupItem value={DownloadColumnsScopes.SELECTED}>
Selected
</RadioGroupItem>
</RadioGroup>
<RadioGroup
color="primary"
value={columnsScope}
onChange={setColumnsScope}
items={[
{ value: DownloadColumnsScopes.ALL, label: 'All' },
{ value: DownloadColumnsScopes.SELECTED, label: 'Selected' },
]}
/>
</div>
</>
)}
@@ -115,9 +119,9 @@ export default function DownloadOptionsMenu({
variant="solid"
color="primary"
prefix={<Download size={16} />}
size="md"
width="100%"
onClick={handleExport}
className="export-button"
disabled={isDownloading}
loading={isDownloading}
>
Export
@@ -148,13 +152,14 @@ export default function DownloadOptionsMenu({
<Button
variant="ghost"
color="secondary"
size="icon"
prefix={<Download size={14} />}
size="sm"
icon
aria-label="Download"
data-testid={`periscope-btn-download-${dataSource}`}
disabled={isDownloading}
testId={`periscope-btn-download-${dataSource}`}
loading={isDownloading}
/>
>
<Download size={14} />
</Button>
</Tooltip>
</Popover>
);

View File

@@ -38,18 +38,23 @@ function DeleteMemberDialog({
const footer = (
<>
<Button variant="solid" color="secondary" onClick={onClose}>
<X size={12} />
<Button
size="md"
variant="solid"
color="secondary"
onClick={onClose}
prefix={<X size={12} />}
>
Cancel
</Button>
<Button
size="md"
variant="solid"
color="destructive"
disabled={isDeleting}
color="danger"
onClick={onConfirm}
loading={isDeleting}
prefix={<Trash2 size={12} />}
>
<Trash2 size={12} />
{isDeleting ? 'Processing...' : title}
</Button>
</>

View File

@@ -90,7 +90,6 @@
font-weight: var(--uppercase-small-500-font-weight);
line-height: 100%;
letter-spacing: 0.44px;
text-transform: uppercase;
}
}
@@ -113,6 +112,7 @@
&__footer-left {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
&__footer-right {
@@ -128,10 +128,6 @@
flex-shrink: 0;
}
&__tooltip-wrapper {
display: inline-flex;
}
&__footer-btn {
display: inline-flex;
align-items: center;
@@ -200,6 +196,7 @@
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-radius: 2px;
padding-right: var(--spacing-4);
}
&__link-text-wrap {
@@ -220,8 +217,4 @@
line-height: var(--line-height-18);
letter-spacing: -0.07px;
}
&__copy-btn {
border-left: 1px solid var(--l1-border);
}
}

View File

@@ -519,7 +519,12 @@ function EditMemberDrawer({
localRoles.map((roleId) => {
const role = availableRoles.find((r) => r.id === roleId);
return (
<Badge key={roleId} color="vanilla">
<Badge
textTransform="none"
variant="solid"
key={roleId}
color="secondary"
>
{role?.name ?? roleId}
</Badge>
);
@@ -559,15 +564,15 @@ function EditMemberDrawer({
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">Status</span>
{member?.status === MemberStatus.Active ? (
<Badge color="forest" variant="outline">
<Badge color="success" variant="outlined">
ACTIVE
</Badge>
) : member?.status === MemberStatus.Deleted ? (
<Badge color="cherry" variant="outline">
<Badge color="danger" variant="outlined">
DELETED
</Badge>
) : (
<Badge color="amber" variant="outline">
<Badge color="warning" variant="outlined">
INVITED
</Badge>
)}
@@ -575,12 +580,16 @@ function EditMemberDrawer({
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">{joinedOnLabel}</span>
<Badge color="vanilla">{formatTimestamp(member?.joinedOn)}</Badge>
<Badge textTransform="none" variant="solid" color="secondary">
{formatTimestamp(member?.joinedOn)}
</Badge>
</div>
{!isInvited && (
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">Last Modified</span>
<Badge color="vanilla">{formatTimestamp(member?.updatedAt)}</Badge>
<Badge textTransform="none" variant="solid" color="secondary">
{formatTimestamp(member?.updatedAt)}
</Badge>
</div>
)}
</div>
@@ -611,55 +620,59 @@ function EditMemberDrawer({
{!isDeleted && (
<>
<div className="edit-member-drawer__footer-left">
<Tooltip title={getDeleteTooltip(isRootUser, isSelf)}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
onClick={(): void => setShowDeleteConfirm(true)}
disabled={isRootUser || isSelf}
variant="link"
color="destructive"
>
<Trash2 size={12} />
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
</span>
</Tooltip>
<Button
disabledTooltip={getDeleteTooltip(isRootUser, isSelf)}
size="md"
onClick={(): void => setShowDeleteConfirm(true)}
disabled={isRootUser || isSelf}
variant="link"
color="danger"
prefix={<Trash2 size={12} />}
>
{isInvited ? 'Revoke Invite' : 'Delete Member'}
</Button>
<div className="edit-member-drawer__footer-divider" />
<Tooltip title={isRootUser ? ROOT_USER_TOOLTIP : undefined}>
<span className="edit-member-drawer__tooltip-wrapper">
<Button
onClick={handleGenerateResetLink}
disabled={isGeneratingLink || isRootUser || isLoadingTokenStatus}
variant="link"
color="warning"
>
<RefreshCw size={12} />
{isGeneratingLink
? 'Generating...'
: isInvited
? getInviteButtonLabel(
isLoadingTokenStatus,
existingToken,
isTokenExpired,
tokenNotFound,
)
: 'Generate Password Reset Link'}
</Button>
</span>
</Tooltip>
<Button
disabledTooltip={ROOT_USER_TOOLTIP}
size="md"
onClick={handleGenerateResetLink}
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 variant="outlined" color="secondary" onClick={handleClose}>
<X size={14} />
<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 || isSaving || isRootUser}
disabled={!isDirty || isRootUser}
onClick={handleSave}
loading={isSaving}
>

View File

@@ -45,11 +45,11 @@ function ResetLinkDialog({
<span className="reset-link-dialog__link-text">{resetLink}</span>
</div>
<Button
size="md"
variant="link"
color="secondary"
onClick={onCopy}
prefix={hasCopied ? <Check size={12} /> : <Copy size={12} />}
className="reset-link-dialog__copy-btn"
>
{hasCopied ? 'Copied!' : 'Copy'}
</Button>

View File

@@ -251,7 +251,7 @@ describe('EditMemberDrawer', () => {
expect(screen.getByText('ACTIVE')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /save member details/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('enables Save after editing name and calls updateUser on confirm', async () => {
@@ -271,7 +271,9 @@ describe('EditMemberDrawer', () => {
await user.type(nameInput, 'Alice Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
@@ -295,7 +297,9 @@ describe('EditMemberDrawer', () => {
await user.type(nameInput, 'Alice Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -323,7 +327,9 @@ describe('EditMemberDrawer', () => {
await user.click(await screen.findByTitle('signoz-editor'));
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -349,7 +355,9 @@ describe('EditMemberDrawer', () => {
await user.click(removeBtn);
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -505,7 +513,9 @@ describe('EditMemberDrawer', () => {
await user.type(nameInput, 'Bob Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -541,7 +551,9 @@ describe('EditMemberDrawer', () => {
await user.type(nameInput, 'Alice Updated');
const saveBtn = screen.getByRole('button', { name: /save member details/i });
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await waitFor(() =>
expect(saveBtn).not.toHaveAttribute('aria-disabled', 'true'),
);
await user.click(saveBtn);
await waitFor(() => {
@@ -619,7 +631,7 @@ describe('EditMemberDrawer', () => {
renderDrawer({ member: selfMember });
expect(
screen.getByRole('button', { name: /delete member/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('does not open delete confirm dialog when Delete is clicked while disabled (isSelf)', async () => {
@@ -642,7 +654,7 @@ describe('EditMemberDrawer', () => {
renderDrawer({ member: selfMember });
expect(
screen.getByRole('button', { name: /generate password reset link/i }),
).not.toBeDisabled();
).not.toHaveAttribute('aria-disabled', 'true');
});
});
@@ -664,21 +676,21 @@ describe('EditMemberDrawer', () => {
renderDrawer();
expect(
screen.getByRole('button', { name: /delete member/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('disables Reset Link button for root user', () => {
renderDrawer();
expect(
screen.getByRole('button', { name: /generate password reset link/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('disables Save button for root user', () => {
renderDrawer();
expect(
screen.getByRole('button', { name: /save member details/i }),
).toBeDisabled();
).toHaveAttribute('aria-disabled', 'true');
});
it('does not open delete confirm dialog when Delete is clicked while disabled (root)', async () => {

View File

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

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { DropdownMenuSimple, type MenuProps } from '@signozhq/ui/dropdown-menu';
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
@@ -137,16 +137,15 @@ function ExplorerCard({
);
};
const moreOptionMenu: MenuProps = {
items: [
{
key: 'delete',
label: <Typography.Text strong>Delete</Typography.Text>,
onClick: onDeleteHandler,
icon: <Trash2 size="md" />,
},
],
};
const moreOptionItems: DropdownItemType[] = [
{
type: 'item',
value: 'delete',
label: <Typography.Text strong>Delete</Typography.Text>,
onClick: onDeleteHandler,
prefix: <Trash2 size="md" />,
},
];
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
const saveButtonIcon = isQueryUpdated ? null : <Save size="md" />;
@@ -230,9 +229,14 @@ function ExplorerCard({
</Popover>
<Share2 onClick={onCopyUrlHandler} size="md" />
{viewKey && (
<DropdownMenuSimple menu={moreOptionMenu}>
<Dropdown
items={moreOptionItems}
nativeButton
align="end"
side="bottom"
>
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
</DropdownMenuSimple>
</Dropdown>
)}
</Space>
</OffSetCol>

View File

@@ -26,8 +26,4 @@
font-size: var(--periscope-font-size-base);
}
}
.export-button {
width: 100%;
}
}

View File

@@ -1,8 +1,8 @@
import { Download } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { RadioGroup } from '@signozhq/ui/radio-group';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import {
ClientExportData,
@@ -51,36 +51,41 @@ export default function ExportMenu({
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<TooltipSimple title="Download">
<Tooltip title="Download">
<PopoverTrigger asChild>
<Button
variant="ghost"
color="secondary"
size="icon"
size="sm"
icon
aria-label="Download"
data-testid={`export-menu-${dataSource}`}
disabled={isExporting}
testId={`export-menu-${dataSource}`}
loading={isExporting}
>
<Download size={14} />
</Button>
</PopoverTrigger>
</TooltipSimple>
</Tooltip>
<PopoverContent align="end" className="export-menu-popover">
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={ExportFormat.Csv}>csv</RadioGroupItem>
<RadioGroupItem value={ExportFormat.Jsonl}>jsonl</RadioGroupItem>
</RadioGroup>
<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"
className="export-button"
width="100%"
onClick={handleExport}
disabled={isExporting}
loading={isExporting}
prefix={<Download size={16} />}
>

View File

@@ -80,6 +80,6 @@ describe('ExportMenu', () => {
mockIsExporting = true;
renderMenu();
expect(screen.getByTestId(TEST_ID)).toBeDisabled();
expect(screen.getByTestId(TEST_ID)).toHaveAttribute('aria-disabled', 'true');
});
});

View File

@@ -57,9 +57,8 @@ function SortableField({
</div>
{!isRequired && (
<Button
className={cx(styles.removeBtn, 'periscope-btn')}
variant="outlined"
color="destructive"
variant="solid"
color="danger"
size="sm"
onClick={(): void => onRemove(field)}
>

View File

@@ -98,11 +98,15 @@
user-select: none;
font-size: 13px;
> [data-slot='button'] {
opacity: 0;
transition: opacity 0.15s ease-in-out;
}
&:hover {
background-color: var(--l2-background);
.removeBtn,
.addBtn {
> [data-slot='button'] {
opacity: 1;
}
}
@@ -137,14 +141,6 @@
height: 32px;
}
.removeBtn,
.addBtn {
padding: 4px 10px;
opacity: 0;
transition: opacity 0.15s ease-in-out;
flex-shrink: 0;
}
.footer {
display: flex;
gap: 12px;

View File

@@ -173,6 +173,7 @@ function FieldsSelectorContent({
{hasUnsavedChanges && (
<div className={styles.footer}>
<Button
size="md"
variant="outlined"
color="secondary"
onClick={handleDiscard}
@@ -181,6 +182,7 @@ function FieldsSelectorContent({
Discard
</Button>
<Button
size="md"
variant="solid"
color="primary"
onClick={handleSave}

View File

@@ -138,7 +138,6 @@ function OtherFields({
<span className={styles.fieldKey}>{attr.name}</span>
{!isAtLimit && (
<Button
className={cx(styles.addBtn, 'periscope-btn')}
variant="outlined"
color="secondary"
size="sm"

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { toast } from '@signozhq/ui/sonner';
import { Button, Input } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { handleContactSupport } from 'container/Integrations/utils';
@@ -102,10 +102,12 @@ function FeedbackModal({ onClose }: { onClose: () => void }): JSX.Element {
return (
<div className="feedback-modal-container">
<div className="feedback-modal-header">
<ToggleGroupSimple
<ToggleGroup
variant="outlined"
color="secondary"
size="sm"
type="single"
value={activeTab}
className="feedback-modal-tabs"
onChange={setActiveTab}
items={items}
/>

View File

@@ -1,7 +1,8 @@
.header-right-section-container {
display: flex;
align-items: center;
gap: 8px;
gap: var(--spacing-8);
padding-right: var(--spacing-8);
}
.header-ai-assistant-btn-container {
@@ -121,78 +122,26 @@
}
.feedback-modal-container {
.feedback-modal-tabs {
width: 100%;
.feedback-modal-tab-label {
display: flex;
align-items: center;
gap: 8px;
.ant-radio-button-wrapper {
flex: 1;
margin: 0px !important;
border: 1px solid var(--l1-border);
&:before {
display: none;
}
.ant-radio-button-checked {
background-color: var(--l3-background);
}
.tab-icon {
width: 6px;
height: 6px;
}
.feedback-modal-tab-label {
display: flex;
align-items: center;
gap: 8px;
.tab-icon {
width: 6px;
height: 6px;
}
.feedback-tab {
background-color: var(--danger-background);
}
.bug-tab {
background-color: var(--warning-background);
}
.feature-tab {
background-color: var(--primary-background);
}
.feedback-tab {
background-color: var(--danger-background);
}
.ant-tabs-nav-list {
.ant-tabs-tab {
padding: 6px 16px;
.bug-tab {
background-color: var(--warning-background);
}
border-radius: 2px;
background: var(--l2-background);
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
border: 1px solid var(--l1-border);
margin: 0 !important;
.ant-tabs-tab-btn {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
letter-spacing: -0.06px;
}
&-active {
background: var(--l3-background);
color: var(--l1-foreground);
border-bottom: none !important;
.ant-tabs-tab-btn {
color: var(--l1-foreground);
}
}
}
.feature-tab {
background-color: var(--primary-background);
}
}

View File

@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Dot } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { Popover } from 'antd';
@@ -113,24 +113,26 @@ function HeaderRightSection({
</span>
) : null}
<TooltipSimple title={NOZ_TOOLTIP_TITLE}>
<Button
variant="solid"
color="secondary"
className="noz-wave"
onClick={handleOpenAIAssistant}
aria-label={
showHeaderPendingBadge
? pendingUserInputCount === 1
? 'Open Noz, 1 action needs your response'
: `Open Noz, ${pendingUserInputCount} actions need your response`
: 'Open Noz'
}
prefix={<Noz size={20} />}
>
<Typography.Text>Noz</Typography.Text>
</Button>
</TooltipSimple>
<span className="noz-wave">
<Tooltip title={NOZ_TOOLTIP_TITLE}>
<Button
size="md"
variant="ghost"
color="secondary"
onClick={handleOpenAIAssistant}
aria-label={
showHeaderPendingBadge
? pendingUserInputCount === 1
? 'Open Noz, 1 action needs your response'
: `Open Noz, ${pendingUserInputCount} actions need your response`
: 'Open Noz'
}
prefix={<Noz size={20} />}
>
<Typography.Text>Noz</Typography.Text>
</Button>
</Tooltip>
</span>
</div>
)}
@@ -147,13 +149,15 @@ function HeaderRightSection({
onOpenChange={handleOpenFeedbackModalChange}
>
<Button
variant="ghost"
size="icon"
className="share-feedback-btn"
color="primary"
variant="link"
size="md"
icon
aria-label="Feedback"
prefix={<SquarePen size={14} />}
onClick={handleOpenFeedbackModal}
/>
>
<SquarePen size={14} />
</Button>
</Popover>
)}
@@ -170,16 +174,19 @@ function HeaderRightSection({
onOpenChange={handleOpenAnnouncementsModalChange}
>
<Button
variant="ghost"
size="icon"
color="primary"
variant="link"
size="md"
icon
aria-label="Announcements"
prefix={<Inbox size={14} />}
onClick={(): void => {
logEvent('Announcements: Clicked', {
page: location.pathname,
});
}}
/>
>
<Inbox size={14} />
</Button>
</Popover>
)}
@@ -196,12 +203,15 @@ function HeaderRightSection({
onOpenChange={handleOpenShareURLModalChange}
>
<Button
variant="ghost"
size="icon"
color="primary"
variant="link"
size="md"
icon
aria-label="Share"
prefix={<Globe size={14} />}
onClick={handleOpenShareURLModal}
/>
>
<Globe size={14} />
</Button>
</Popover>
)}
</div>

View File

@@ -149,6 +149,9 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
<Info size={14} color={Color.BG_AMBER_600} />
)}
<Switch
color="primary"
textPlacement="right"
disabledTooltip="Please select / enter valid relative time to toggle."
value={enableAbsoluteTime}
disabled={!isValidateRelativeTime}
onChange={(): void => {
@@ -173,6 +176,8 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
</Typography.Text>
<div className="absolute-relative-time-toggler">
<Switch
color="primary"
textPlacement="right"
value={enableExtraOption}
onChange={(): void => setEnableExtraOption((prev) => !prev)}
/>

View File

@@ -69,23 +69,23 @@ describe('FeedbackModal', () => {
const user = userEvent.setup();
render(<FeedbackModal onClose={mockOnClose} />);
// Initially, feedback radio should be active
const feedbackRadio = screen.getByRole('radio', { name: 'Feedback' });
expect(feedbackRadio).toBeChecked();
// Initially, feedback button should be active
const feedbackButton = screen.getByRole('button', { name: 'Feedback' });
expect(feedbackButton).toHaveAttribute('aria-pressed', 'true');
const bugTab = screen.getByText('Report a bug');
await user.click(bugTab);
// Bug radio should now be active
const bugRadio = screen.getByRole('radio', { name: 'Report a bug' });
expect(bugRadio).toBeChecked();
// Bug button should now be active
const bugButton = screen.getByRole('button', { name: 'Report a bug' });
expect(bugButton).toHaveAttribute('aria-pressed', 'true');
const featureTab = screen.getByText('Feature request');
await user.click(featureTab);
// Feature radio should now be active
const featureRadio = screen.getByRole('radio', { name: 'Feature request' });
expect(featureRadio).toBeChecked();
// Feature button should now be active
const featureButton = screen.getByRole('button', { name: 'Feature request' });
expect(featureButton).toHaveAttribute('aria-pressed', 'true');
});
it('should update feedback text when typing in textarea', async () => {
@@ -133,9 +133,9 @@ describe('FeedbackModal', () => {
const bugTab = screen.getByText('Report a bug');
await user.click(bugTab);
// Verify bug report radio is now active
const bugRadio = screen.getByRole('radio', { name: 'Report a bug' });
expect(bugRadio).toBeChecked();
// Verify bug report button is now active
const bugButton = screen.getByRole('button', { name: 'Report a bug' });
expect(bugButton).toHaveAttribute('aria-pressed', 'true');
const textarea = screen.getByPlaceholderText('Write your feedback here...');
const submitButton = screen.getByRole('button', { name: /submit/i });
@@ -166,9 +166,9 @@ describe('FeedbackModal', () => {
const featureTab = screen.getByText('Feature request');
await user.click(featureTab);
// Verify feature request radio is now active
const featureRadio = screen.getByRole('radio', { name: 'Feature request' });
expect(featureRadio).toBeChecked();
// Verify feature request button is now active
const featureButton = screen.getByRole('button', { name: 'Feature request' });
expect(featureButton).toHaveAttribute('aria-pressed', 'true');
const textarea = screen.getByPlaceholderText('Write your feedback here...');
const submitButton = screen.getByRole('button', { name: /submit/i });
@@ -262,8 +262,8 @@ describe('FeedbackModal', () => {
);
expect(newTextArea).toHaveValue(''); // Should be empty
// Verify active radio is reset to default (Feedback radio)
const feedbackRadio = screen.getByRole('radio', { name: 'Feedback' });
expect(feedbackRadio).toBeChecked();
// Verify active button is reset to default (Feedback button)
const feedbackButton = screen.getByRole('button', { name: 'Feedback' });
expect(feedbackButton).toHaveAttribute('aria-pressed', 'true');
});
});

View File

@@ -176,7 +176,7 @@ describe('ShareURLModal', () => {
expect(
screen.getByText('Please select / enter valid relative time to toggle.'),
).toBeInTheDocument();
expect(screen.getByRole('switch')).toBeDisabled();
expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true');
});
it('should process URL with absolute time for non-custom time', async () => {

View File

@@ -1,44 +1,32 @@
import { Badge } from '@signozhq/ui/badge';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
type BadgeColor =
| 'vanilla'
| 'robin'
| 'forest'
| 'amber'
| 'sienna'
| 'cherry'
| 'sakura'
| 'aqua';
function getStatusCodeColor(statusCode: number): BadgeColorType {
if (statusCode >= 200 && statusCode < 300) {
return 'success';
}
if (statusCode >= 300 && statusCode < 400) {
return 'primary';
}
if (statusCode >= 400 && statusCode < 500) {
return 'warning';
}
if (statusCode >= 500) {
return 'danger';
}
if (statusCode >= 100 && statusCode < 200) {
return 'secondary';
}
return 'primary';
}
interface HttpStatusBadgeProps {
statusCode: string | number;
testId?: string;
className?: string;
}
function getStatusCodeColor(statusCode: number): BadgeColor {
if (statusCode >= 200 && statusCode < 300) {
return 'forest'; // Success - green
}
if (statusCode >= 300 && statusCode < 400) {
return 'robin'; // Redirect - blue
}
if (statusCode >= 400 && statusCode < 500) {
return 'amber'; // Client error - amber
}
if (statusCode >= 500) {
return 'cherry'; // Server error - red
}
if (statusCode >= 100 && statusCode < 200) {
return 'vanilla'; // Informational - neutral
}
return 'robin'; // Default fallback
}
function HttpStatusBadge({
statusCode,
testId,
className,
}: HttpStatusBadgeProps): JSX.Element | null {
const numericStatusCode = Number(statusCode);
@@ -49,12 +37,7 @@ function HttpStatusBadge({
const color = getStatusCodeColor(numericStatusCode);
return (
<Badge
color={color}
variant="outline"
data-testid={testId}
className={className}
>
<Badge color={color} variant="outlined" testId={testId}>
{statusCode}
</Badge>
);

View File

@@ -119,11 +119,13 @@ function InviteMembers({
<div className={styles.cellAction}>
{canRemoveRow && (
<Button
variant="ghost"
color="destructive"
size="md"
variant="solid"
color="danger"
onClick={(): void => removeRow(row.id)}
aria-label="Remove row"
data-testid={`invite-remove-${row.id}`}
testId={`invite-remove-${row.id}`}
icon
>
<Trash2 size={12} />
</Button>
@@ -136,11 +138,12 @@ function InviteMembers({
{showAddButton && (
<div className={styles.addRow}>
<Button
size="md"
variant="dashed"
color="secondary"
prefix={<Plus size={12} />}
onClick={addRow}
data-testid="invite-add-row"
testId="invite-add-row"
>
Add another
</Button>

View File

@@ -158,37 +158,14 @@
}
}
.views-tabs {
color: var(--l2-foreground);
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
> button {
border: 1px solid var(--l1-border);
width: 114px;
&::before {
background: var(--l1-border);
}
&[data-state='on'] {
background: var(--l3-background);
color: var(--l1-foreground);
border: 1px solid var(--l1-border);
&::before {
background: var(--l1-border);
}
}
}
.view-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
}
.search-input {
@@ -239,42 +216,4 @@
align-items: center;
margin-left: 8px;
}
.log-arrow-btn {
padding: 0;
min-width: 28px;
height: 28px;
border-radius: 4px;
background: var(--l2-background);
color: var(--l2-foreground);
border: 1px solid var(--l1-border);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease-in-out;
}
.log-arrow-btn-up,
.log-arrow-btn-down {
background: var(--l2-background);
}
.log-arrow-btn:active,
.log-arrow-btn:focus {
background: var(--l3-background);
color: var(--l1-foreground);
}
.log-arrow-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
background: var(--l1-background);
color: var(--l3-foreground);
.log-arrow-btn:hover:not([disabled]) {
background: var(--l3-background);
color: var(--l1-foreground);
}
}
}

View File

@@ -6,14 +6,6 @@
gap: 8px;
}
.tooltipContent {
--tooltip-z-index: 2100;
}
.dropdownContent {
--dropdown-menu-content-z-index: 2100;
}
.leftSection {
display: flex;
align-items: center;

View File

@@ -1,8 +1,8 @@
import { Button } from '@signozhq/ui/button';
import { Divider } from '@signozhq/ui/divider';
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
import { Typography } from '@signozhq/ui/typography';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { toast } from '@signozhq/ui/sonner';
@@ -23,8 +23,6 @@ import { useCopyToClipboard } from 'react-use';
import styles from './LogDetailsHeader.module.scss';
const TOOLTIP_CONTENT_PROPS = { className: styles.tooltipContent };
interface LogDetailsHeaderProps {
log: ILog;
onNavigatePrev: () => void;
@@ -53,17 +51,19 @@ function LogDetailsHeader({
toast.success('Copied to clipboard', { position: 'bottom-right' });
};
const menuItems = [
const menuItems: DropdownItemType[] = [
{
key: 'copy-log',
type: 'item',
value: 'copy-log',
label: 'Copy log',
icon: <Copy size={14} />,
prefix: <Copy size={14} />,
onClick: handleCopyLog,
},
{
key: 'copy-link',
type: 'item',
value: 'copy-link',
label: 'Copy link to log',
icon: <Link size={14} />,
prefix: <Link size={14} />,
onClick: (): void => onLogCopy(),
},
];
@@ -91,6 +91,7 @@ function LogDetailsHeader({
<div className={styles.actions}>
{showOpenInExplorer && (
<Button
size="md"
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
@@ -100,51 +101,57 @@ function LogDetailsHeader({
</Button>
)}
<Dropdown
menu={{ items: menuItems }}
align="end"
className={styles.dropdownContent}
onClick={(e: MouseEvent): void => e.stopPropagation()}
>
<Dropdown items={menuItems} nativeButton align="end" side="bottom">
<Button
size="md"
variant="link"
color="secondary"
prefix={<Ellipsis size={16} />}
data-testid="log-details-header-menu"
/>
icon
aria-label="Log actions"
testId="log-details-header-menu"
onClick={(e: MouseEvent): void => e.stopPropagation()}
>
<Ellipsis size={16} />
</Button>
</Dropdown>
<div className={styles.arrows}>
<TooltipSimple
title="Move to previous log"
<Tooltip
title={isPrevDisabled ? undefined : 'Move to previous log'}
side="top"
open={isPrevDisabled ? false : undefined}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
>
<Button
disabledTooltip="No previous log"
size="md"
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
icon
aria-label="Move to previous log"
disabled={isPrevDisabled}
onClick={onNavigatePrev}
data-testid="log-details-header-prev"
/>
</TooltipSimple>
<TooltipSimple
title="Move to next log"
testId="log-details-header-prev"
>
<ChevronUp size={14} />
</Button>
</Tooltip>
<Tooltip
title={isNextDisabled ? undefined : 'Move to next log'}
side="top"
open={isNextDisabled ? false : undefined}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
>
<Button
disabledTooltip="No next log"
size="md"
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
icon
aria-label="Move to next log"
disabled={isNextDisabled}
onClick={onNavigateNext}
data-testid="log-details-header-next"
/>
</TooltipSimple>
testId="log-details-header-next"
>
<ChevronDown size={14} />
</Button>
</Tooltip>
</div>
</div>
</div>

View File

@@ -12,13 +12,6 @@
}
}
.valueBadge {
--badge-font-size: 13px;
box-sizing: border-box;
max-width: 100%;
min-width: 0;
}
// Truncating text inside a badge
.badgeText {
min-width: 0;
@@ -29,11 +22,14 @@
.serviceDot {
width: 6px;
min-width: 6px;
height: 6px;
min-height: 6px;
border-radius: 50%;
background: var(--accent-forest);
flex-shrink: 0;
margin-right: 4px;
display: inline-block;
}
.traceLink {

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { Badge, type BadgeColorType } from '@signozhq/ui/badge';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { ILog } from 'types/api/logs/log';
@@ -8,13 +8,13 @@ import styles from './LogHighlights.module.scss';
import TraceIdField from './TraceIdField';
// Severity badge color mirrors the LogStateIndicator bar
const SEVERITY_COLOR: Record<string, BadgeColor> = {
[LogType.TRACE]: 'forest',
[LogType.DEBUG]: 'aqua',
[LogType.INFO]: 'robin',
[LogType.WARN]: 'amber',
[LogType.ERROR]: 'cherry',
[LogType.FATAL]: 'sakura',
const SEVERITY_COLOR: Record<string, BadgeColorType> = {
[LogType.TRACE]: 'success',
[LogType.DEBUG]: 'info',
[LogType.INFO]: 'primary',
[LogType.WARN]: 'warning',
[LogType.ERROR]: 'danger',
[LogType.FATAL]: 'highlight-danger',
};
export interface LogHighlightConfig {
@@ -32,9 +32,13 @@ const getAttr = (log: ILog, key: string): string =>
const valueBadge = (
value: string,
options?: { prefix?: ReactNode; color?: BadgeColor },
options?: { prefix?: ReactNode; color?: BadgeColorType },
): ReactNode => (
<Badge color={options?.color ?? 'vanilla'} className={styles.valueBadge}>
<Badge
variant="solid"
color={options?.color ?? 'secondary'}
textTransform="none"
>
{options?.prefix}
<span className={styles.badgeText} title={value}>
{value}

View File

@@ -129,6 +129,18 @@ describe('LogDetail drawer — header', () => {
});
});
it('keeps the drawer open when a ⋯ menu item is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onClose = jest.fn();
renderDrawer({ onClose });
await user.click(screen.getByTestId('log-details-header-menu'));
await user.click(await screen.findByText('Copy log'));
expect(onClose).not.toHaveBeenCalled();
});
it('shows "Open in Explorer" when a handleOpenInExplorer handler is provided', () => {
renderDrawer({ handleOpenInExplorer: jest.fn() });
@@ -217,8 +229,14 @@ describe('LogDetail drawer — header', () => {
// Active log is the first one.
renderDrawer({ log: logs[0], logs, onNavigateLog });
expect(screen.getByTestId('log-details-header-prev')).toBeDisabled();
expect(screen.getByTestId('log-details-header-next')).toBeEnabled();
expect(screen.getByTestId('log-details-header-prev')).toHaveAttribute(
'aria-disabled',
'true',
);
expect(screen.getByTestId('log-details-header-next')).not.toHaveAttribute(
'aria-disabled',
'true',
);
await user.click(screen.getByTestId('log-details-header-next'));
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[1]);

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
@@ -86,7 +86,8 @@ function LogDetailInner({
target.closest('.cm-tooltip-autocomplete') ||
target.closest('.drawer-popover') ||
target.closest('.query-status-popover') ||
target.closest('[data-radix-popper-content-wrapper]')
target.closest('[data-radix-popper-content-wrapper]') ||
target.closest('[data-base-ui-portal]')
) {
return;
}
@@ -315,9 +316,12 @@ function LogDetailInner({
<div className="log-detail-drawer__section-divider" />
<div className="tabs-and-search">
<ToggleGroupSimple
<ToggleGroup
variant="outlined"
color="secondary"
size="sm"
type="single"
className="views-tabs"
testId="log-detail-views-tabs"
onChange={handleModeChange}
value={selectedView}
items={[
@@ -363,9 +367,12 @@ function LogDetailInner({
variant="link"
color="secondary"
size="sm"
prefix={<Filter size="lg" />}
icon
aria-label="Show Filters"
onClick={handleFilterVisible}
/>
>
<Filter size="lg" />
</Button>
</Tooltip>
)}
</div>

View File

@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import type { ReactElement, ReactNode } from 'react';
import {
Bold,
CodeXml,
@@ -11,16 +11,17 @@ import {
Type,
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { READ_ONLY_TOOLTIP } from './constants';
import InsertVariableMenu from './InsertVariableMenu';
import MarkdownHelp from './MarkdownHelp';
import type { EditorCommand, EditorVariable } from './types';
import styles from './MarkdownEditor.module.scss';
const COMMAND_ICONS: Record<string, ReactNode> = {
const COMMAND_ICONS: Record<string, ReactElement> = {
heading: <Heading size={14} />,
bold: <Bold size={14} />,
italic: <Italic size={14} />,
@@ -61,20 +62,22 @@ function EditorToolbar({
<span className={styles.toolbarDivider} />
<div className={styles.commands}>
{commands.map((command) => (
<TooltipSimple key={command.id} title={command.label}>
<Tooltip key={command.id} title={disabled ? undefined : command.label}>
<Button
disabledTooltip={READ_ONLY_TOOLTIP}
type="button"
variant="ghost"
color="secondary"
size="icon"
size="sm"
icon
disabled={disabled}
aria-label={command.label}
data-testid={`markdown-command-${command.id}`}
testId={`markdown-command-${command.id}`}
onClick={(): void => onRunCommand(command)}
>
{COMMAND_ICONS[command.id]}
</Button>
</TooltipSimple>
</Tooltip>
))}
</div>
<div className={styles.toolbarEnd}>

View File

@@ -1,8 +1,9 @@
import { useMemo, useState } from 'react';
import { ChevronDown, DollarSign, Search } from '@signozhq/icons';
import { ChevronDown, DollarSign } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple, type MenuItem } from '@signozhq/ui/dropdown-menu';
import { Dropdown, type DropdownActionItemType } from '@signozhq/ui/dropdown';
import { READ_ONLY_TOOLTIP } from './constants';
import type { EditorVariable } from './types';
import styles from './MarkdownEditor.module.scss';
@@ -17,9 +18,10 @@ interface InsertVariableMenuProps {
function toMenuItems(
variables: EditorVariable[],
onSelect: (name: string) => void,
): MenuItem[] {
): DropdownActionItemType[] {
return variables.map((variable) => ({
key: variable.name,
type: 'item',
value: variable.name,
label: (
<span
className={styles.variableRow}
@@ -60,15 +62,17 @@ function InsertVariableMenu({
}
return (
<DropdownMenuSimple
className={styles.variableMenu}
menu={{
items,
search: {
placeholder: 'Search variables',
searchIcon: <Search size={14} />,
onSearchChange: setSearch,
},
<Dropdown
items={items}
nativeButton
align="end"
side="bottom"
contentMaxWidth={320}
disabled={disabled}
disabledTooltip={READ_ONLY_TOOLTIP}
searchInputProps={{
placeholder: 'Search variables',
onChange: setSearch,
}}
>
<Button
@@ -76,15 +80,13 @@ function InsertVariableMenu({
variant="outlined"
color="secondary"
size="sm"
disabled={disabled}
prefix={<DollarSign size={14} className={styles.insertVariableIcon} />}
suffix={<ChevronDown size={14} />}
className={styles.insertVariable}
data-testid="markdown-insert-variable"
testId="markdown-insert-variable"
>
Insert variable
</Button>
</DropdownMenuSimple>
</Dropdown>
);
}

View File

@@ -74,27 +74,10 @@
margin-left: auto;
}
.insertVariable {
white-space: nowrap;
}
.insertVariableIcon {
color: var(--text-amber-400);
}
// The ui library's dropdown assumes a global border-box reset this app doesn't
// have (`box-sizing` is set on `body` only and doesn't inherit): its items are
// `width: 100%` + padding, so in the portal they lay out content-box and
// overflow the popup by the padding — clipping the flush-right badge.
.variableMenu,
.variableMenu * {
box-sizing: border-box;
}
.variableMenu {
width: 320px;
}
// Shrinkable, so a clamped popup truncates the name instead of clipping the
// badge at the content's `overflow: hidden` edge.
.variableRow {

View File

@@ -15,9 +15,10 @@ function MarkdownHelp(): JSX.Element {
type="button"
variant="ghost"
color="secondary"
size="icon"
size="sm"
icon
aria-label="Markdown syntax help"
data-testid="markdown-help-trigger"
testId="markdown-help-trigger"
>
<CircleHelp size={14} />
</Button>

View File

@@ -256,8 +256,14 @@ describe('MarkdownEditor', () => {
/>,
);
expect(screen.getByTestId('markdown-command-bold')).toBeDisabled();
expect(screen.getByTestId('markdown-insert-variable')).toBeDisabled();
expect(screen.getByTestId('markdown-command-bold')).toHaveAttribute(
'aria-disabled',
'true',
);
expect(screen.getByTestId('markdown-insert-variable')).toHaveAttribute(
'aria-disabled',
'true',
);
});
it('hides the insert-variable control when none are available', () => {

View File

@@ -1,6 +1,8 @@
// The body is persisted inline in the dashboard JSON, so its length is capped.
export const MARKDOWN_MAX_LENGTH = 16000;
export const READ_ONLY_TOOLTIP = 'The editor is read-only';
/** The canonical syntax; the renderer resolves the other three too. */
export const formatVariableToken = (name: string): string => `$${name}`;

View File

@@ -95,7 +95,6 @@
font-weight: var(--uppercase-small-500-font-weight);
line-height: 100%;
letter-spacing: 0.44px;
text-transform: uppercase;
}
}
}

View File

@@ -55,14 +55,14 @@ function NameEmailCell({
function StatusBadge({ status }: { status: MemberRow['status'] }): JSX.Element {
if (status === MemberStatus.Active) {
return (
<Badge color="forest" variant="outline">
<Badge color="success" variant="outlined">
ACTIVE
</Badge>
);
}
if (status === MemberStatus.Deleted) {
return (
<Badge color="cherry" variant="outline">
<Badge color="danger" variant="outlined">
DELETED
</Badge>
);
@@ -70,13 +70,17 @@ function StatusBadge({ status }: { status: MemberRow['status'] }): JSX.Element {
if (status === MemberStatus.Invited) {
return (
<Badge color="amber" variant="outline">
<Badge color="warning" variant="outlined">
INVITED
</Badge>
);
}
return <Badge color="vanilla">⎯</Badge>;
return (
<Badge variant="solid" color="secondary">
⎯
</Badge>
);
}
function MembersEmptyState({

View File

@@ -20,7 +20,7 @@ import {
import { Color } from '@signozhq/design-tokens';
import { Button, Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { TooltipProvider, TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip/TextToolTip';
@@ -758,9 +758,14 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
tabIndex={isActive ? 0 : -1}
>
<Checkbox
color="primary"
value={isSelected}
className="option-checkbox"
onClick={(e): void => selectFromButton(e, 'checkbox')}
width="100%"
onChange={(): void => {
handleItemSelection('checkbox');
setActiveChipIndex(-1);
setActiveIndex(-1);
}}
>
<div className="option-content">
<Typography.Text truncate={1} className="option-label-text">
@@ -1595,7 +1600,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}}
>
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Checkbox value={allOptionsSelected} className="option-checkbox">
<Checkbox color="primary" value={allOptionsSelected} width="100%">
<div className="option-content">
<div className="all-option-text">ALL</div>
</div>
@@ -1973,13 +1978,9 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
// `label` arrives already cut to maxTagTextLength, so the reveal reads the
// option's own text (falling back to the raw value for freeform tags).
return (
<TooltipSimple
side="top"
delayDuration={300}
title={findOptionLabelText(options, value)}
>
<Tooltip side="top" title={findOptionLabelText(options, value)}>
{tag}
</TooltipSimple>
</Tooltip>
);
}
@@ -2015,56 +2016,51 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
return (
// Self-provided so the per-tag tooltips work wherever this select is rendered,
// without every consumer having to sit under an app-level provider.
<TooltipProvider>
<div
className={cx('custom-multiselect-wrapper', {
'all-selected': allOptionShown || isAllSelected,
<div
className={cx('custom-multiselect-wrapper', {
'all-selected': allOptionShown || isAllSelected,
})}
>
{(allOptionShown || isAllSelected) && !searchText && (
<div className="all-text">ALL</div>
)}
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
>
{(allOptionShown || isAllSelected) && !searchText && (
<div className="all-text">ALL</div>
)}
<Select
ref={selectRef}
className={cx('custom-multiselect', className, {
'has-selection': selectedChips.length > 0 && !isAllSelected,
'is-all-selected': isAllSelected,
})}
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={(newValue): void => {
handleInternalChange(newValue, false);
}}
onClear={onClearHandler}
onDropdownVisibleChange={handleDropdownVisibleChange}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<ChevronDown style={{ cursor: 'default' }} size="md" />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx(
'custom-multiselect-dropdown-container',
popupClassName,
)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? undefined : maxTagCount}
{...rest}
/>
</div>
</TooltipProvider>
placeholder={placeholder}
mode="multiple"
showSearch
filterOption={false}
onSearch={handleSearch}
value={displayValue}
onChange={(newValue): void => {
handleInternalChange(newValue, false);
}}
onClear={onClearHandler}
onDropdownVisibleChange={handleDropdownVisibleChange}
open={isOpen}
defaultActiveFirstOption={defaultActiveFirstOption}
popupMatchSelectWidth={dropdownMatchSelectWidth}
allowClear={allowClear}
getPopupContainer={getPopupContainer ?? popupContainer}
suffixIcon={<ChevronDown style={{ cursor: 'default' }} size="md" />}
dropdownRender={customDropdownRender}
menuItemSelectedIcon={null}
popupClassName={cx('custom-multiselect-dropdown-container', popupClassName)}
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
onKeyDown={handleKeyDown}
tagRender={tagRender as any}
placement={placement}
listHeight={300}
searchValue={searchText}
maxTagTextLength={maxTagTextLength}
maxTagCount={isAllSelected ? undefined : maxTagCount}
{...rest}
/>
</div>
);
};

View File

@@ -1,6 +1,5 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import CustomMultiSelect from '../CustomMultiSelect';
@@ -14,15 +13,13 @@ const SELECTED = ['checkout-service-prod', 'payments-service-prod'];
function renderSelect(): void {
render(
<TooltipProvider>
<CustomMultiSelect
options={OPTIONS}
value={SELECTED}
maxTagCount={1}
maxTagTextLength={10}
maxTagPlaceholder={(omitted): string => `+${omitted.length}`}
/>
</TooltipProvider>,
<CustomMultiSelect
options={OPTIONS}
value={SELECTED}
maxTagCount={1}
maxTagTextLength={10}
maxTagPlaceholder={(omitted): string => `+${omitted.length}`}
/>,
);
}

View File

@@ -498,114 +498,95 @@ $custom-border-color: #2c3044;
margin-bottom: 8px;
}
.option-checkbox {
.all-option-text {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: default;
}
// The checkbox button is the only pointer target on the left; the label
// still toggles on click but keeps a default cursor.
> button {
cursor: pointer;
}
.option-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
// @signozhq/ui Checkbox renders children inside a <label> that is
// content-sized by default. Make it fill the row (min-width: 0 lets it
// shrink) so the option text below can truncate instead of overflowing.
> label {
.option-label-text {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.all-option-text {
.option-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
background-color: $custom-border-color;
color: var(--l2-foreground);
margin-left: 8px;
}
}
// "Only"/"All" is the primary action — a filled pill that reads as a
// button; "Toggle" is a secondary hint in plain text. Sized to the row's
// resting height so revealing them on hover never shifts it.
.only-btn,
.toggle-btn {
display: none;
align-items: center;
justify-content: center;
height: 18px;
min-height: 0;
font-size: 12px;
line-height: 1;
box-shadow: none;
}
.only-btn {
padding: 4px 8px;
// Black interior + a visible border so the pill stands out clearly
// against the near-black row when revealed on hover.
border: 1px solid var(--l3-border);
border-radius: 3px;
background-color: var(--bg-ink-500, #0b0c0e);
color: var(--l1-foreground);
cursor: pointer;
}
.toggle-btn {
padding: 0 6px;
border: none;
background-color: transparent;
color: var(--l2-foreground);
cursor: pointer;
}
// Toggle appears over the checkbox area; "Only/All" takes over the row
// content and hides Toggle there (higher specificity wins).
&:hover {
.toggle-btn {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.option-content {
.option-badge {
display: none;
}
}
.option-content:hover {
.only-btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.option-label-text {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.option-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
background-color: $custom-border-color;
color: var(--l2-foreground);
margin-left: 8px;
}
}
// "Only"/"All" is the primary action — a filled pill that reads as a
// button; "Toggle" is a secondary hint in plain text. Sized to the row's
// resting height so revealing them on hover never shifts it.
.only-btn,
.toggle-btn {
display: none;
align-items: center;
justify-content: center;
height: 18px;
min-height: 0;
font-size: 12px;
line-height: 1;
box-shadow: none;
}
.only-btn {
padding: 4px 8px;
// Black interior + a visible border so the pill stands out clearly
// against the near-black row when revealed on hover.
border: 1px solid var(--l3-border);
border-radius: 3px;
background-color: var(--bg-ink-500, #0b0c0e);
color: var(--l1-foreground);
cursor: pointer;
}
.toggle-btn {
padding: 0 6px;
border: none;
background-color: transparent;
color: var(--l2-foreground);
cursor: pointer;
}
// Toggle appears over the checkbox area; "Only/All" takes over the row
// content and hides Toggle there (higher specificity wins).
&:hover {
.toggle-btn {
display: flex;
}
.option-badge {
display: none;
}
}
.option-content:hover {
.only-btn {
display: flex;
}
.toggle-btn {
display: none;
}
.option-badge {
display: none;
}
.option-badge {
display: none;
}
}
}

View File

@@ -513,8 +513,4 @@
color: var(--l2-foreground) !important;
}
}
.query-actions-dropdown {
cursor: pointer;
}
}

View File

@@ -1,19 +1,19 @@
.query-add-ons {
width: 100%;
--toggle-group-secondary-bg: var(
--toggle-group-secondary-outlined-background: var(
--query-builder-v2-toggle-group-background-color,
var(--l1-background-hover)
);
--toggle-group-secondary-border: var(
--toggle-group-border: var(
--query-builder-v2-toggle-group-border-color,
var(--l2-border)
);
--toggle-group-secondary-active-bg: var(
--toggle-group-secondary-outlined-background-active: var(
--query-builder-v2-toggle-group-active-background-color,
var(--l1-background)
);
--toggle-group-secondary-bg-hover: var(
--toggle-group-secondary-outlined-background-hover: var(
--query-builder-v2-toggle-group-background-color-hover,
var(--l2-background)
);
@@ -36,51 +36,6 @@
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;
flex-wrap: wrap;
.add-on-tab-title {
display: flex;
gap: var(--margin-2);
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--query-builder-v2-color, var(--l2-foreground));
}
> button {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid
var(--query-builder-v2-border-color, var(--l2-border));
}
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
}
&[data-state='on'] {
color: var(--text-robin-500);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
display: none;
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
}
}
}
}
.compass-button {
width: 30px;
height: 30px;

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import cx from 'classnames';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
@@ -562,9 +562,12 @@ function QueryAddOns({
</div>
)}
<ToggleGroupSimple
<ToggleGroup
variant="outlined"
color="secondary"
size="sm"
type="multiple"
className="add-ons-tabs"
allowClear
value={selectedViews.map((view) => view.key)}
onChange={(newKeys: string[]): void => {
const oldKeys: string[] = selectedViews.map((view) => view.key);

View File

@@ -724,26 +724,62 @@ function QuerySearch({
// Helper function to render a badge for the current context mode
const renderContextBadge = (): JSX.Element => {
if (!editingMode) {
return <Badge color="vanilla">Unknown</Badge>;
return (
<Badge textTransform="none" variant="solid" color="secondary">
Unknown
</Badge>
);
}
switch (editingMode) {
case 'key':
return <Badge color="robin">Key</Badge>;
return (
<Badge textTransform="none" variant="solid" color="primary">
Key
</Badge>
);
case 'operator':
return <Badge color="sakura">Operator</Badge>;
return (
<Badge textTransform="none" variant="solid" color="highlight-danger">
Operator
</Badge>
);
case 'value':
return <Badge color="forest">Value</Badge>;
return (
<Badge textTransform="none" variant="solid" color="success">
Value
</Badge>
);
case 'conjunction':
return <Badge color="amber">Conjunction</Badge>;
return (
<Badge textTransform="none" variant="solid" color="warning">
Conjunction
</Badge>
);
case 'function':
return <Badge color="aqua">Function</Badge>;
return (
<Badge textTransform="none" variant="solid" color="info">
Function
</Badge>
);
case 'parenthesis':
return <Badge color="sakura">Parenthesis</Badge>;
return (
<Badge textTransform="none" variant="solid" color="highlight-danger">
Parenthesis
</Badge>
);
case 'bracketList':
return <Badge color="cherry">Bracket List</Badge>;
return (
<Badge textTransform="none" variant="solid" color="danger">
Bracket List
</Badge>
);
default:
return <Badge color="vanilla">Unknown</Badge>;
return (
<Badge textTransform="none" variant="solid" color="secondary">
Unknown
</Badge>
);
}
};
@@ -1465,27 +1501,45 @@ function QuerySearch({
Currently editing: {renderContextBadge()}
{queryContext?.keyToken && (
<span className="triplet-info">
Key: <Badge color="vanilla">{queryContext.keyToken}</Badge>
Key:{' '}
<Badge textTransform="none" variant="solid" color="secondary">
{queryContext.keyToken}
</Badge>
</span>
)}
{queryContext?.operatorToken && (
<span className="triplet-info">
Operator: <Badge color="vanilla">{queryContext.operatorToken}</Badge>
Operator:{' '}
<Badge textTransform="none" variant="solid" color="secondary">
{queryContext.operatorToken}
</Badge>
</span>
)}
{queryContext?.valueToken && (
<span className="triplet-info">
Value: <Badge color="vanilla">{queryContext.valueToken}</Badge>
Value:{' '}
<Badge textTransform="none" variant="solid" color="secondary">
{queryContext.valueToken}
</Badge>
</span>
)}
{queryContext?.currentPair && (
<span className="triplet-info query-pair-info">
Current pair: <Badge color="robin">{queryContext.currentPair.key}</Badge>
<Badge color="sakura">{queryContext.currentPair.operator}</Badge>
Current pair:{' '}
<Badge textTransform="none" variant="solid" color="primary">
{queryContext.currentPair.key}
</Badge>
<Badge textTransform="none" variant="solid" color="highlight-danger">
{queryContext.currentPair.operator}
</Badge>
{queryContext.currentPair.value && (
<Badge color="forest">{queryContext.currentPair.value}</Badge>
<Badge textTransform="none" variant="solid" color="success">
{queryContext.currentPair.value}
</Badge>
)}
<Badge
textTransform="none"
variant="solid"
color={queryContext.currentPair.isComplete ? 'success' : 'warning'}
>
{queryContext.currentPair.isComplete ? 'Complete' : 'Incomplete'}
@@ -1495,7 +1549,9 @@ function QuerySearch({
{queryContext?.queryPairs && queryContext.queryPairs.length > 0 && (
<span className="triplet-info">
Total pairs:{' '}
<Badge color="robin">{queryContext.queryPairs.length}</Badge>
<Badge variant="solid" color="primary">
{queryContext.queryPairs.length}
</Badge>
</span>
)}
</div>

View File

@@ -0,0 +1,229 @@
import { EditorView } from '@codemirror/view';
import { userEvent, waitFor, within } from 'storybook/test';
/** Suggestions wait on a 300ms debounce and a fetch, past the 1s default. */
const untilLoaded = { timeout: 15_000 };
/** Throws until `found` holds something, which is what `waitFor` retries on. */
const present = <TValue>(
found: TValue | null | undefined,
what: string,
): TValue => {
if (found === null || found === undefined) {
throw new Error(`${what} not found`);
}
return found;
};
const suggestionList = (canvasElement: HTMLElement): HTMLElement | null =>
canvasElement.querySelector<HTMLElement>('.cm-tooltip-autocomplete');
const suggestionRow = (
canvasElement: HTMLElement,
text: string,
): HTMLElement | undefined => {
const list = suggestionList(canvasElement);
return list
? within(list)
.queryAllByRole('option')
.find((option) => option.textContent?.includes(text))
: undefined;
};
/** Ctrl+Space, the editor's own shortcut for asking for suggestions. */
const requestSuggestions = (editor: HTMLElement): void => {
editor.dispatchEvent(
new KeyboardEvent('keydown', {
key: ' ',
code: 'Space',
ctrlKey: true,
bubbles: true,
}),
);
};
const currentView = (
canvasElement: HTMLElement,
): { editor: HTMLElement; view: EditorView } => {
// An explorer renders one editor per query; the first is the one on screen.
const editor = present(
canvasElement.querySelector<HTMLElement>(
'.code-mirror-where-clause .cm-content',
),
'filter editor',
);
return {
editor,
view: present(EditorView.findFromDOM(editor), 'editor view'),
};
};
/**
* The explorers wrap the filter in `OverlayScrollbar`, which initialises when
* the browser is idle. Initialising moves the content, the editor with it, and
* focuses the editor again through the DOM, which puts the caret back at the
* start and swaps the suggestions for the key list. Throws until every wrapper
* around the filter has initialised.
*/
const assertScrollbarsReady = (editor: HTMLElement): void => {
for (
let wrapper = editor.closest('.overlay-scrollbar');
wrapper;
wrapper = wrapper.parentElement?.closest('.overlay-scrollbar') ?? null
) {
if (!wrapper.hasAttribute('data-overlayscrollbars')) {
throw new Error('scrollbars around the filter still initialising');
}
}
};
/**
* Waits until `text` shows in the suggestion list, asking for suggestions
* whenever the list is shut. Focus and typing only open it once the keys have
* loaded, and moving the caret never does.
*/
const waitForSuggestion = (
canvasElement: HTMLElement,
text: string,
): Promise<HTMLElement> =>
waitFor(
() => {
const { editor } = currentView(canvasElement);
if (!suggestionList(canvasElement)) {
requestSuggestions(editor);
}
return present(suggestionRow(canvasElement, text), `suggestion "${text}"`);
},
{ ...untilLoaded, interval: 250 },
);
/**
* Focuses the filter once the scrollbars around it have initialised, and waits
* for its suggestion list.
*/
const focusFilter = async (canvasElement: HTMLElement): Promise<EditorView> => {
await waitFor(
() => {
const { editor, view } = currentView(canvasElement);
assertScrollbarsReady(editor);
if (!view.hasFocus) {
view.focus();
}
if (!suggestionList(canvasElement)) {
requestSuggestions(editor);
}
return present(suggestionList(canvasElement), 'suggestion list');
},
{ ...untilLoaded, interval: 250 },
);
return currentView(canvasElement).view;
};
/** Waits for a row of the suggestion list. */
export const findSuggestion = (
canvasElement: HTMLElement,
text: string,
): Promise<HTMLElement> => waitForSuggestion(canvasElement, text);
/** Focuses the empty filter: every key, with any recent filters above them. */
export const openKeySuggestions = async (
canvasElement: HTMLElement,
row: string,
): Promise<void> => {
await focusFilter(canvasElement);
await waitForSuggestion(canvasElement, row);
};
/**
* Focuses the filter and types onto the end of it one character at a time,
* each as the transaction a keystroke makes, leaving the caret at the end so
* the suggestion list follows what was typed. Quotes and brackets are not
* closed for it: type both.
*
* `userEvent.type` cannot be used: CodeMirror redraws the line as tokens are
* highlighted, which strands the caret `userEvent` tracks.
*/
export const typeFilter = async (
canvasElement: HTMLElement,
text: string,
): Promise<void> => {
const view = await focusFilter(canvasElement);
for (const character of text) {
const at = view.state.doc.length;
view.dispatch({
changes: { from: at, insert: character },
selection: { anchor: at + character.length },
userEvent: 'input.type',
});
}
};
/**
* Types an expression, then steps the caret back inside it, before a closing
* bracket or parenthesis, where the suggestions are about what goes in there.
*/
export const typeFilterWithCaretBack = async (
canvasElement: HTMLElement,
text: string,
stepsBack: number,
row: string,
): Promise<void> => {
await typeFilter(canvasElement, text);
const { view } = currentView(canvasElement);
view.dispatch({
selection: { anchor: view.state.doc.length - stepsBack },
userEvent: 'select',
});
await waitForSuggestion(canvasElement, row);
};
/**
* Moves focus off the filter, which is when the expression is validated and
* the error marker can show.
*/
export const blurFilter = async (canvasElement: HTMLElement): Promise<void> => {
await userEvent.keyboard('{Escape}');
await userEvent.click(canvasElement.ownerDocument.body);
};
/** Types an expression, leaves the filter and opens its validation errors. */
export const showFilterErrors = async (
canvasElement: HTMLElement,
text: string,
): Promise<void> => {
await typeFilter(canvasElement, text);
await blurFilter(canvasElement);
const marker = await waitFor(
() =>
present(
canvasElement.querySelector<HTMLElement>('.query-status-container button'),
'error marker',
),
untilLoaded,
);
await userEvent.hover(marker);
await waitFor(
() =>
present(
canvasElement.ownerDocument.querySelector('.query-validation-error'),
'validation error',
),
untilLoaded,
);
};

View File

@@ -6,7 +6,7 @@ import {
useMemo,
useState,
} from 'react';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -91,6 +91,26 @@ export const QueryV2 = forwardRef(function QueryV2(
cloneQuery('query', query);
};
const queryActionItems: DropdownItemType[] = [
{
type: 'item',
label: 'Clone',
value: 'clone-query',
prefix: <Copy size={14} />,
onClick: handleCloneEntity,
},
];
if (queriesCount && queriesCount > 1) {
queryActionItems.push({
type: 'item',
label: 'Delete',
value: 'delete-query',
prefix: <Trash size={14} />,
onClick: handleDeleteQuery,
});
}
const showReduceTo = useMemo(
() =>
dataSource === DataSource.METRICS &&
@@ -224,32 +244,14 @@ export const QueryV2 = forwardRef(function QueryV2(
)}
{isMultiQueryAllowed && (
<DropdownMenuSimple
className="query-actions-dropdown"
menu={{
items: [
{
label: 'Clone',
key: 'clone-query',
icon: <Copy size={14} />,
onClick: handleCloneEntity,
},
...(queriesCount && queriesCount > 1
? [
{
label: 'Delete',
key: 'delete-query',
icon: <Trash size={14} />,
onClick: handleDeleteQuery,
},
]
: []),
],
}}
<Dropdown
items={queryActionItems}
nativeButton={false}
align="end"
side="bottom"
>
<Ellipsis size={16} />
</DropdownMenuSimple>
</Dropdown>
)}
</div>
</div>

View File

@@ -303,6 +303,29 @@ describe('QueryAddOns', () => {
);
});
it('closes the only open add-on when its tab is clicked again', async () => {
const user = userEvent.setup();
render(
<QueryAddOns
query={baseQuery()}
version="v5"
isRawQuery={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
isForTraceOperator={false}
/>,
);
const legendTab = screen.getByTestId('query-add-on-legend_format');
await user.click(legendTab);
expect(screen.getByTestId('legend-format-content')).toBeInTheDocument();
await user.click(legendTab);
expect(screen.queryByTestId('legend-format-content')).not.toBeInTheDocument();
});
it('does not override existing legend when enabling Legend format', async () => {
const user = userEvent.setup();
const query = baseQuery({

View File

@@ -146,7 +146,7 @@
}
// Hovering the checkbox reveals the "Toggle" action.
.check-box:hover ~ .checkbox-value-section .toggle-btn {
[data-slot='checkbox']:hover ~ .checkbox-value-section .toggle-btn {
display: flex;
opacity: 1;
transform: translateX(0);

View File

@@ -18,7 +18,9 @@ import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
import useActiveQueryIndex from 'components/QuickFilters/hooks/useActiveQueryIndex';
import useCheckboxDisclosure from './useCheckboxDisclosure';
import useCheckboxFilterActions from './useCheckboxFilterActions';
import useCheckboxFilterState from './useCheckboxFilterState';
import useCheckboxFilterState, {
FILTER_DISABLED_REASON,
} from './useCheckboxFilterState';
import useCheckboxFilterValues from './useCheckboxFilterValues';
import './Checkbox.styles.scss';
@@ -132,6 +134,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value={value}
checked={currentFilterState[value]}
disabled={isFilterDisabled}
disabledTooltip={FILTER_DISABLED_REASON}
title={filter.title}
onlyButtonLabel={
isSomeFilterPresentForCurrentAttribute

View File

@@ -2,12 +2,13 @@ import { Button } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
interface CheckboxValueRowProps {
value: string;
checked: boolean;
disabled: boolean;
disabledTooltip?: string;
title: string;
onlyButtonLabel: string;
customRendererForValue?: (value: string) => JSX.Element;
@@ -19,6 +20,7 @@ function CheckboxValueRow({
value,
checked,
disabled,
disabledTooltip,
title,
onlyButtonLabel,
customRendererForValue,
@@ -28,10 +30,11 @@ function CheckboxValueRow({
return (
<div className="value">
<Checkbox
color="primary"
disabledTooltip={disabledTooltip}
onChange={(isChecked): void => onCheckboxChange(isChecked === true)}
value={checked}
disabled={disabled}
className="check-box"
/>
<div
@@ -47,11 +50,11 @@ function CheckboxValueRow({
{customRendererForValue ? (
customRendererForValue(value)
) : (
<TooltipSimple title={String(value)} side="top" align="start">
<Tooltip title={String(value)} side="top" align="start">
<Typography.Text className="value-string" truncate={1}>
{String(value)}
</Typography.Text>
</TooltipSimple>
</Tooltip>
)}
<div className="value-actions">
<Button type="text" className="only-btn">
@@ -67,6 +70,7 @@ function CheckboxValueRow({
}
CheckboxValueRow.defaultProps = {
disabledTooltip: undefined,
customRendererForValue: undefined,
};

View File

@@ -17,6 +17,9 @@ interface UseCheckboxFilterStateReturn {
isMultipleValuesTrueForTheKey: boolean;
}
export const FILTER_DISABLED_REASON =
'This attribute is used more than once in the filter bar';
/**
* Reads the active query and derives the per-value checked state for this
* attribute, whether the filter is disabled (same key used more than once in

View File

@@ -38,6 +38,6 @@
.rightAction {
display: flex;
align-items: center;
gap: var(--spacing-1);
gap: var(--spacing-4);
flex-shrink: 0;
}

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Tooltip } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { ChevronDown, ChevronRight, Search, Undo2 } from '@signozhq/icons';
@@ -62,13 +62,7 @@ export function CheckboxFilterV2Header({
) : (
<ChevronRight size={13} cursor="pointer" />
)}
{isTitleTruncated ? (
<TooltipSimple title={title} delayDuration={400}>
{titleText}
</TooltipSimple>
) : (
titleText
)}
<Tooltip title={isTitleTruncated ? title : undefined}>{titleText}</Tooltip>
</section>
{isOpen && (
<section className={classNames(styles.rightAction, actionsClassName)}>

View File

@@ -3,6 +3,7 @@ import {
CheckedState,
} from 'components/QuickFilters/types';
import { FILTER_DISABLED_REASON } from '../useCheckboxFilterState';
import { CheckboxFilterV2ValueRow } from './CheckboxFilterV2ValueRow';
import { SectionDivider } from './SectionDivider';
import { Section } from './useSectionedValues';
@@ -84,6 +85,7 @@ export function CheckboxFilterV2Section(
value={value}
checkedState={checkedState}
disabled={isFilterDisabled}
disabledTooltip={FILTER_DISABLED_REASON}
title={filter.title}
badge={badge}
onlyButtonLabel={

View File

@@ -37,53 +37,31 @@
align-items: center;
justify-items: end;
--button-height: 21px;
--button-padding: var(--spacing-5);
// Stack badge / only / toggle in a single cell so the crossfade overlaps
// instead of laying them side-by-side mid-transition.
> * {
grid-area: 1 / 1;
}
}
.badge {
opacity: 1;
transition:
opacity 0.16s ease,
display 0.16s allow-discrete;
}
.onlyButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
> [data-action='badge'] {
opacity: 1;
transition:
opacity 0.16s ease,
display 0.16s allow-discrete;
}
}
.toggleButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
> [data-action='only'],
> [data-action='toggle'] {
display: none;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
}
}
@@ -94,19 +72,15 @@
color: var(--l3-foreground);
}
.onlyButton {
cursor: not-allowed;
color: var(--l3-foreground);
}
.toggleButton {
[data-action='only'],
[data-action='toggle'] {
cursor: not-allowed;
color: var(--l3-foreground);
}
}
.valueButton:hover {
.onlyButton {
[data-action='only'] {
display: flex;
opacity: 1;
transform: translateX(0);
@@ -117,14 +91,14 @@
}
}
.badge {
[data-action='badge'] {
display: none;
opacity: 0;
}
}
.checkbox:hover ~ .valueButton {
.toggleButton {
[data-action='toggle'] {
display: flex;
opacity: 1;
transform: translateX(0);
@@ -135,16 +109,14 @@
}
}
.badge {
[data-action='badge'] {
display: none;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.badge,
.onlyButton,
.toggleButton {
.actions > * {
transition: none;
}
}

View File

@@ -12,6 +12,7 @@ interface ValueRowProps {
value: string;
checkedState: CheckedState;
disabled: boolean;
disabledTooltip?: string;
title: string;
onlyButtonLabel: string;
customRendererForValue?: (value: string) => JSX.Element;
@@ -36,6 +37,7 @@ export function CheckboxFilterV2ValueRow({
value,
checkedState,
disabled,
disabledTooltip,
title,
onlyButtonLabel,
customRendererForValue,
@@ -54,6 +56,7 @@ export function CheckboxFilterV2ValueRow({
>
<div className={styles.checkbox}>
<Checkbox
disabledTooltip={disabledTooltip}
onChange={(isChecked): void =>
onCheckboxChange(isChecked === true, checkedState)
}
@@ -97,18 +100,18 @@ export function CheckboxFilterV2ValueRow({
<div className={styles.actions}>
{badge && (
<Badge
variant="outline"
variant="outlined"
color={badge.color}
className={styles.badge}
data-action="badge"
testId={`badge-${badge.key}`}
>
{badge.label}
</Badge>
)}
<Button variant="ghost" color="secondary" className={styles.onlyButton}>
<Button size="md" variant="ghost" color="secondary" data-action="only">
{onlyButtonLabel}
</Button>
<Button variant="ghost" color="secondary" className={styles.toggleButton}>
<Button size="md" variant="ghost" color="secondary" data-action="toggle">
Toggle
</Button>
</div>

View File

@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { CheckboxFilterV2Header } from '../CheckboxFilterV2Header';
@@ -158,11 +157,7 @@ describe('CheckboxFilterV2Header', () => {
it('shows the full name on hover when the title is truncated', async () => {
mockTitleWidths(200, 100);
const user = userEvent.setup();
render(
<TooltipProvider>
<CheckboxFilterV2Header {...defaultProps} />
</TooltipProvider>,
);
render(<CheckboxFilterV2Header {...defaultProps} />);
await user.hover(screen.getByText(defaultProps.title));
@@ -174,11 +169,7 @@ describe('CheckboxFilterV2Header', () => {
it('shows no tooltip when the title fits', async () => {
mockTitleWidths(100, 100);
const user = userEvent.setup();
render(
<TooltipProvider>
<CheckboxFilterV2Header {...defaultProps} />
</TooltipProvider>,
);
render(<CheckboxFilterV2Header {...defaultProps} />);
await user.hover(screen.getByText(defaultProps.title));

View File

@@ -64,7 +64,7 @@ describe('CheckboxFilterV2ValueRow', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
badge={{ key: 'related', label: 'Related', color: 'robin' }}
badge={{ key: 'related', label: 'Related', color: 'primary' }}
/>,
);

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