Compare commits

..

15 Commits

Author SHA1 Message Date
Abhi Kumar
9216bfdd0b test(query-builder): scope selectors to the query block and wait on own requests
Run waits on a request it started; panel-editor structural edits re-run
until the newest request matches, since their draft can lag the DOM.
2026-10-30 02:06:09 +05:30
Abhi Kumar
f238d227dd test(query-builder): cover trace operator, signal switching, query tabs and run/cancel
holdQueryRange ignores a request cancelled while held: there is nothing
left to answer.
2026-10-30 00:43:42 +05:30
Abhi Kumar
6c2fdde9d1 test(query-builder): cover panel-kind add-ons, reduce-to, multi-query and formulas 2026-10-30 00:02:45 +05:30
Abhi Kumar
e680b369ff test(query-builder): cover group by, having, order, limit and legend add-ons
Negative limit stays fixme: the input accepts it and the backend
rejects it with a 400.
2026-10-29 23:39:38 +05:30
Abhi Kumar
6f9e44d8b5 test(query-builder): cover metric-type defaults, aggregations and step interval
Gauge and histogram metrics are seeded per worker: the golden dataset
only has sums.
2026-10-29 23:31:19 +05:30
Abhi Kumar
4265e484ac test(query-builder): cover explorer-to-dashboard and alert trace operator round trips
pickChannelByName re-clicks while the select is still closed: a condition
re-render could close it as the click opened it.
2026-10-29 23:12:35 +05:30
Abhi Kumar
64c9eb2bf7 test(query-builder): cover legacy conversion, history, IN lists and quick filters
Legacy having stays fixme: convertHavingToExpression keeps the stored
COUNT() casing and the v5 backend rejects it with a 400.
2026-10-29 22:49:10 +05:30
Abhi Kumar
f9d0911947 test(query-builder): add qb driver with url and filter editor specs
runShortcut waits two frames: a keypress in the same frame as the last
keystroke reads the pre-commit React state, which no user can produce.
2026-10-29 22:27:38 +05:30
Abhi Kumar
59c9d28a39 chore(query-builder): add data-testids for e2e selectors
Block roots are keyed by query name (query-A, formula-F1) so child ids
stay unindexed and survive reordering.
2026-10-29 19:44:55 +05:30
Naman Verma
5097d1578f chore: preemptively update rules v2 schema's api spec properties (#13306)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description

Rules v1 to v2 migration is gonna happen soon, so its better to have at
least our api spec updated so that agents stop creating v1 rules
2026-10-29 09:29:02 +00:00
Aditya Singh
5b5b9794f9 chore(saved-views): retire the old views pages, v1 client and the flag (#13258)
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
- The new saved views panel and the bottom strip are on for everyone.
This PR removes the old saved views code they replaced:
- the Views tabs and the saved views page on Logs, Traces, Metrics and
Meter
  - the `ExplorerCard` and `ExplorerOptions` bars
  - the v1 saved views client
  - the `SAVED_VIEW_ENABLED` flag
- Old views URLs still redirect to their explorer.
- The explorer URL now carries only `viewKey`. The app no longer writes
`viewName`.
- `ExplorerCard` no longer runs. Its header was hidden, but it still
fetched the v1 views list on every Logs, Traces and LLM explorer load.
- Stories now mock `/api/v2/saved_views`, and the old Views tab stories
are gone.

#### Issues closed by this PR

Closes https://github.com/SigNoz/events-pod/issues/77

#### Additional Information
- The dashboards list status bar and the side nav Noz entry stay. Both
are still live UI, and the status bar holds the only rail toggle.
- Noz no longer gets the view name in its page context. It still gets
the view id.
2026-10-28 14:30:39 +00:00
Jugal Kishore
1957e5a112 feat(onboarding): add Hono, Caddy, PM2, and GCP one-click datasources (#13291)
#### Description

- Add onboarding cards for Hono, Caddy, PM2, and GCP One-Click
Integration.
- GCP Integration card: add `logs` tag and log keywords, and route to
Integrations after onboarding.
- Add Hono, Caddy, and PM2 logos.

#### Issues closed by this PR

Closes SigNoz/signoz.io#4390
Closes SigNoz/signoz.io#4387
Closes SigNoz/signoz.io#4370
Closes SigNoz/signoz.io#4367
Closes SigNoz/signoz.io#4356
2026-10-28 13:42:02 +00:00
Nikhil Soni
18eb8ca849 feat(tracedetail): read waterfall/flamegraph attributes from JSON column (#13274)
#### Description

- `StorableSpan.Attributes()` prefers the flattened JSON column per span
and falls back to the legacy maps, so waterfall, flamegraph and thread
share one attribute bag (replaces the thread-only helper and
`AttributeValue`).
- Waterfall and flamegraph always read `attributes`; maps are blanked in
SQL for rows whose JSON is non-empty (`if(notEmpty(attributes), map(),
…)`), so dual-written spans ship attributes once on 10k/100k-span loads
while pre-rollout spans keep their maps.
- Attribute reads are no longer gated on `use_trace_attributes_json`, so
the waterfall store and unexported module helpers drop their unused
`orgID`.

#### Additional Information

- Requires the `attributes` column on `signoz_index_v3` (otel-collector
schema migration); without it waterfall/flamegraph error instead of
falling back.
- Flamegraph `GROUP BY`: the blanking `if` wraps `any(<map>)`, not the
reverse — `any(attributes) AS attributes` makes `attributes` resolve to
the aggregated alias, and nesting it is `ILLEGAL_AGGREGATION`.
- Thread reads `attributes` and the maps as-is (pages are capped at ~100
spans): its gen_ai filter reads `attributes_string` in WHERE, which a
blanking alias would shadow and drop dual-written spans.
- Supersedes #12798.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:05:43 +00:00
Ashwin Bhatkal
3b34a0d951 fix(notification-channels): match the relaxed channel checks and pick webhook auth with a radio (#13276)
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

- Drops the Google Chat, Jira and incident.io URL checks the API removed
in #13008, so a proxy or relay in front of those vendors can be saved.
- Webhook auth is now a None / Basic auth / Bearer token radio. Only the
chosen credentials are shown and sent, so a username plus a bearer token
can no longer reach the API and fail.
- A webhook username without a password is now allowed, matching the
API.
- The form now catches what the API still rejects: a Slack field without
a value, a Slack button without a URL or name, and an incident.io token
with a `Bearer ` prefix or surrounding spaces.
- Form errors are now nested by field path, so a Slack row error shows
under its row.

#### Issues closed by this PR

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


#### Screenshots / Screen Recordings

Visual diff of every `Pages/Alerts/Channels` story, dark and light,
against the merge-base:
https://claude.ai/artifact/GBULJmZFpzhe5bRviSnBoY

- 4 pairs changed: the webhook form on the new and edit pages.
- 16 new pairs, from 8 new stories.
- 125 of 148 pairs are pixel-identical.
- 3 pairs moved for reasons outside this change: a focus ring present in
one run only.
2026-10-28 11:23:55 +00:00
Abhi kumar
0036cda4f1 refactor(query-builder): drop stale bar-panel data-source override (#13218)
#### Description

- `QueryBuilderV2` pinned the provider's `initialDataSource` to metrics
for bar panels, a leftover from when bar charts were metrics-only. It
has no visible effect today, but implies bar panels are metrics-only.
- Removed it and the AI override that only existed to undo it; the AI
tab already passes a static traces config.
- Added tests for the provider config sync.

Closes https://github.com/SigNoz/pulse-pod/issues/603
2026-10-28 09:03:16 +00:00
208 changed files with 3830 additions and 5882 deletions

View File

@@ -46,9 +46,9 @@ One endpoint feeding several widgets stays one handler that reads the request. `
```ts
rest.get(
'http://localhost/api/v1/explorer/views',
'http://localhost/api/v2/saved_views',
response.json((req) =>
savedViews(values.savedViews, req.url.searchParams.get('sourcePage') ?? 'logs'),
savedViews(values.savedViews, req.url.searchParams.get('source') ?? 'logs'),
),
),
```

View File

@@ -11389,8 +11389,7 @@ components:
evaluation:
allOf:
- $ref: '#/components/schemas/RuletypesEvaluationEnvelope'
description: The evaluation window, as a kind and spec. Required when schemaVersion
is v2alpha1; derived from evalWindow and frequency for v1.
description: The evaluation window, as a kind and spec.
frequency:
description: How often the rule is evaluated, as a Go duration such as 1m.
Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set
@@ -11406,8 +11405,7 @@ components:
notificationSettings:
allOf:
- $ref: '#/components/schemas/RuletypesNotificationSettings'
description: Grouping, re-notification and routing behaviour. Required when
schemaVersion is v2alpha1; derived from the v1 fields otherwise.
description: Grouping, re-notification and routing behaviour.
preferredChannels:
description: The displayName of each notification channel to notify, for
schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries
@@ -11420,26 +11418,30 @@ components:
- $ref: '#/components/schemas/RuletypesRuleType'
description: 'The evaluation strategy: threshold_rule compares against thresholds,
promql_rule evaluates a PromQL expression, anomaly_rule scores deviation
from seasonal history. Defaults to threshold_rule for builder queries
and is forced to promql_rule for promql queries.'
from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql
queries and is forced to promql_rule for promql queries.'
schemaVersion:
description: 'The rule payload schema: v1 (the default when omitted) uses
the flat condition fields, v2alpha1 uses condition.thresholds, evaluation
and notificationSettings.'
description: The rule payload schema.
enum:
- v2alpha1
type: string
source:
deprecated: true
description: The URL of the page the rule was created from. Informational;
it is not used for evaluation.
type: string
version:
description: The query payload version. Only v5 is accepted and it is filled
in when omitted.
deprecated: true
description: The query payload version. Nothing reads it.
type: string
required:
- alert
- alertType
- ruleType
- condition
- evaluation
- schemaVersion
- notificationSettings
type: object
RuletypesPostableRuleView:
properties:
@@ -11528,8 +11530,7 @@ components:
evaluation:
allOf:
- $ref: '#/components/schemas/RuletypesEvaluationEnvelope'
description: The evaluation window, as a kind and spec. Required when schemaVersion
is v2alpha1; derived from evalWindow and frequency for v1.
description: The evaluation window, as a kind and spec.
frequency:
description: How often the rule is evaluated, as a Go duration such as 1m.
Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set
@@ -11548,8 +11549,7 @@ components:
notificationSettings:
allOf:
- $ref: '#/components/schemas/RuletypesNotificationSettings'
description: Grouping, re-notification and routing behaviour. Required when
schemaVersion is v2alpha1; derived from the v1 fields otherwise.
description: Grouping, re-notification and routing behaviour.
preferredChannels:
description: The displayName of each notification channel to notify, for
schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries
@@ -11562,14 +11562,15 @@ components:
- $ref: '#/components/schemas/RuletypesRuleType'
description: 'The evaluation strategy: threshold_rule compares against thresholds,
promql_rule evaluates a PromQL expression, anomaly_rule scores deviation
from seasonal history. Defaults to threshold_rule for builder queries
and is forced to promql_rule for promql queries.'
from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql
queries and is forced to promql_rule for promql queries.'
schemaVersion:
description: 'The rule payload schema: v1 (the default when omitted) uses
the flat condition fields, v2alpha1 uses condition.thresholds, evaluation
and notificationSettings.'
description: The rule payload schema.
enum:
- v2alpha1
type: string
source:
deprecated: true
description: The URL of the page the rule was created from. Informational;
it is not used for evaluation.
type: string
@@ -11584,8 +11585,8 @@ components:
updatedBy:
type: string
version:
description: The query payload version. Only v5 is accepted and it is filled
in when omitted.
deprecated: true
description: The query payload version. Nothing reads it.
type: string
required:
- id
@@ -11594,6 +11595,9 @@ components:
- alertType
- ruleType
- condition
- evaluation
- schemaVersion
- notificationSettings
type: object
RuletypesRuleCondition:
properties:
@@ -11643,8 +11647,7 @@ components:
against, hourly, daily or weekly.'
selectedQueryName:
description: The name of the query whose result is evaluated, such as A
or F1. Must match a query in compositeQuery. When omitted, F1 is used
if present, otherwise the enabled query with the highest name.
or F1. Must match a query in compositeQuery.
type: string
target:
description: 'schemaVersion v1 only: the value compared against, in targetUnit.
@@ -11660,10 +11663,11 @@ components:
thresholds:
allOf:
- $ref: '#/components/schemas/RuletypesRuleThresholdData'
description: The thresholds to evaluate, as a kind and spec. Required when
schemaVersion is v2alpha1; derived from op, target and matchType for v1.
description: The thresholds to evaluate, as a kind and spec.
required:
- compositeQuery
- selectedQueryName
- thresholds
type: object
RuletypesRuleThresholdData:
discriminator:
@@ -25312,13 +25316,13 @@ paths:
schemaVersion: v2alpha1
version: v5
metric_anomaly:
description: Anomaly rules are not yet supported under schemaVersion
v2alpha1, so this example uses the v1 shape. Wraps a builder query
in the `anomaly` function with daily seasonality SigNoz compares
each point against the forecast for that time of day. Fires when
the anomaly score stays below the threshold for the entire window;
`requireMinPoints` guards against noisy intervals.
summary: Metric anomaly rule (v1 only)
description: Wraps a builder query in the `anomaly` function with
daily seasonality, so SigNoz compares each point against the forecast
for that time of day. The threshold is compared against the signed
anomaly score, so `below` with a target of -2 fires when an abrupt
drop holds for the entire window; `requireMinPoints` guards against
noisy intervals.
summary: Metric anomaly rule
value:
alert: Anomalous drop in ingested spans
alertType: METRIC_BASED_ALERT
@@ -25353,22 +25357,36 @@ paths:
stepInterval: 21600
type: builder_query
queryType: builder
matchType: all_the_times
op: below
requireMinPoints: true
requiredNumPoints: 3
seasonality: daily
selectedQueryName: A
target: 2
thresholds:
kind: basic
spec:
- channels:
- slack-ingestion
matchType: all_the_times
name: warning
op: below
target: -2
description: Detect an abrupt drop in span ingestion using a z-score
anomaly function
evalWindow: 24h
frequency: 3h
evaluation:
kind: rolling
spec:
evalWindow: 24h
frequency: 3h
labels:
severity: warning
preferredChannels:
- slack-ingestion
notificationSettings:
renotify:
alertStates:
- firing
enabled: true
interval: 4h
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
@@ -26264,13 +26282,13 @@ paths:
schemaVersion: v2alpha1
version: v5
metric_anomaly:
description: Anomaly rules are not yet supported under schemaVersion
v2alpha1, so this example uses the v1 shape. Wraps a builder query
in the `anomaly` function with daily seasonality SigNoz compares
each point against the forecast for that time of day. Fires when
the anomaly score stays below the threshold for the entire window;
`requireMinPoints` guards against noisy intervals.
summary: Metric anomaly rule (v1 only)
description: Wraps a builder query in the `anomaly` function with
daily seasonality, so SigNoz compares each point against the forecast
for that time of day. The threshold is compared against the signed
anomaly score, so `below` with a target of -2 fires when an abrupt
drop holds for the entire window; `requireMinPoints` guards against
noisy intervals.
summary: Metric anomaly rule
value:
alert: Anomalous drop in ingested spans
alertType: METRIC_BASED_ALERT
@@ -26305,22 +26323,36 @@ paths:
stepInterval: 21600
type: builder_query
queryType: builder
matchType: all_the_times
op: below
requireMinPoints: true
requiredNumPoints: 3
seasonality: daily
selectedQueryName: A
target: 2
thresholds:
kind: basic
spec:
- channels:
- slack-ingestion
matchType: all_the_times
name: warning
op: below
target: -2
description: Detect an abrupt drop in span ingestion using a z-score
anomaly function
evalWindow: 24h
frequency: 3h
evaluation:
kind: rolling
spec:
evalWindow: 24h
frequency: 3h
labels:
severity: warning
preferredChannels:
- slack-ingestion
notificationSettings:
renotify:
alertStates:
- firing
enabled: true
interval: 4h
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
@@ -27117,13 +27149,13 @@ paths:
schemaVersion: v2alpha1
version: v5
metric_anomaly:
description: Anomaly rules are not yet supported under schemaVersion
v2alpha1, so this example uses the v1 shape. Wraps a builder query
in the `anomaly` function with daily seasonality SigNoz compares
each point against the forecast for that time of day. Fires when
the anomaly score stays below the threshold for the entire window;
`requireMinPoints` guards against noisy intervals.
summary: Metric anomaly rule (v1 only)
description: Wraps a builder query in the `anomaly` function with
daily seasonality, so SigNoz compares each point against the forecast
for that time of day. The threshold is compared against the signed
anomaly score, so `below` with a target of -2 fires when an abrupt
drop holds for the entire window; `requireMinPoints` guards against
noisy intervals.
summary: Metric anomaly rule
value:
alert: Anomalous drop in ingested spans
alertType: METRIC_BASED_ALERT
@@ -27158,22 +27190,36 @@ paths:
stepInterval: 21600
type: builder_query
queryType: builder
matchType: all_the_times
op: below
requireMinPoints: true
requiredNumPoints: 3
seasonality: daily
selectedQueryName: A
target: 2
thresholds:
kind: basic
spec:
- channels:
- slack-ingestion
matchType: all_the_times
name: warning
op: below
target: -2
description: Detect an abrupt drop in span ingestion using a z-score
anomaly function
evalWindow: 24h
frequency: 3h
evaluation:
kind: rolling
spec:
evalWindow: 24h
frequency: 3h
labels:
severity: warning
preferredChannels:
- slack-ingestion
notificationSettings:
renotify:
alertStates:
- firing
enabled: true
interval: 4h
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
@@ -28676,13 +28722,13 @@ paths:
schemaVersion: v2alpha1
version: v5
metric_anomaly:
description: Anomaly rules are not yet supported under schemaVersion
v2alpha1, so this example uses the v1 shape. Wraps a builder query
in the `anomaly` function with daily seasonality SigNoz compares
each point against the forecast for that time of day. Fires when
the anomaly score stays below the threshold for the entire window;
`requireMinPoints` guards against noisy intervals.
summary: Metric anomaly rule (v1 only)
description: Wraps a builder query in the `anomaly` function with
daily seasonality, so SigNoz compares each point against the forecast
for that time of day. The threshold is compared against the signed
anomaly score, so `below` with a target of -2 fires when an abrupt
drop holds for the entire window; `requireMinPoints` guards against
noisy intervals.
summary: Metric anomaly rule
value:
alert: Anomalous drop in ingested spans
alertType: METRIC_BASED_ALERT
@@ -28717,22 +28763,36 @@ paths:
stepInterval: 21600
type: builder_query
queryType: builder
matchType: all_the_times
op: below
requireMinPoints: true
requiredNumPoints: 3
seasonality: daily
selectedQueryName: A
target: 2
thresholds:
kind: basic
spec:
- channels:
- slack-ingestion
matchType: all_the_times
name: warning
op: below
target: -2
description: Detect an abrupt drop in span ingestion using a z-score
anomaly function
evalWindow: 24h
frequency: 3h
evaluation:
kind: rolling
spec:
evalWindow: 24h
frequency: 3h
labels:
severity: warning
preferredChannels:
- slack-ingestion
notificationSettings:
renotify:
alertStates:
- firing
enabled: true
interval: 4h
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL

View File

@@ -141,7 +141,7 @@ const preview: Preview = {
'Services',
['List', 'Detail', 'Top Level Operations', 'Service Map'],
'Logs',
['Explorer', 'Live Tail', 'Saved Views', 'Pipelines', 'Settings'],
['Explorer', 'Live Tail', 'Pipelines', 'Settings'],
'Traces',
['Explorer', 'Trace Details', 'Funnel Details'],
'Metrics',

View File

@@ -17,11 +17,9 @@
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
"tooltip_email_to": "Enter email addresses separated by commas.",
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to. It must be an https URL on chat.googleapis.com.",
"google_chat_webhook_url_invalid": "Webhook URL must be an https URL on chat.googleapis.com",
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
"field_jira_site": "Site URL",
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net. Only Jira Cloud is supported.",
"jira_site_invalid": "Site URL must be an https URL on an atlassian.net domain",
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
"jira_service_account_tip_link": "Learn how",
@@ -82,7 +80,6 @@
"field_incidentio_description": "Description",
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
"incidentio_required_fields": "Alert source URL and token are required",
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
"field_incidentio_metadata": "Additional metadata",
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
"placeholder_incidentio_metadata_key": "Key",
@@ -92,8 +89,8 @@
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",
"field_slack_description": "Description",
"field_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)",
"field_webhook_username": "Username",
"field_webhook_password": "Password",
"field_pager_routing_key": "Routing Key",
"field_pager_description": "Description",
"field_pager_severity": "Severity",
@@ -112,13 +109,10 @@
"help_pager_group": "A cluster or grouping of sources",
"help_pager_component": "The part or component of the affected system that is broke",
"help_pager_severity": "Severity of the incident, must be one of: must be one of the following: 'critical', 'warning', 'error' or 'info'",
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
"help_webhook_password": "Specify a password or bearer token",
"help_pager_description": "Shows up as description in pagerduty",
"channel_creation_done": "Successfully created the channel",
"channel_edit_done": "Channels Edited Successfully",
"selected_channel_invalid": "Channel type selected is invalid",
"username_no_password": "A Password must be provided with user name",
"channel_test_done": "An alert has been sent to this channel",
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",

View File

@@ -1,3 +0,0 @@
{
"name_of_the_view": "Name of the view"
}

View File

@@ -47,7 +47,6 @@
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",

View File

@@ -25,11 +25,9 @@
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
"tooltip_email_to": "Enter email addresses separated by commas.",
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to. It must be an https URL on chat.googleapis.com.",
"google_chat_webhook_url_invalid": "Webhook URL must be an https URL on chat.googleapis.com",
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to.",
"field_jira_site": "Site URL",
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net. Only Jira Cloud is supported.",
"jira_site_invalid": "Site URL must be an https URL on an atlassian.net domain",
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
"jira_service_account_tip_link": "Learn how",
@@ -90,7 +88,8 @@
"field_incidentio_description": "Description",
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
"incidentio_required_fields": "Alert source URL and token are required",
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
"incidentio_token_bearer_prefix": "Paste the token only, without the Bearer prefix",
"incidentio_token_whitespace": "Remove the spaces around the token",
"field_incidentio_metadata": "Additional metadata",
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
"placeholder_incidentio_metadata_key": "Key",
@@ -116,6 +115,7 @@
"field_slack_field_short": "Short",
"add_slack_field": "Add field",
"remove_slack_field": "Remove field",
"slack_field_value_required": "Enter a value for this field",
"field_slack_actions": "Actions",
"help_slack_actions": "Buttons rendered under the attachment. A button with a URL links out.",
"label_slack_action_text": "Text",
@@ -133,6 +133,7 @@
"placeholder_slack_action_style": "Style: default, primary or danger",
"placeholder_slack_action_confirm": "Confirmation prompt (optional)",
"add_slack_action": "Add action",
"slack_action_url_or_name_required": "Enter a URL or a name for this button",
"remove_slack_action": "Remove action",
"field_opsgenie_api_key": "API Key",
"field_opsgenie_description": "Description",
@@ -143,9 +144,15 @@
"help_email_html": "Send email in html format",
"field_email_html": "Email body template",
"placeholder_email_html": "Email body template",
"field_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)",
"field_webhook_bearer_token": "Bearer token (optional)",
"field_webhook_username": "Username",
"field_webhook_password": "Password",
"field_webhook_bearer_token": "Bearer token",
"field_webhook_auth": "Authentication",
"option_webhook_auth_none": "None",
"option_webhook_auth_basic": "Basic auth",
"option_webhook_auth_bearer": "Bearer token",
"webhook_basic_auth_required": "Enter a username or password",
"webhook_bearer_token_required": "Enter a bearer token",
"field_pager_routing_key": "Routing Key",
"field_pager_description": "Description",
"field_pager_severity": "Severity",
@@ -168,9 +175,7 @@
"help_pager_group": "A cluster or grouping of sources",
"help_pager_component": "The part or component of the affected system that is broke",
"help_pager_severity": "Severity of the incident, must be one of: must be one of the following: 'critical', 'warning', 'error' or 'info'",
"help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.",
"help_webhook_password": "Specify a password or bearer token",
"help_webhook_bearer_token": "Sent as an Authorization: Bearer header. Leave the username and password empty when using it.",
"help_webhook_bearer_token": "Sent as an Authorization: Bearer header.",
"help_pager_description": "Shows up as description in pagerduty",
"help_opsgenie_message": "Shows up as message in opsgenie",
"help_opsgenie_priority": "Priority of the incident",
@@ -178,7 +183,6 @@
"channel_creation_done": "Successfully created the channel",
"channel_edit_done": "Channels Edited Successfully",
"selected_channel_invalid": "Channel type selected is invalid",
"username_no_password": "A Password must be provided with user name",
"channel_test_done": "An alert has been sent to this channel",
"channel_test_failed": "Failed to send a test message to this channel, please confirm that the parameters are set correctly",
"channel_test_unexpected": "An unexpected error occurred while sending a message to this channel, please try again",

View File

@@ -1,4 +0,0 @@
{
"name_of_the_view": "Name of the view",
"delete_confirm_message": "Are you sure you want to delete {{viewName}} view? Deleting a view is irreversible and cannot be undone."
}

View File

@@ -50,8 +50,6 @@
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
"WORKSPACE_ACCESS_RESTRICTED": "SigNoz | Workspace Access Restricted",
"SUPPORT": "SigNoz | Support",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"TRACES_FUNNELS": "SigNoz | Traces Funnels",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts",
@@ -66,10 +64,8 @@
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
"API_MONITORING": "SigNoz | External APIs",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",

View File

@@ -303,10 +303,10 @@ describe('PrivateRoute', () => {
it.each([
['/logs-save-views', ROUTES.LOGS_EXPLORER],
['/traces-save-views', ROUTES.TRACES_EXPLORER],
[ROUTES.LOGS_SAVE_VIEWS, ROUTES.LOGS_EXPLORER],
[ROUTES.TRACES_SAVE_VIEWS, ROUTES.TRACES_EXPLORER],
[ROUTES.METRICS_EXPLORER_VIEWS, ROUTES.METRICS_EXPLORER_EXPLORER],
[ROUTES.METER_EXPLORER_VIEWS, ROUTES.METER_EXPLORER],
['/logs/saved-views', ROUTES.LOGS_EXPLORER],
['/traces/saved-views', ROUTES.TRACES_EXPLORER],
['/metrics-explorer/views', ROUTES.METRICS_EXPLORER_EXPLORER],
['/meter/explorer/views', ROUTES.METER_EXPLORER],
])('should redirect the old views route %s to %s', (from, to) => {
renderPrivateRoute({ initialRoute: from });

View File

@@ -18,7 +18,6 @@ import AppLayout from 'container/AppLayout';
import Hex from 'crypto-js/enc-hex';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -66,7 +65,6 @@ function App(): JSX.Element {
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isSavedViewEnabled = useSavedViewEnabled();
const chatSupport = useChatSupport();
const { hostname } = window.location;
@@ -256,18 +254,8 @@ function App(): JSX.Element {
useEffect(() => {
// The bottom strip carries Support, so the floating bubble goes entirely.
if (
isSavedViewEnabled ||
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/') ||
pathname === '/ai-assistant' ||
pathname.startsWith('/ai-assistant/')
) {
window.Pylon?.('hideChatBubble');
} else {
window.Pylon?.('showChatBubble');
}
}, [pathname, isSavedViewEnabled]);
window.Pylon?.('hideChatBubble');
}, [pathname]);
// Identity for the Pylon widget. Whether this user gets Pylon at all is
// `useChatSupport`'s call — this only fills in who they are.

View File

@@ -26,10 +26,6 @@ export const ServiceMapPage = Loadable(
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
);
export const LogsSaveViews = Loadable(
() => import(/* webpackChunkName: "LogsSaveViews" */ 'pages/LogsModulePage'), // TODO: Add a wrapper so that the same component can be used in traces
);
export const TracesExplorer = Loadable(
() =>
import(
@@ -37,11 +33,6 @@ export const TracesExplorer = Loadable(
),
);
export const TracesSaveViews = Loadable(
() =>
import(/* webpackChunkName: "Traces Save Views" */ 'pages/TracesModulePage'),
);
export const TracesFunnels = Loadable(
() =>
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),

View File

@@ -26,7 +26,6 @@ import {
Login,
Logs,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
@@ -48,7 +47,6 @@ import {
TracesExplorer,
TracesFunnelDetails,
TracesFunnels,
TracesSaveViews,
UnAuthorized,
UsageExplorerPage,
WorkspaceAccessRestricted,
@@ -120,13 +118,6 @@ const routes: AppRoutes[] = [
exact: true,
key: 'SERVICE_MAP',
},
{
path: ROUTES.LOGS_SAVE_VIEWS,
component: LogsSaveViews,
isPrivate: true,
exact: true,
key: 'LOGS_SAVE_VIEWS',
},
{
path: ROUTES.TRACE_DETAIL,
exact: true,
@@ -218,13 +209,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'TRACES_EXPLORER',
},
{
path: ROUTES.TRACES_SAVE_VIEWS,
exact: true,
component: TracesSaveViews,
isPrivate: true,
key: 'TRACES_SAVE_VIEWS',
},
{
path: ROUTES.TRACES_FUNNELS,
exact: true,
@@ -414,13 +398,6 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_VIEWS,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
exact: true,
@@ -443,13 +420,6 @@ const routes: AppRoutes[] = [
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorerPage,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.API_MONITORING,
exact: true,

View File

@@ -12584,6 +12584,12 @@ export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
*/
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
/**
* The rule payload schema.
*/
export enum RuletypesPostableRuleDTOSchemaVersion {
v2alpha1 = 'v2alpha1',
}
export enum RuletypesSeasonalityDTO {
hourly = 'hourly',
daily = 'daily',
@@ -12660,9 +12666,9 @@ export interface RuletypesRuleConditionDTO {
seasonality?: RuletypesSeasonalityDTO;
/**
* @type string
* @description The name of the query whose result is evaluated, such as A or F1. Must match a query in compositeQuery. When omitted, F1 is used if present, otherwise the enabled query with the highest name.
* @description The name of the query whose result is evaluated, such as A or F1. Must match a query in compositeQuery.
*/
selectedQueryName?: string;
selectedQueryName: string;
/**
* @type number,null
* @format double
@@ -12675,9 +12681,9 @@ export interface RuletypesRuleConditionDTO {
*/
targetUnit?: string;
/**
* @description The thresholds to evaluate, as a kind and spec. Required when schemaVersion is v2alpha1; derived from op, target and matchType for v1.
* @description The thresholds to evaluate, as a kind and spec.
*/
thresholds?: RuletypesRuleThresholdDataDTO;
thresholds: RuletypesRuleThresholdDataDTO;
}
export interface RuletypesPostableRuleDTO {
@@ -12715,9 +12721,9 @@ export interface RuletypesPostableRuleDTO {
*/
evalWindow?: string;
/**
* @description The evaluation window, as a kind and spec. Required when schemaVersion is v2alpha1; derived from evalWindow and frequency for v1.
* @description The evaluation window, as a kind and spec.
*/
evaluation?: RuletypesEvaluationEnvelopeDTO;
evaluation: RuletypesEvaluationEnvelopeDTO;
/**
* @type string
* @description How often the rule is evaluated, as a Go duration such as 1m. Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set it in evaluation.spec instead.
@@ -12729,23 +12735,24 @@ export interface RuletypesPostableRuleDTO {
*/
labels?: RuletypesPostableRuleDTOLabels;
/**
* @description Grouping, re-notification and routing behaviour. Required when schemaVersion is v2alpha1; derived from the v1 fields otherwise.
* @description Grouping, re-notification and routing behaviour.
*/
notificationSettings?: RuletypesNotificationSettingsDTO;
notificationSettings: RuletypesNotificationSettingsDTO;
/**
* @type array
* @description The displayName of each notification channel to notify, for schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries its own channels.
*/
preferredChannels?: string[];
/**
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder queries and is forced to promql_rule for promql queries.
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql queries and is forced to promql_rule for promql queries.
*/
ruleType: RuletypesRuleTypeDTO;
/**
* @enum v2alpha1
* @type string
* @description The rule payload schema: v1 (the default when omitted) uses the flat condition fields, v2alpha1 uses condition.thresholds, evaluation and notificationSettings.
* @description The rule payload schema.
*/
schemaVersion?: string;
schemaVersion: RuletypesPostableRuleDTOSchemaVersion;
/**
* @type string
* @description The URL of the page the rule was created from. Informational; it is not used for evaluation.
@@ -12753,7 +12760,7 @@ export interface RuletypesPostableRuleDTO {
source?: string;
/**
* @type string
* @description The query payload version. Only v5 is accepted and it is filled in when omitted.
* @description The query payload version. Nothing reads it.
*/
version?: string;
}
@@ -12776,6 +12783,12 @@ export type RuletypesRuleDTOAnnotations = { [key: string]: string };
*/
export type RuletypesRuleDTOLabels = { [key: string]: string };
/**
* The rule payload schema.
*/
export enum RuletypesRuleDTOSchemaVersion {
v2alpha1 = 'v2alpha1',
}
export interface RuletypesRuleDTO {
/**
* @type string
@@ -12820,9 +12833,9 @@ export interface RuletypesRuleDTO {
*/
evalWindow?: string;
/**
* @description The evaluation window, as a kind and spec. Required when schemaVersion is v2alpha1; derived from evalWindow and frequency for v1.
* @description The evaluation window, as a kind and spec.
*/
evaluation?: RuletypesEvaluationEnvelopeDTO;
evaluation: RuletypesEvaluationEnvelopeDTO;
/**
* @type string
* @description How often the rule is evaluated, as a Go duration such as 1m. Only read when schemaVersion is v1, defaulting to 1m; v2alpha1 rules set it in evaluation.spec instead.
@@ -12839,23 +12852,24 @@ export interface RuletypesRuleDTO {
*/
labels?: RuletypesRuleDTOLabels;
/**
* @description Grouping, re-notification and routing behaviour. Required when schemaVersion is v2alpha1; derived from the v1 fields otherwise.
* @description Grouping, re-notification and routing behaviour.
*/
notificationSettings?: RuletypesNotificationSettingsDTO;
notificationSettings: RuletypesNotificationSettingsDTO;
/**
* @type array
* @description The displayName of each notification channel to notify, for schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries its own channels.
*/
preferredChannels?: string[];
/**
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder queries and is forced to promql_rule for promql queries.
* @description The evaluation strategy: threshold_rule compares against thresholds, promql_rule evaluates a PromQL expression, anomaly_rule scores deviation from seasonal history. Defaults to threshold_rule for builder and clickhouse_sql queries and is forced to promql_rule for promql queries.
*/
ruleType: RuletypesRuleTypeDTO;
/**
* @enum v2alpha1
* @type string
* @description The rule payload schema: v1 (the default when omitted) uses the flat condition fields, v2alpha1 uses condition.thresholds, evaluation and notificationSettings.
* @description The rule payload schema.
*/
schemaVersion?: string;
schemaVersion: RuletypesRuleDTOSchemaVersion;
/**
* @type string
* @description The URL of the page the rule was created from. Informational; it is not used for evaluation.
@@ -12876,7 +12890,7 @@ export interface RuletypesRuleDTO {
updatedBy?: string;
/**
* @type string
* @description The query payload version. Only v5 is accepted and it is filled in when omitted.
* @description The query payload version. Nothing reads it.
*/
version?: string;
}

View File

@@ -1,5 +0,0 @@
import axios from 'api';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
axios.delete(`/explorer/views/${uuid}`);

View File

@@ -1,9 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = (
sourcepage: DataSource | 'meter',
): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@@ -1,24 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { ViewProps } from 'types/api/saveViews/types';
/**
* Fetches a single saved view by ID (`GET /api/v1/explorer/views/{viewId}`).
*
* Hand-maintained alongside the other `api/saveView/*` clients — explorer views
* are not in `docs/api/openapi.yml`, so Orval does not generate a hook here
* (unlike e.g. `useGetChannelByID` under `api/generated/services/channels`).
*
* Used by the AI assistant "Open view" action to load `compositeQuery` and
* navigate to the correct explorer without listing every view per source page.
* See `container/AIAssistant/components/ActionsSection/utils/openSavedView.ts`.
*/
export interface GetViewByIdProps {
status: string;
data: ViewProps;
}
export const getViewById = (
viewKey: string,
): Promise<AxiosResponse<GetViewByIdProps>> =>
axios.get(`/explorer/views/${viewKey}`);

View File

@@ -1,16 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
export const saveView = ({
compositeQuery,
sourcePage,
viewName,
extraData,
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
axios.post('/explorer/views', {
name: viewName,
sourcePage,
compositeQuery,
extraData,
});

View File

@@ -1,19 +0,0 @@
import axios from 'api';
import {
UpdateViewPayloadProps,
UpdateViewProps,
} from 'types/api/saveViews/types';
export const updateView = ({
compositeQuery,
viewName,
extraData,
sourcePage,
viewKey,
}: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
axios.put(`/explorer/views/${viewKey}`, {
name: viewName,
compositeQuery,
extraData,
sourcePage,
});

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#1F88C0" d="M11.094.47c-.842 0-1.696.092-2.552.288a11.37 11.37 0 0 0-4.87 2.423 10.632 10.632 0 0 0-2.36 2.826A10.132 10.132 0 0 0 .305 8.582c-.398 1.62-.4 3.336-.043 5.048.085.405.183.809.31 1.212a11.85 11.85 0 0 0 1.662 3.729 3.273 3.273 0 0 0-.086.427 3.323 3.323 0 0 0 2.848 3.71 3.279 3.279 0 0 0 1.947-.346c1.045.51 2.17.864 3.339 1.04a11.66 11.66 0 0 0 4.285-.155 11.566 11.566 0 0 0 4.936-2.485 10.643 10.643 0 0 0 2.352-2.894 11.164 11.164 0 0 0 1.356-4.424 11.214 11.214 0 0 0-.498-4.335c.175-.077.338-.175.486-.293a.444.444 89.992 0 0 .001 0c.402-.322.693-.794.777-1.342a2.146 2.146 0 0 0-1.79-2.434 2.115 2.115 0 0 0-1.205.171c-.038-.043-.078-.086-.113-.13a11.693 11.693 0 0 0-3.476-2.93 13.348 13.348 0 0 0-1.76-.81 13.55 13.55 0 0 0-2.06-.613A12.121 12.121 0 0 0 11.093.47Zm.714.328c.345-.004.688.01 1.028.042a9.892 9.892 0 0 1 2.743.639c.984.39 1.89.958 2.707 1.632.803.662 1.502 1.45 2.091 2.328.026.039.048.08.07.12a2.12 2.12 0 0 0-.435 2.646c-.158.114-.97.692-1.634 1.183-.414.308-.733.557-.733.557l.581.68s.296-.276.665-.638c.572-.562 1.229-1.233 1.395-1.403a2.122 2.122 0 0 0 1.907.677 11.229 11.229 0 0 1-.013 4.046 11.41 11.41 0 0 1-1.475 3.897 12.343 12.343 0 0 1-2.079 2.587c-1.19 1.125-2.633 2.022-4.306 2.531a10.826 10.826 0 0 1-3.973.484 11.04 11.04 0 0 1-3.057-.652 3.304 3.304 0 0 0 1.417-2.294 3.275 3.275 0 0 0-.294-1.842c.18-.162.403-.363.656-.6 1.015-.955 2.353-2.303 2.353-2.303l-.47-.599s-1.63.972-2.801 1.728c-.307.198-.573.378-.777.517a3.273 3.273 0 0 0-1.516-.611c-1.507-.198-2.927.672-3.487 2.017a10.323 10.323 0 0 1-.695-1.078A10.92 10.92 0 0 1 .728 14.8a10.35 10.35 0 0 1-.2-1.212c-.164-1.653.103-3.258.629-4.754a12.95 12.95 0 0 1 1.087-2.288c.57-.968 1.248-1.872 2.069-2.656A11.013 11.013 0 0 1 11.808.797Zm-.147 3.257a3.838 3.838 0 0 0-3.82 3.82v2.36h-.94c-.751 0-1.377.625-1.377 1.377v3.8h1.46v-3.718h9.354v6.264H10.02v1.46h6.4c.751 0 1.377-.625 1.377-1.377v-6.43c0-.751-.626-1.377-1.377-1.377h-.94v-2.36a3.838 3.838 0 0 0-3.82-3.819zm0 1.46a2.371 2.371 0 0 1 2.36 2.36v2.36H9.3v-2.36a2.372 2.372 0 0 1 2.36-2.36zm10.141.392a1.253 1.253 0 0 1 1.296 1.434c-.049.319-.217.59-.453.78-.266.213-.61.318-.968.264a1.253 1.253 0 0 1-1.045-1.42 1.255 1.255 0 0 1 1.17-1.058zM5.384 17.425a2.02 2.02 0 0 1 1.917 1.298c.116.3.159.628.114.967a2.015 2.015 0 0 1-2.249 1.728 2.016 2.016 0 0 1-1.727-2.25 2.017 2.017 0 0 1 1.945-1.743z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#E36002" d="M12.445.002a45.529 45.529 0 0 0-5.252 8.146 8.595 8.595 0 0 1-.555-.53 27.796 27.796 0 0 0-1.205-1.542 8.762 8.762 0 0 0-1.251 2.12 20.743 20.743 0 0 0-1.448 5.88 8.867 8.867 0 0 0 .338 3.468c1.312 3.48 3.794 5.593 7.445 6.337 3.055.438 5.755-.333 8.097-2.312 2.677-2.59 3.359-5.634 2.047-9.132a33.287 33.287 0 0 0-2.988-5.59A91.34 91.34 0 0 0 12.615.053a.216.216 0 0 0-.17-.051Zm-.336 3.906a50.93 50.93 0 0 1 4.794 6.552c.448.767.817 1.57 1.108 2.41.606 2.386-.044 4.354-1.951 5.904-1.845 1.298-3.87 1.683-6.072 1.156-2.376-.737-3.75-2.335-4.121-4.794a5.107 5.107 0 0 1 .242-2.266c.358-.908.79-1.774 1.3-2.601l1.446-2.121a397.33 397.33 0 0 0 3.254-4.24Z"/></svg>

After

Width:  |  Height:  |  Size: 758 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -1,43 +0,0 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from 'antd';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import logEvent from 'api/common/logEvent';
import { MessageSquareText } from '@signozhq/icons';
export default function ChatSupportGateway(): JSX.Element {
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
useState(false);
const { pathname } = useLocation();
return (
<>
<div className="chat-support-gateway">
<Button
className="chat-support-gateway-btn"
onClick={(): void => {
logEvent('Disabled Chat Support: Clicked', {
source: `chat support icon`,
page: pathname,
});
setIsAddCreditCardModalOpen(true);
}}
>
<MessageSquareText size={24} />
</Button>
</div>
<AddCreditCardModal
open={isAddCreditCardModalOpen}
onClose={(): void => setIsAddCreditCardModalOpen(false)}
onAddCreditCard={(): void => {
logEvent('Add Credit card modal: Clicked', {
source: `chat support icon`,
page: pathname,
});
}}
/>
</>
);
}

View File

@@ -1,252 +0,0 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
import useErrorNotification from 'hooks/useErrorNotification';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
import MenuItemGenerator from './MenuItemGenerator';
import SaveViewWithName from './SaveViewWithName';
import {
DropDownOverlay,
ExplorerCardHeadContainer,
OffSetCol,
} from './styles';
import { ExplorerCardProps } from './types';
import { deleteViewHandler } from './utils';
import { Ellipsis, Save, Share2, Trash2 } from '@signozhq/icons';
function ExplorerCard({
sourcepage,
children,
}: ExplorerCardProps): JSX.Element {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [, setCopyUrl] = useCopyToClipboard();
const { notifications } = useNotifications();
const onCopyUrlHandler = (): void => {
setCopyUrl(window.location.href);
notifications.success({
message: 'Copied to clipboard',
});
};
const {
currentQuery,
panelType,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
isStagedQueryUpdated,
} = useQueryBuilder();
const {
data: viewsData,
isLoading,
error,
isRefetching,
refetch: refetchAllView,
} = useGetAllViews(sourcepage);
useErrorNotification(error);
const handleOpenChange = (newOpen = false): void => {
setIsOpen(newOpen);
};
const { viewName, viewKey } = useGetSavedViewParams();
const { options } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
const isQueryUpdated = isStagedQueryUpdated(
viewsData?.data?.data,
viewKey,
options,
);
const { mutateAsync: updateViewAsync } = useUpdateView({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: '',
sourcePage: sourcepage,
viewName,
});
const { mutateAsync: deleteViewAsync } = useDeleteView(viewKey);
const showErrorNotification = (err: Error): void => {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
};
const onDeleteHandler = (): void =>
deleteViewHandler({
deleteViewAsync,
notifications,
panelType,
redirectWithQueryBuilderData,
refetchAllView,
viewId: viewKey,
viewKey,
updateAllQueriesOperators,
sourcePage: sourcepage,
});
const onUpdateQueryHandler = (): void => {
updateViewAsync(
{
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: '',
sourcePage: sourcepage,
viewName,
},
{
onSuccess: () => {
notifications.success({
message: 'View Updated Successfully',
});
refetchAllView();
},
onError: (err) => {
showErrorNotification(err);
},
},
);
};
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" />;
const showSaveView = false;
return (
<>
{showSaveView && (
<ExplorerCardHeadContainer size="small">
<Row align="middle">
<Col span={6}>
<Space>
<Typography>Query Builder</Typography>
<TextToolTip
url={ExploreHeaderToolTip.url}
text={ExploreHeaderToolTip.text}
useFilledIcon={false}
/>
</Space>
</Col>
<OffSetCol span={18}>
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
<Select
getPopupContainer={popupContainer}
loading={isLoading || isRefetching}
showSearch
placeholder="Select a view"
dropdownStyle={DropDownOverlay}
dropdownMatchSelectWidth={false}
optionLabelProp="value"
value={viewName || undefined}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.id} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.id}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}
/>
</Select.Option>
))}
</Select>
</Space>
)}
{isQueryUpdated && (
<Button type="primary" icon={<Save />} onClick={onUpdateQueryHandler}>
Save changes
</Button>
)}
<Popover
getPopupContainer={popupContainer}
placement="bottomLeft"
trigger="click"
content={
<SaveViewWithName
sourcePage={sourcepage}
handlePopOverClose={handleOpenChange}
refetchAllView={refetchAllView}
/>
}
showArrow={false}
open={isOpen}
onOpenChange={handleOpenChange}
>
<Button
type={saveButtonType}
icon={saveButtonIcon}
data-testid="traces-save-view-action"
>
{isQueryUpdated
? SaveButtonText.SAVE_AS_NEW_VIEW
: SaveButtonText.SAVE_VIEW}
</Button>
</Popover>
<Share2 onClick={onCopyUrlHandler} size="md" />
{viewKey && (
<Dropdown
items={moreOptionItems}
nativeButton
align="end"
side="bottom"
>
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
</Dropdown>
)}
</Space>
</OffSetCol>
</Row>
</ExplorerCardHeadContainer>
)}
<div>{children}</div>
</>
);
}
export default ExplorerCard;

View File

@@ -1,103 +0,0 @@
import { MouseEvent, useCallback } from 'react';
import { Trash2 } from '@signozhq/icons';
import { Col, Row, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { MenuItemContainer } from './styles';
import { MenuItemLabelGeneratorProps } from './types';
import {
deleteViewHandler,
getViewDetailsUsingViewKey,
trimViewName,
} from './utils';
function MenuItemGenerator({
viewName,
viewKey,
createdBy,
uuid,
viewData,
refetchAllView,
sourcePage,
}: MenuItemLabelGeneratorProps): JSX.Element {
const { panelType, redirectWithQueryBuilderData, updateAllQueriesOperators } =
useQueryBuilder();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { notifications } = useNotifications();
const { mutateAsync: deleteViewAsync } = useDeleteView(uuid);
const onDeleteHandler = (event: MouseEvent<SVGSVGElement>): void => {
event.stopPropagation();
deleteViewHandler({
deleteViewAsync,
notifications,
panelType,
redirectWithQueryBuilderData,
refetchAllView,
viewId: uuid,
viewKey,
updateAllQueriesOperators,
sourcePage,
});
};
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
const currentViewDetails = getViewDetailsUsingViewKey(key, viewData);
if (!currentViewDetails) {
return;
}
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
handleExplorerTabChange(currentPanelType, {
query,
viewName: name,
viewKey: id,
});
},
[viewData, handleExplorerTabChange],
);
const onLabelClickHandler = (): void => {
onMenuItemSelectHandler({
key: uuid,
});
};
const newViewName = trimViewName(viewName);
return (
<MenuItemContainer onClick={onLabelClickHandler}>
<Row justify="space-between">
<Col span={22}>
<Row>
<Tooltip title={viewName}>
<Typography.Text strong>{newViewName}</Typography.Text>
</Tooltip>
</Row>
<Row>
<Typography.Text color="muted">Created by {createdBy}</Typography.Text>
</Row>
</Col>
<Col span={2}>
<Typography.Link>
<Trash2
role="img"
aria-label="Delete view"
onClick={onDeleteHandler}
size="md"
/>
</Typography.Link>
</Col>
</Row>
</MenuItemContainer>
);
}
export default MenuItemGenerator;

View File

@@ -1,79 +0,0 @@
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Card, Form } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSaveView } from 'hooks/saveViews/useSaveView';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { SaveButton } from './styles';
import { SaveViewFormProps, SaveViewWithNameProps } from './types';
import { saveViewHandler } from './utils';
function SaveViewWithName({
sourcePage,
handlePopOverClose,
refetchAllView,
}: SaveViewWithNameProps): JSX.Element {
const [form] = Form.useForm<SaveViewFormProps>();
const { t } = useTranslation(['explorer']);
const { currentQuery, panelType, redirectWithQueryBuilderData } =
useQueryBuilder();
const { notifications } = useNotifications();
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
const { isLoading, mutateAsync: saveViewAsync } = useSaveView({
viewName: form.getFieldValue('viewName'),
compositeQuery,
sourcePage,
extraData: '',
});
const onSaveHandler = (): void => {
saveViewHandler({
compositeQuery,
handlePopOverClose,
extraData: '',
notifications,
panelType: panelType || PANEL_TYPES.LIST,
redirectWithQueryBuilderData,
refetchAllView,
saveViewAsync,
sourcePage,
viewName: form.getFieldValue('viewName'),
form,
});
};
return (
<Card>
<Typography>{t('name_of_the_view')}</Typography>
<Form form={form} onFinish={onSaveHandler} requiredMark>
<Form.Item
name={['viewName']}
required
rules={[
{
required: true,
message: 'Please enter view name',
},
]}
>
<Input placeholder="Enter Name" />
</Form.Item>
<SaveButton
htmlType="submit"
type="primary"
loading={isLoading}
data-testid="save-view-name-action-button"
>
Save
</SaveButton>
</Form>
</Card>
);
}
export default SaveViewWithName;

View File

@@ -1,32 +0,0 @@
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const viewMockData: ViewProps[] = [
{
id: 'view1',
name: 'View 1',
createdBy: 'User 1',
category: 'category 1',
compositeQuery: {} as ICompositeMetricQuery,
createdAt: '2021-07-07T06:31:00.000Z',
updatedAt: '2021-07-07T06:33:00.000Z',
extraData: '',
sourcePage: DataSource.TRACES,
tags: [],
updatedBy: 'User 1',
},
{
id: 'view2',
name: 'View 2',
createdBy: 'User 2',
category: 'category 2',
compositeQuery: {} as ICompositeMetricQuery,
createdAt: '2021-07-07T06:30:00.000Z',
updatedAt: '2021-07-07T06:30:00.000Z',
extraData: '',
sourcePage: DataSource.TRACES,
tags: [],
updatedBy: 'User 2',
},
];

View File

@@ -1,9 +0,0 @@
export const ExploreHeaderToolTip = {
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
text: 'More details on how to use query builder',
};
export const SaveButtonText = {
SAVE_AS_NEW_VIEW: 'Save as new view',
SAVE_VIEW: 'Save view',
};

View File

@@ -1,29 +0,0 @@
import { Button, Card, Col } from 'antd';
import styled, { CSSProperties } from 'styled-components';
export const ExplorerCardHeadContainer = styled(Card)`
margin: 1rem 0;
padding: 0;
`;
export const OffSetCol = styled(Col)`
text-align: right;
`;
export const SaveButton = styled(Button)`
&&& {
margin: 1rem 0;
width: 5rem;
}
`;
export const DropDownOverlay: CSSProperties = {
maxHeight: '20rem',
overflowY: 'auto',
width: '20rem',
padding: 0,
};
export const MenuItemContainer = styled(Card)`
padding: 0;
`;

View File

@@ -1,96 +0,0 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
const historyReplace = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...jest.requireActual('react-router-dom').useHistory(),
replace: historyReplace,
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: jest.fn(() => ({
data: { data: { data: viewMockData } },
isLoading: false,
error: null,
isRefetching: false,
refetch: jest.fn(),
})),
}));
jest.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
jest.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 1,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));
describe('ExplorerCard', () => {
it('renders a card with a title and a description', () => {
render(
<MockQueryClientProvider>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
});
it('renders a save view button', () => {
render(
<MockQueryClientProvider>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
});
});

View File

@@ -1,63 +0,0 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import MenuItemGenerator from '../MenuItemGenerator';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
jest.mock('antd', () => ({
...jest.requireActual('antd'),
useForm: jest.fn().mockReturnValue({
onFinish: jest.fn(),
}),
}));
describe('MenuItemGenerator', () => {
it('should render MenuItemGenerator component', () => {
const screen = render(
<MockQueryClientProvider>
<MenuItemGenerator
viewName={viewMockData[0].name}
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
</MockQueryClientProvider>,
);
expect(screen.getByText(viewMockData[0].name)).toBeInTheDocument();
});
it('should call onMenuItemSelectHandler on click of MenuItemGenerator', () => {
render(
<MockQueryClientProvider>
<MenuItemGenerator
viewName={viewMockData[0].name}
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
</MockQueryClientProvider>,
);
const spanElement = screen.getByRole('img', {
name: /delete view/i,
});
expect(spanElement).toBeInTheDocument();
});
});

View File

@@ -1,63 +0,0 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { fireEvent, render } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import SaveViewWithName from '../SaveViewWithName';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
describe('SaveViewWithName', () => {
it('should render SaveViewWithName component', () => {
const screen = render(
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);
expect(screen.getByText('Save')).toBeInTheDocument();
});
it('should call saveViewAsync on click of Save button', () => {
const screen = render(
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);
fireEvent.click(screen.getByText('Save'));
expect(screen.getByText('Save')).toBeInTheDocument();
});
});

View File

@@ -1,92 +0,0 @@
import { UseMutateAsyncFunction } from 'react-query';
import { FormInstance } from 'antd';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
DeleteViewPayloadProps,
SaveViewPayloadProps,
SaveViewProps,
ViewProps,
} from 'types/api/saveViews/types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
export interface ExplorerCardProps {
sourcepage: DataSource;
children: React.ReactNode;
}
export type GetViewDetailsUsingViewKey = (
viewKey: string,
data: ViewProps[] | undefined,
) =>
| {
query: Query;
name: string;
id: string;
panelType: PANEL_TYPES;
extraData?: string;
}
| undefined;
export interface IsQueryUpdatedInViewProps {
viewKey: string;
data: ViewProps[] | undefined;
stagedQuery: Query | null;
currentPanelType: PANEL_TYPES | null;
options: OptionsQuery;
}
export interface SaveViewWithNameProps {
sourcePage: ExplorerCardProps['sourcepage'];
handlePopOverClose: VoidFunction;
refetchAllView: VoidFunction;
}
export interface SaveViewFormProps {
viewName: string;
}
export interface MenuItemLabelGeneratorProps {
viewName: string;
viewKey: string;
createdBy: string;
uuid: string;
viewData: ViewProps[];
refetchAllView: VoidFunction;
sourcePage: ExplorerCardProps['sourcepage'];
}
export interface SaveViewHandlerProps {
viewName: string;
compositeQuery: ICompositeMetricQuery;
sourcePage: ExplorerCardProps['sourcepage'];
extraData: string;
panelType: PANEL_TYPES | null;
notifications: NotificationInstance;
refetchAllView: SaveViewWithNameProps['refetchAllView'];
saveViewAsync: UseMutateAsyncFunction<
AxiosResponse<SaveViewPayloadProps>,
Error,
SaveViewProps,
SaveViewPayloadProps
>;
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
form: FormInstance<SaveViewFormProps>;
}
export interface DeleteViewHandlerProps {
deleteViewAsync: UseMutateAsyncFunction<DeleteViewPayloadProps, Error, string>;
refetchAllView: MenuItemLabelGeneratorProps['refetchAllView'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
notifications: NotificationInstance;
panelType: PANEL_TYPES | null;
viewKey: string;
viewId: string;
updateAllQueriesOperators: QueryBuilderContextType['updateAllQueriesOperators'];
sourcePage: ExplorerCardProps['sourcepage'];
}

View File

@@ -1,204 +0,0 @@
import type { NotificationInstance } from 'antd/es/notification/interface';
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import isEqual from 'lodash-es/isEqual';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
DeleteViewHandlerProps,
GetViewDetailsUsingViewKey,
IsQueryUpdatedInViewProps,
SaveViewHandlerProps,
} from './types';
export const showErrorNotification = (
notifications: NotificationInstance,
err: Error,
): void => {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
};
export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
viewKey,
data,
) => {
const selectedView = data?.find((view) => view.id === viewKey);
if (selectedView) {
const { compositeQuery, name, id, extraData } = selectedView;
const query = mapQueryDataFromApi(compositeQuery);
return { query, name, id, panelType: compositeQuery.panelType, extraData };
}
return undefined;
};
export const omitIdFromQuery = (query: Query | null): any => ({
...query,
builder: {
...query?.builder,
queryData: query?.builder.queryData.map((queryData) => {
const { id: _aggregateAttributeId, ...rest } =
queryData.aggregateAttribute || {};
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id: _groupByAttributeId, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters?.items?.map((item) => {
const { id: _itemId, ...newItem } = item;
if (item.key) {
const { id: _keyId, ...rest } = item.key;
return {
...newItem,
key: rest,
};
}
return newItem;
});
return {
...queryData,
aggregateAttribute: newAggregateAttribute,
groupBy: newGroupByAttributes,
filters: {
...queryData.filters,
items: newItems,
},
limit: queryData.limit ? queryData.limit : 0,
offset: queryData.offset ? queryData.offset : 0,
pageSize: queryData.pageSize ? queryData.pageSize : 0,
};
}),
},
});
export const isQueryUpdatedInView = ({
viewKey,
data,
stagedQuery,
currentPanelType,
options,
}: IsQueryUpdatedInViewProps): boolean => {
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
if (!currentViewDetails) {
return false;
}
const { query, panelType, extraData } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
if (
updatedCurrentQuery?.builder === undefined ||
updatedCurrentQuery.clickhouse_sql === undefined ||
updatedCurrentQuery.promql === undefined
) {
return false;
}
return (
panelType !== currentPanelType ||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
!isEqual(query.promql, updatedCurrentQuery?.promql) ||
!isEqual(
options?.selectColumns,
extraData && JSON.parse(extraData)?.selectColumns,
) ||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
);
};
export const saveViewHandler = ({
saveViewAsync,
refetchAllView,
notifications,
handlePopOverClose,
viewName,
compositeQuery,
sourcePage,
extraData,
redirectWithQueryBuilderData,
panelType,
form,
}: SaveViewHandlerProps): void => {
saveViewAsync(
{
viewName,
compositeQuery,
sourcePage,
extraData,
},
{
onSuccess: (data) => {
refetchAllView();
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
[QueryParams.panelTypes]: panelType,
[QueryParams.viewName]: viewName,
[QueryParams.viewKey]: data.data.data,
});
notifications.success({
message: 'View Saved Successfully',
});
},
onError: (err) => {
showErrorNotification(notifications, err);
},
onSettled: () => {
handlePopOverClose();
form.resetFields();
},
},
);
};
export const deleteViewHandler = ({
deleteViewAsync,
refetchAllView,
redirectWithQueryBuilderData,
notifications,
panelType,
viewKey,
viewId,
updateAllQueriesOperators,
sourcePage,
}: DeleteViewHandlerProps): void => {
deleteViewAsync(viewKey, {
onSuccess: () => {
if (viewId === viewKey) {
redirectWithQueryBuilderData(
updateAllQueriesOperators(
initialQueriesMap[sourcePage],
panelType || PANEL_TYPES.LIST,
sourcePage,
),
{
[QueryParams.viewName]: '',
[QueryParams.panelTypes]: panelType,
[QueryParams.viewKey]: '',
},
);
}
notifications.success({
message: 'View Deleted Successfully',
});
refetchAllView();
},
onError: (err) => {
showErrorNotification(notifications, err);
},
});
};
export const trimViewName = (viewName: string): string => {
if (viewName.length > 20) {
return `${viewName.substring(0, 20)}...`;
}
return viewName;
};

View File

@@ -1,5 +1,5 @@
import { DrawerProps } from 'antd';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';

View File

@@ -4,7 +4,7 @@ import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import {
getBodyDisplayString,
getSanitizedLogBody,

View File

@@ -1,6 +1,6 @@
import { MouseEvent } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';

View File

@@ -1,14 +1,9 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getBuilderVariant } from 'lib/query/queryMode';
import {
BuilderVariant,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderField } from './queryBuilderFields.types';
@@ -64,20 +59,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
[config],
);
const isAIQuery =
getBuilderVariant(currentQuery.builder) === BuilderVariant.AI;
useEffect(() => {
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
// AI queries are traces-only; BAR must not force metrics on them.
if (isAIQuery) {
handleSetConfig(newPanelType, DataSource.TRACES);
return;
}
if (newPanelType === PANEL_TYPES.BAR) {
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
return;
}
handleSetConfig(newPanelType, currentDataSource);
}
}, [
@@ -86,7 +69,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
initialDataSource,
currentDataSource,
newPanelType,
isAIQuery,
]);
useEffect(() => {

View File

@@ -129,6 +129,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
onChange={handleChangeOperator}
operators={operators}
className="metrics-operators-select"
data-testid="metrics-time-aggregation"
/>
</div>
</div>
@@ -192,7 +193,10 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
AGGREGATE ACROSS TIME SERIES
</div>
</Tooltip>
<div className="metrics-aggregation-section-content-item-value">
<div
className="metrics-aggregation-section-content-item-value"
data-testid="metrics-space-aggregation"
>
<SpaceAggregationOptions
panelType={panelType}
key={`${panelType}${queryAggregation.spaceAggregation}${queryAggregation.timeAggregation}`}
@@ -229,7 +233,10 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
<div className="metrics-space-aggregation-section">
<div className="metrics-aggregation-section-content">
<div className="metrics-aggregation-section-content-item">
<div className="metrics-aggregation-section-content-item-value">
<div
className="metrics-aggregation-section-content-item-value"
data-testid="metrics-space-aggregation"
>
<SpaceAggregationOptions
panelType={panelType}
key={`${panelType}${queryAggregation.spaceAggregation}${queryAggregation.timeAggregation}`}

View File

@@ -332,6 +332,7 @@ function HavingFilter({
value={input}
onChange={handleChange}
theme={isDarkMode ? copilot : githubLight}
data-testid="having-editor"
className="having-filter-select-editor"
width="100%"
extensions={[

View File

@@ -615,6 +615,7 @@ function QueryAggregationSelect({
setInput(value);
onChange?.(value);
}}
data-testid="query-aggregation-editor"
className={`query-aggregation-select-editor ${
validationError ? 'error' : ''
}`}

View File

@@ -1607,6 +1607,7 @@ function QuerySearch({
onChange={handleChange}
onUpdate={handleUpdate}
onCreateEditor={handleEditorCreate}
data-testid="query-where-clause-editor"
className={cx('query-where-clause-editor', {
isValid: validation.isValid === true,
hasErrors: validation.errors.length > 0,
@@ -1680,6 +1681,7 @@ function QuerySearch({
className={cx('query-status-container', {
hasErrors: validation.errors.length > 0,
})}
data-testid="query-validation-status"
>
<Popover
placement="bottomRight"

View File

@@ -183,6 +183,7 @@ export const QueryV2 = forwardRef(function QueryV2(
<div
className={cx('query-v2', { 'where-clause-view': showOnlyWhereClause })}
ref={ref}
data-testid={`query-${query.queryName}`}
>
<div className="qb-content-section">
{(!showOnlyWhereClause || showTraceOperator) && (
@@ -246,6 +247,7 @@ export const QueryV2 = forwardRef(function QueryV2(
{isMultiQueryAllowed && (
<Dropdown
items={queryActionItems}
testId="query-actions"
nativeButton={false}
align="end"
side="bottom"

View File

@@ -61,7 +61,10 @@ export default function TraceOperator({
);
return (
<div className={cx('qb-trace-operator', !isRawQuery && 'non-list-view')}>
<div
className={cx('qb-trace-operator', !isRawQuery && 'non-list-view')}
data-testid="trace-operator"
>
<div className="qb-trace-operator-container">
<div
className={cx(

View File

@@ -378,6 +378,7 @@ function TraceOperatorEditor({
theme={isDarkMode ? copilot : githubLight}
onChange={handleChange}
onUpdate={handleUpdate}
data-testid="trace-operator-editor"
className={cx('query-where-clause-editor', {
isValid: validation.isValid === true,
hasErrors: validation.errors.length > 0,

View File

@@ -130,7 +130,6 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
} as unknown as QueryBuilderContextType;

View File

@@ -136,7 +136,6 @@ describe('MetricsSelect - signal source switching (standalone)', () => {
updateAllQueriesOperators: ((q: any) => q) as any,
updateQueriesData: ((q: any) => q) as any,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
});
});
@@ -293,7 +292,6 @@ describe('DataSource change - Logs to Traces', () => {
updateAllQueriesOperators: ((q: any) => q) as any,
updateQueriesData: ((q: any) => q) as any,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
});
});

View File

@@ -0,0 +1,109 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { render } from 'tests/test-utils';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2 } from '../QueryBuilderV2';
jest.mock('hooks/queryBuilder/useQueryBuilder');
jest.mock('../QueryV2/QueryV2', () => ({ QueryV2: (): null => null }));
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
const aiQuery: Query = {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
...query,
builderQueryType: QueryMode.AI_QUERY_BUILDER,
})),
},
};
function mockContext({
panelType = null,
initialDataSource = null,
currentQuery = initialQueriesMap.metrics,
}: {
panelType?: PANEL_TYPES | null;
initialDataSource?: DataSource | null;
currentQuery?: Query;
}): jest.Mock {
const handleSetConfig = jest.fn();
mockedUseQueryBuilder.mockReturnValue({
currentQuery,
panelType,
initialDataSource,
handleSetConfig,
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
addTraceOperator: jest.fn(),
handleRunQuery: jest.fn(),
} as unknown as ReturnType<typeof useQueryBuilder>);
return handleSetConfig;
}
describe('QueryBuilderV2 provider config sync', () => {
it('leaves the data source unset for a bar panel without a static config', () => {
const handleSetConfig = mockContext({});
render(<QueryBuilderV2 panelType={PANEL_TYPES.BAR} version="v3" />);
expect(handleSetConfig).toHaveBeenCalledWith(PANEL_TYPES.BAR, null);
});
it('syncs the static data source', () => {
const handleSetConfig = mockContext({});
render(
<QueryBuilderV2
version="v3"
panelType={PANEL_TYPES.BAR}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
/>,
);
expect(handleSetConfig).toHaveBeenCalledWith(
PANEL_TYPES.BAR,
DataSource.LOGS,
);
});
it('keeps an AI query on traces for a bar panel', () => {
const handleSetConfig = mockContext({ currentQuery: aiQuery });
render(
<QueryBuilderV2
version="v3"
panelType={PANEL_TYPES.BAR}
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
/>,
);
expect(handleSetConfig).toHaveBeenCalledWith(
PANEL_TYPES.BAR,
DataSource.TRACES,
);
});
it('does not reset the config when the provider already matches', () => {
const handleSetConfig = mockContext({
panelType: PANEL_TYPES.TIME_SERIES,
initialDataSource: DataSource.TRACES,
});
render(
<QueryBuilderV2
version="v3"
panelType={PANEL_TYPES.TIME_SERIES}
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
/>,
);
expect(handleSetConfig).not.toHaveBeenCalled();
});
});

View File

@@ -5,7 +5,6 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import SavedViewsHeader from 'container/SavedViews/SavedViewsHeader';
import SavedViewsPanel from 'container/SavedViews/SavedViewsPanel';
import SavedViewsRestore from 'container/SavedViews/SavedViewsRestore';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import QuickFilters from '../QuickFilters';
import { SIDEBAR_TOOLTIP_DELAY_MS } from './constants';
@@ -50,10 +49,9 @@ function QuickFiltersLayout({
children,
}: QuickFiltersLayoutProps): JSX.Element {
const [isViewsListOpen, setIsViewsListOpen] = useState(false);
const isSavedViewEnabled = useSavedViewEnabled();
const hasQuickFilters = !!quickFilterProps;
const hasSavedViews = !!savedViewProps && isSavedViewEnabled;
const hasSavedViews = !!savedViewProps;
const showSidebar = showFilters && (hasQuickFilters || hasSavedViews);

View File

@@ -1,6 +1,5 @@
import { Button } from '@signozhq/ui/button';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { QuickFiltersSource } from '../../types';
@@ -20,10 +19,6 @@ jest.mock('../QuickFiltersLayout.module.scss', () => ({
},
}));
jest.mock('hooks/useSavedViewEnabled', () => ({
useSavedViewEnabled: jest.fn(() => true),
}));
jest.mock('container/SavedViews/SavedViewsRestore', () => ({
__esModule: true,
default: ({ source }: { source: string }): JSX.Element => (
@@ -71,10 +66,6 @@ const drawer = (): HTMLElement =>
screen.getByTestId('quick-filters-layout-drawer');
describe('QuickFiltersLayout', () => {
beforeEach(() => {
jest.mocked(useSavedViewEnabled).mockReturnValue(true);
});
it('renders QuickFilters with the given props inside the filters pane', () => {
render(
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
@@ -149,35 +140,6 @@ describe('QuickFiltersLayout', () => {
});
describe('saved views', () => {
it('ignores savedViewProps while the flag is off', () => {
jest.mocked(useSavedViewEnabled).mockReturnValue(false);
const { unmount } = render(
<QuickFiltersLayout
showFilters
quickFilterProps={quickFilterProps}
savedViewProps={savedViewProps}
>
<div>content</div>
</QuickFiltersLayout>,
);
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
expect(screen.queryByTestId('saved-views-header')).not.toBeInTheDocument();
expect(screen.queryByTestId('saved-views-panel')).not.toBeInTheDocument();
// Saved views only, so nothing is left to show.
unmount();
render(
<QuickFiltersLayout showFilters savedViewProps={savedViewProps}>
<div>content</div>
</QuickFiltersLayout>,
);
expect(
screen.queryByTestId('quick-filters-layout-filters'),
).not.toBeInTheDocument();
expect(screen.queryByTestId('saved-views-restore')).not.toBeInTheDocument();
});
it('opens the panel from the header and slides the quick filters drawer, keeping the same QuickFilters node', async () => {
render(
<QuickFiltersLayout

View File

@@ -21,7 +21,6 @@ export enum LOCALSTORAGE {
CHAT_SUPPORT = 'CHAT_SUPPORT',
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
@@ -47,5 +46,4 @@ export enum LOCALSTORAGE {
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
LLM_ATTRIBUTE_MAPPING_TEST_SPAN = 'LLM_ATTRIBUTE_MAPPING_TEST_SPAN',
SAVED_VIEW_ENABLED = 'SAVED_VIEW_ENABLED',
}

View File

@@ -50,8 +50,6 @@ const ROUTES = {
ROLE_EDIT: '/settings/roles/:roleId/edit',
MEMBERS_SETTINGS: '/settings/members',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',
TRACES_FUNNELS: '/traces/funnels',
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
WORKSPACE_LOCKED: '/workspace-locked',
@@ -69,7 +67,6 @@ const ROUTES = {
MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview',
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
METRICS_EXPLORER_VOLUME_CONTROL: '/metrics-explorer/volume-control',
API_MONITORING_BASE: '/api-monitoring',
API_MONITORING: '/api-monitoring/explorer',
@@ -77,7 +74,6 @@ const ROUTES = {
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
METER: '/meter',
METER_EXPLORER: '/meter/explorer',
METER_EXPLORER_VIEWS: '/meter/explorer/views',
HOME_PAGE: '/',
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',

View File

@@ -1,15 +0,0 @@
.trigger {
position: absolute;
bottom: 24px;
right: 24px;
z-index: 10;
transition: transform 0.15s;
&:hover {
transform: scale(1.08);
}
&:active {
transform: scale(0.96);
}
}

View File

@@ -1,61 +0,0 @@
import { useCallback } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
import {
openAIAssistant,
useAIAssistantStore,
} from '../store/useAIAssistantStore';
import styles from './AIAssistantTrigger.module.scss';
/**
* Floating action button anchored to the bottom-right of the content area.
* Hidden when the panel is already open or when on the full-screen AI Assistant page.
*/
export default function AIAssistantTrigger(): JSX.Element | null {
const { pathname } = useLocation();
const isDrawerOpen = useAIAssistantStore((s) => s.isDrawerOpen);
const isModalOpen = useAIAssistantStore((s) => s.isModalOpen);
const isFullScreenPage = !!matchPath(pathname, {
path: ROUTES.AI_ASSISTANT,
exact: true,
});
const handleOpen = useCallback((): void => {
void logEvent(AIAssistantEvents.Opened, {
source: AIAssistantOpenSource.Icon,
currentPage: normalizePage(pathname),
});
openAIAssistant();
}, [pathname]);
if (isDrawerOpen || isModalOpen || isFullScreenPage) {
return null;
}
return (
<div className={`${styles.trigger} noz-wave`}>
<Tooltip title={NOZ_TOOLTIP_TITLE}>
<Button
size="md"
variant="solid"
color="primary"
icon
onClick={handleOpen}
aria-label="Open Noz"
>
<Noz size={24} />
</Button>
</Tooltip>
</div>
);
}

View File

@@ -1,2 +0,0 @@
export * from './AIAssistantTrigger';
export { default } from './AIAssistantTrigger';

View File

@@ -102,7 +102,7 @@ describe('resolvePageType', () => {
expect(resolvePageType(ROUTES.METRICS_EXPLORER_EXPLORER, '')).toBe(
PageTypeDTO.metrics_explorer,
);
expect(resolvePageType(ROUTES.METRICS_EXPLORER_VIEWS, '')).toBe(
expect(resolvePageType('/metrics-explorer/views', '')).toBe(
PageTypeDTO.metrics_explorer,
);
});

View File

@@ -235,7 +235,6 @@ describe('buildExplorerNavigationUrl', () => {
{ queryType: 'builder' } as never,
{
[QueryParams.panelTypes]: PANEL_TYPES.LIST,
[QueryParams.viewName]: 'My view',
[QueryParams.viewKey]: 'view-1',
},
);
@@ -259,7 +258,7 @@ describe('openSavedView', () => {
expect(pushedUrl).toContain(ROUTES.LOGS_EXPLORER);
const params = new URLSearchParams(pushedUrl.split('?')[1]);
expect(params.get(QueryParams.viewKey)).toBe('"view-logs"');
expect(params.get(QueryParams.viewName)).toBe('"View view-logs"');
expect(params.get(QueryParams.viewName)).toBeNull();
expect(params.get(QueryParams.panelTypes)).toBe('"list"');
});

View File

@@ -9,7 +9,7 @@ import { SavedViewSourcePage } from 'container/SavedViews/types';
import { findSavedView } from 'container/SavedViews/utils/findSavedView';
import { getSavedViewQuery } from 'container/SavedViews/utils/getSavedViewQuery';
import { toSavedViewSource } from 'container/SavedViews/utils/toSavedViewSource';
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
import { SOURCEPAGE_VS_ROUTES } from 'container/SavedViews/constants';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { History } from 'history';
@@ -108,7 +108,6 @@ export function openSavedView(
const query = getSavedViewQuery(view);
const url = buildExplorerNavigationUrl(route, query, {
[QueryParams.panelTypes]: view.spec.panelType as unknown as PANEL_TYPES,
[QueryParams.viewName]: view.spec.displayName,
[QueryParams.viewKey]: view.id,
});
history.push(url);

View File

@@ -13,7 +13,7 @@ import { matchPath } from 'react-router-dom';
*
* **URL-only.** All inputs come from `pathname` + `search`; we never reach
* into Redux / context. SigNoz already encodes most page state into the URL
* (`compositeQuery`, `startTime`/`endTime`, `viewName`/`viewKey`,
* (`compositeQuery`, `startTime`/`endTime`, `viewKey`,
* `variables`, `expandedWidgetId`, `activeLogId`, …), so URL parsing is
* sufficient. Anything not in the URL is simply omitted from `metadata` —
* the server applies its own defaults.
@@ -288,7 +288,7 @@ export function getAutoContexts(
// ── Metrics ───────────────────────────────────────────────────────────────
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer, views).
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer).
if (
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
) {
@@ -361,10 +361,6 @@ function collectSharedMetadata(
if (viewKey) {
out.savedViewId = viewKey;
}
const viewName = params.get(QueryParams.viewName);
if (viewName) {
out.savedViewName = viewName;
}
const variablesRaw = params.get(QueryParams.variables);
if (variablesRaw) {
try {

View File

@@ -72,40 +72,6 @@
}
}
.chat-support-gateway {
position: fixed;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: calc(20px + var(--bottom-strip-height, 0px));
right: 20px;
z-index: 1000;
.chat-support-gateway-btn {
max-width: 48px;
width: 48px;
max-height: 48px;
height: 48px;
padding: 12px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background-color: #f25733;
border: none;
&:hover {
color: white !important;
border-color: white !important;
}
.chat-support-gateway-btn-icon {
fill: white;
}
}
}
.add-credit-card-btn,
.cancel-btn {
display: inline-flex;

View File

@@ -28,7 +28,6 @@ import getUserLatestVersion from 'api/v1/version/getLatestVersion';
import { AxiosError } from 'axios';
import cx from 'classnames';
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
@@ -51,8 +50,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useNotifications } from 'hooks/useNotifications';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import useTabVisibility from 'hooks/useTabFocus';
import history from 'lib/history';
import { isNull } from 'lodash-es';
@@ -401,7 +398,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}, [pathname]);
const isToDisplayLayout = isLoggedIn;
const isSavedViewEnabled = useSavedViewEnabled();
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
const pageTitle = t(routeKey);
@@ -489,8 +485,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [isDarkMode]);
const chatSupport = useChatSupport();
// Listen for API warnings
const handleWarning = (
isSlow: boolean,
@@ -834,9 +828,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</LayoutContent>
</Sentry.ErrorBoundary>
{isSavedViewEnabled && isToDisplayLayout && !renderFullScreen && (
<BottomStrip />
)}
{isToDisplayLayout && !renderFullScreen && <BottomStrip />}
</div>
{isLoggedIn && isAIAssistantEnabled && (
@@ -847,9 +839,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
</Flex>
{chatSupport === ChatSupportState.NeedsCard && !isSavedViewEnabled && (
<ChatSupportGateway />
)}
{showChangelogModal && changelog && (
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
)}

View File

@@ -2,7 +2,7 @@ import { AlertmanagertypesGettableNotificationChannelDTO } from 'api/generated/s
import { toChannelConfig, toPostableChannel } from './channelConfig';
import { toChannelFormState } from './channelFormValues';
import { ChannelFormValues, ChannelKind } from './types';
import { ChannelFormValues, ChannelKind, WebhookAuth } from './types';
const slackValues: ChannelFormValues = {
name: 'prod alerts',
@@ -56,6 +56,22 @@ describe('toChannelConfig', () => {
});
});
it.each([
[WebhookAuth.none, {}],
[WebhookAuth.basic, { username: 'u', password: 'p' }],
[WebhookAuth.bearer, { bearerToken: 't' }],
])('sends only the %s webhook credentials', (webhookAuth, credentials) => {
expect(
toChannelConfig(ChannelKind.webhook, {
url: 'https://example.com/hook',
webhookAuth,
username: 'u',
password: 'p',
bearerToken: 't',
}).spec,
).toStrictEqual({ url: 'https://example.com/hook', ...credentials });
});
it('drops an attachment list the user emptied instead of sending []', () => {
expect(
toChannelConfig(ChannelKind.slack, {

View File

@@ -18,8 +18,17 @@ import {
import { omit, pick } from 'lodash-es';
import { omitBlank } from 'utils/omitBlank';
import { ChannelFormValues, ChannelKind, ChannelSpecFormValues } from './types';
import { canonicalJiraReopenDuration, filledRows } from './utils';
import {
ChannelFormValues,
ChannelKind,
ChannelSpecFormValues,
WebhookAuth,
} from './types';
import {
canonicalJiraReopenDuration,
filledRows,
webhookAuthOf,
} from './utils';
const SPEC_FIELDS = {
[ChannelKind.slack]: [
@@ -210,6 +219,17 @@ export function toChannelConfig(
);
}
if (kind === ChannelKind.webhook) {
const auth = webhookAuthOf(values);
if (auth !== WebhookAuth.basic) {
delete picked.username;
delete picked.password;
}
if (auth !== WebhookAuth.bearer) {
delete picked.bearerToken;
}
}
const spec: Record<string, unknown> = omitBlank(picked);
// the API answers a read with the duration prometheus would print, and

View File

@@ -1,6 +1,7 @@
import { AlertmanagertypesGettableNotificationChannelDTO } from 'api/generated/services/sigNoz.schemas';
import { ChannelFormValues, ChannelKind } from './types';
import { webhookAuthOf } from './utils';
export interface ChannelFormState {
kind: ChannelKind;
@@ -22,11 +23,15 @@ function toChannelKind(kind: string): ChannelKind {
export function toChannelFormState(
channel: AlertmanagertypesGettableNotificationChannelDTO,
): ChannelFormState {
return {
kind: toChannelKind(channel.config.kind),
values: {
...channel.config.spec,
name: channel.displayName,
},
const kind = toChannelKind(channel.config.kind);
const values: ChannelFormValues = {
...channel.config.spec,
name: channel.displayName,
};
if (kind === ChannelKind.webhook) {
values.webhookAuth = webhookAuthOf(values);
}
return { kind, values };
}

View File

@@ -1,4 +1,5 @@
import { TFunction } from 'i18next';
import { get, set } from 'lodash-es';
import type { FieldErrors, Resolver } from 'react-hook-form';
import type { $ZodIssue } from 'zod/v4/core';
@@ -10,12 +11,12 @@ const toFieldErrors = (
issues: $ZodIssue[],
t: TFunction,
): FieldErrors<ChannelFormValues> => {
const errors: Record<string, { type: string; message: string }> = {};
const errors = {};
issues.forEach((issue) => {
const field = issue.path.join('.') || 'name';
if (!errors[field]) {
errors[field] = { type: 'validation', message: t(issue.message) };
const path = issue.path.length ? issue.path : ['name'];
if (!get(errors, path)) {
set(errors, path, { type: 'validation', message: t(issue.message) });
}
});

View File

@@ -23,7 +23,6 @@ const SPECS: Record<string, Record<string, unknown>> = {
url: 'https://example.com/hook',
username: 'u',
password: 'p',
bearerToken: 't',
sendResolved: true,
},
email: {

View File

@@ -1,11 +1,7 @@
import { z } from 'zod';
import { isValidGoogleChatWebhookURL } from '../utils';
import { requiredString } from './shared';
export const googleChatSchema = z.object({
webhookUrl: requiredString('webhook_url_required').refine(
isValidGoogleChatWebhookURL,
{ message: 'google_chat_webhook_url_invalid' },
),
webhookUrl: requiredString('webhook_url_required'),
});

View File

@@ -1,13 +1,19 @@
import { z } from 'zod';
import { isValidIncidentIOURL } from '../utils';
import { requiredString } from './shared';
const required = requiredString('incidentio_required_fields');
export const incidentIOSchema = z.object({
url: required.refine(isValidIncidentIOURL, {
message: 'incidentio_url_invalid',
}),
token: required,
url: required,
token: z
.string({ error: 'incidentio_required_fields' })
.refine((token) => token === token.trim(), {
message: 'incidentio_token_whitespace',
})
.pipe(
required.refine((token) => !/^bearer( |$)/i.test(token), {
message: 'incidentio_token_bearer_prefix',
}),
),
});

View File

@@ -1,12 +1,12 @@
import { z } from 'zod';
import { isValidJiraReopenDuration, isValidJiraSiteURL } from '../utils';
import { isValidJiraReopenDuration } from '../utils';
import { requiredString } from './shared';
const required = requiredString('jira_required_fields');
export const jiraSchema = z.object({
site: required.refine(isValidJiraSiteURL, { message: 'jira_site_invalid' }),
site: required,
email: required,
apiToken: required,
project: required,

View File

@@ -2,6 +2,30 @@ import { z } from 'zod';
import { requiredString } from './shared';
// rows with a blank title or text are dropped on save, so only the rest are checked
const fieldRow = z
.object({ title: z.string().optional(), value: z.string().optional() })
.refine((row) => !row.title?.trim() || !!row.value?.trim(), {
message: 'slack_field_value_required',
path: ['value'],
});
const actionRow = z
.object({
text: z.string().optional(),
url: z.string().optional(),
name: z.string().optional(),
})
.refine(
(row) => !row.text?.trim() || !!row.url?.trim() || !!row.name?.trim(),
{
message: 'slack_action_url_or_name_required',
path: ['url'],
},
);
export const slackSchema = z.object({
apiUrl: requiredString('webhook_url_required'),
fields: z.array(fieldRow).nullish(),
actions: z.array(actionRow).nullish(),
});

View File

@@ -1,16 +1,26 @@
import { z } from 'zod';
import { WebhookAuth } from '../types';
import { webhookAuthOf } from '../utils';
import { requiredString } from './shared';
export const webhookSchema = z
.object({
url: requiredString('webhook_url_required'),
webhookAuth: z.enum(WebhookAuth).optional(),
username: z.string().optional(),
password: z.string().optional(),
bearerToken: z.string().optional(),
})
// the API allows bearer-only and no-auth webhooks, but a username without its
// password is still an incomplete basic auth pair
.refine((v) => !v.username || !!v.password, {
message: 'username_no_password',
path: ['password'],
});
.refine(
(values) =>
webhookAuthOf(values) !== WebhookAuth.basic ||
!!values.username?.trim() ||
!!values.password?.trim(),
{ message: 'webhook_basic_auth_required', path: ['username'] },
)
.refine(
(values) =>
webhookAuthOf(values) !== WebhookAuth.bearer || !!values.bearerToken?.trim(),
{ message: 'webhook_bearer_token_required', path: ['bearerToken'] },
);

View File

@@ -31,6 +31,15 @@ export type ChannelSpecFormValues = Partial<
AlertmanagertypesChannelTelegramConfigDTO
>;
export const WebhookAuth = {
none: 'none',
basic: 'basic',
bearer: 'bearer',
} as const;
export type WebhookAuth = (typeof WebhookAuth)[keyof typeof WebhookAuth];
export type ChannelFormValues = ChannelSpecFormValues & {
name?: string;
/** Never sent: the API reads the mode from which credentials are set. */
webhookAuth?: WebhookAuth;
};

View File

@@ -1,29 +1,4 @@
const GOOGLE_CHAT_WEBHOOK_HOST = 'chat.googleapis.com';
export const isValidGoogleChatWebhookURL = (url: string): boolean => {
try {
const { protocol, hostname } = new URL(url);
return (
protocol === 'https:' && hostname.toLowerCase() === GOOGLE_CHAT_WEBHOOK_HOST
);
} catch {
return false;
}
};
const JIRA_CLOUD_HOST_SUFFIX = '.atlassian.net';
export const isValidJiraSiteURL = (url: string): boolean => {
try {
const { protocol, hostname } = new URL(url);
return (
protocol === 'https:' &&
hostname.toLowerCase().endsWith(JIRA_CLOUD_HOST_SUFFIX)
);
} catch {
return false;
}
};
import { ChannelFormValues, WebhookAuth } from './types';
// mirrors go's prometheus model.Duration units
const JIRA_DURATION_UNIT_MS: Record<string, number> = {
@@ -94,21 +69,21 @@ export const canonicalJiraReopenDuration = (value: string): string => {
}, '');
};
const INCIDENTIO_EVENTS_PATH_PREFIX = '/v2/alert_events/http/';
// the backend enforces the same rule, this is only for a nicer error experience
export const isValidIncidentIOURL = (url: string): boolean => {
try {
const { protocol, pathname } = new URL(url);
const idx = pathname.indexOf(INCIDENTIO_EVENTS_PATH_PREFIX);
return (
protocol === 'https:' &&
idx !== -1 &&
pathname.length > idx + INCIDENTIO_EVENTS_PATH_PREFIX.length
);
} catch {
return false;
export const webhookAuthOf = (
values: Pick<
ChannelFormValues,
'webhookAuth' | 'username' | 'password' | 'bearerToken'
>,
): WebhookAuth => {
if (values.webhookAuth) {
return values.webhookAuth;
}
if (values.bearerToken) {
return WebhookAuth.bearer;
}
return values.username || values.password
? WebhookAuth.basic
: WebhookAuth.none;
};
export const filledRows = <T>(

View File

@@ -0,0 +1,93 @@
import { TFunction } from 'i18next';
import { ChannelFormValues, ChannelKind, WebhookAuth } from './types';
import { validateChannel } from './validation';
const t = ((key: string) => key) as TFunction;
describe('validateChannel', () => {
it.each<[string, ChannelKind, ChannelFormValues, string | null]>([
[
'a google chat webhook behind a proxy',
ChannelKind.googlechat,
{ webhookUrl: 'http://relay.internal/chat' },
null,
],
[
'a jira site that is not on atlassian.net',
ChannelKind.jira,
{
site: 'https://jira-proxy.acme.com/',
email: 'a@acme.com',
apiToken: 't',
project: 'OPS',
issueType: 'Bug',
},
null,
],
[
'any incident.io url',
ChannelKind.incidentio,
{ url: 'https://relay.internal/incidentio', token: 'tok' },
null,
],
[
'an incident.io token pasted with its Bearer prefix',
ChannelKind.incidentio,
{ url: 'https://relay.internal/incidentio', token: 'Bearer tok' },
'incidentio_token_bearer_prefix',
],
[
'an incident.io token with a trailing space',
ChannelKind.incidentio,
{ url: 'https://relay.internal/incidentio', token: 'tok ' },
'incidentio_token_whitespace',
],
[
'a webhook username without a password',
ChannelKind.webhook,
{ url: 'https://hook', webhookAuth: WebhookAuth.basic, username: 'u' },
null,
],
[
'basic auth with neither username nor password',
ChannelKind.webhook,
{ url: 'https://hook', webhookAuth: WebhookAuth.basic },
'webhook_basic_auth_required',
],
[
'bearer auth without a token',
ChannelKind.webhook,
{ url: 'https://hook', webhookAuth: WebhookAuth.bearer, username: 'u' },
'webhook_bearer_token_required',
],
[
'a slack field without a value',
ChannelKind.slack,
{ apiUrl: 'https://hooks', fields: [{ title: 'env', value: '' }] },
'slack_field_value_required',
],
[
'a slack button with neither url nor name',
ChannelKind.slack,
{
apiUrl: 'https://hooks',
actions: [{ type: 'button', text: 'Runbook', url: '' }],
},
'slack_action_url_or_name_required',
],
[
'a slack button with a name only',
ChannelKind.slack,
{
apiUrl: 'https://hooks',
actions: [{ type: 'button', text: 'Ack', name: 'ack' }],
},
null,
],
])('%s', (_, kind, values, expected) => {
expect(validateChannel(kind, { name: 'channel', ...values }, t)).toBe(
expected,
);
});
});

View File

@@ -3,7 +3,6 @@ import { Color } from '@signozhq/design-tokens';
import { Spin } from 'antd';
import { TIMEZONE_DATA } from 'components/CustomTimePicker/timezoneUtils';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { getRandomColor } from 'container/ExplorerOptions/utils';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
@@ -54,7 +53,9 @@ export function getColorForThreshold(thresholdLabel: string): string {
if (thresholdLabel === 'info') {
return Color.BG_ROBIN_500;
}
return getRandomColor();
const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
return Color[randomKey];
}
export function parseGoTime(input: string): {

View File

@@ -1,47 +0,0 @@
import { useEffect, useState } from 'react';
import ExplorerOptions, { ExplorerOptionsProps } from './ExplorerOptions';
import { getExplorerToolBarVisibility } from './utils';
type ExplorerOptionsWrapperProps = Omit<
ExplorerOptionsProps,
'isExplorerOptionDrop'
>;
function ExplorerOptionWrapper({
disabled,
query,
isLoading,
onExport,
sourcepage,
isOneChartPerQuery,
splitedQueries,
signalSource,
handleChangeSelectedView,
}: ExplorerOptionsWrapperProps): JSX.Element {
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
useEffect(() => {
const toolbarVisibility = getExplorerToolBarVisibility(sourcepage);
setIsExplorerOptionHidden(!toolbarVisibility);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ExplorerOptions
disabled={disabled}
query={query}
isLoading={isLoading}
onExport={onExport}
sourcepage={sourcepage}
signalSource={signalSource}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
isOneChartPerQuery={isOneChartPerQuery}
splitedQueries={splitedQueries}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
export default ExplorerOptionWrapper;

View File

@@ -1,234 +0,0 @@
.explorer-options-container {
position: fixed;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: var(--bottom-strip-height, 0px);
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
display: flex;
gap: 8px;
background-color: transparent;
.multi-alert-button,
.multi-dashboard-button {
min-width: 130px;
.ant-select-selector {
.ant-select-selection-placeholder {
margin-left: 0 !important;
}
.ant-select-arrow {
display: none !important;
}
}
}
&__divider {
--divider-vertical-margin: 0;
}
}
.hide-update {
left: calc(50% - 72px) !important;
}
.explorer-update {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: color-mix(in srgb, var(--l1-border) 60%, transparent);
border: 1px solid var(--l1-border);
border-radius: 4px;
backdrop-filter: blur(20px);
box-sizing: border-box;
.action-icon {
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
border-radius: 50px;
border: 1px solid var(--l1-border);
background: var(--l1-border);
cursor: pointer;
box-shadow: none;
}
.hidden {
display: none;
}
}
.explorer-options {
padding: 10px 12px;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
backdrop-filter: blur(20px);
cursor: default;
display: flex;
gap: 16px;
z-index: 1;
.ant-select-selector {
padding: 0 !important;
}
hr {
border-color: var(--l1-border);
}
.view-options,
.actions {
.info-icon {
margin: 8px;
}
.hidden {
display: none;
}
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
.ant-select-focused {
border-color: transparent !important;
.ant-select-selector {
border-color: transparent !important;
box-shadow: none !important;
}
}
.ant-select-selector {
border: transparent !important;
background-color: transparent !important;
.ant-select-selection-placeholder {
margin-left: 12px;
}
}
}
.hidden {
display: none;
}
button {
display: flex;
align-items: center;
justify-content: center;
}
}
.app-content {
&.collapsed {
.explorer-options-container {
left: calc(50% + 72px);
}
}
}
.render-options {
display: flex;
align-items: center;
gap: 8px;
padding: 0 2px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.dot {
margin-left: 6px;
min-height: 6px;
min-width: 6px;
border-radius: 50%;
backdrop-filter: blur(20px);
}
}
.save-view-modal {
width: 384px !important;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
}
.ant-modal-body {
padding: 12px 16px 0px 16px;
.ant-typography {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
.save-view-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
.save-button {
display: flex;
}
}
}
.title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
.explorer-option-droppable-container {
position: fixed;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: var(--bottom-strip-height, 0px);
width: -webkit-fill-available;
height: 24px;
display: flex;
justify-content: center;
border-radius: 10px 10px 0px 0px;
// box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
// backdrop-filter: blur(20px);
.explorer-actions-btn {
display: flex;
gap: 8px;
margin-right: 8px;
.action-btn {
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px 10px 0px 0px;
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
height: 24px !important;
border: none;
}
}
.explorer-show-btn {
border-radius: 6px 6px 0px 0px;
border: 1px solid var(--l1-border);
background: color-mix(in srgb, var(--l1-border) 40%, transparent);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
align-self: center;
padding: 8px 12px;
height: 24px !important;
.menu-bar {
border-radius: 50px;
background: var(--l3-background);
height: 4px;
width: 50px;
}
}
}

View File

@@ -1,84 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button, Tooltip } from 'antd';
import { Disc3, X } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';
import { setExplorerToolBarVisibility } from './utils';
import './ExplorerOptionsHideArea.styles.scss';
interface DroppableAreaProps {
viewName: string;
isQueryUpdated: boolean;
isExplorerOptionHidden?: boolean;
sourcepage: DataSource;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
handleClearSelect: () => void;
onUpdateQueryHandler: () => void;
isEditDeleteSupported: boolean;
}
function ExplorerOptionsHideArea({
viewName,
isQueryUpdated,
isExplorerOptionHidden,
sourcepage,
setIsExplorerOptionHidden,
handleClearSelect,
onUpdateQueryHandler,
isEditDeleteSupported,
}: DroppableAreaProps): JSX.Element {
const handleShowExplorerOption = (): void => {
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(false);
setExplorerToolBarVisibility(true, sourcepage);
}
};
return (
<div className="explorer-option-droppable-container">
{isExplorerOptionHidden && (
<>
{viewName && (
<div className="explorer-actions-btn">
<Tooltip title="Clear this view">
<Button
onClick={handleClearSelect}
className="action-btn"
style={{ background: Color.BG_CHERRY_500 }}
icon={<X size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
{isEditDeleteSupported && isQueryUpdated && (
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}
className="action-btn"
style={{ background: Color.BG_ROBIN_500 }}
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
)}
</div>
)}
<Button
// style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }}
className="explorer-show-btn"
onClick={handleShowExplorerOption}
data-testid="show-explorer-option"
>
<div className="menu-bar" />
</Button>
</>
)}
</div>
);
}
ExplorerOptionsHideArea.defaultProps = {
isExplorerOptionHidden: undefined,
setIsExplorerOptionHidden: undefined,
};
export default ExplorerOptionsHideArea;

View File

@@ -1,391 +0,0 @@
import { useHistory } from 'react-router-dom';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { MOCK_QUERY } from 'container/QueryTable/Drilldown/__tests__/mockTableData';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { rest, server } from 'mocks-server/server';
import {
defaultFeatureFlags,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { buildExportPanelLink } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
import ExplorerOptionWrapper from '../ExplorerOptionWrapper';
import { getExplorerToolBarVisibility } from '../utils';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: jest.fn(),
}));
jest.mock('../utils', () => ({
getExplorerToolBarVisibility: jest.fn(),
generateRGBAFromHex: jest.fn(() => 'rgba(0, 0, 0, 0.08)'),
getRandomColor: jest.fn(() => '#000000'),
saveNewViewHandler: jest.fn(),
setExplorerToolBarVisibility: jest.fn(),
DATASOURCE_VS_ROUTES: {},
}));
const mockGetExplorerToolBarVisibility = jest.mocked(
getExplorerToolBarVisibility,
);
const mockUseHistory = jest.mocked(useHistory);
// Mock data
const TEST_QUERY_ID = 'test-query-id';
const TEST_DASHBOARD_ID = 'test-dashboard-id';
const TEST_DASHBOARD_TITLE_2 = 'Test Dashboard for Export';
const NEW_DASHBOARD_ID = 'new-dashboard-id';
// The export dialog talks to the generated Perses-spec dashboard endpoints.
const V2_LIST_ENDPOINT = '*/api/v2/users/me/dashboards';
const V2_CREATE_ENDPOINT = '*/api/v2/dashboards';
// "Create new dashboard" names the dashboard from the `new_dashboard_title` i18n
// key; the test-utils react-i18next mock returns the key verbatim.
const NEW_DASHBOARD_TITLE = 'new_dashboard_title';
// Use the existing mock query from the codebase
const mockQuery: Query = {
...MOCK_QUERY,
id: TEST_QUERY_ID, // Override with our test ID
} as Query;
const ADD_TO_DASHBOARD_BUTTON_NAME = /add to dashboard/i;
interface PickerDashboard {
id: string;
title: string;
}
/** Register the "list dashboards" endpoint the picker reads. */
function mockDashboardList(dashboards: PickerDashboard[]): void {
server.use(
rest.get(V2_LIST_ENDPOINT, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
dashboards: dashboards.map(({ id, title }) => ({
id,
name: title,
spec: { display: { name: title } },
})),
reservedKeywords: [],
},
}),
),
),
);
}
/** Register the "create dashboard" endpoint the "New dashboard" button hits. */
function mockCreateDashboard(id: string): void {
server.use(
rest.post(V2_CREATE_ENDPOINT, (_req, res, ctx) =>
res(ctx.status(201), ctx.json({ status: 'success', data: { id } })),
),
);
}
// Helper function to render component with props
const renderExplorerOptionWrapper = (
overrides = {},
): ReturnType<typeof render> => {
const props = {
disabled: false,
query: mockQuery,
isLoading: false,
onExport: jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
>,
sourcepage: DataSource.LOGS,
isOneChartPerQuery: false,
splitedQueries: [],
signalSource: 'test-signal',
...overrides,
};
return render(
<ExplorerOptionWrapper
disabled={props.disabled}
query={props.query}
isLoading={props.isLoading}
onExport={props.onExport}
sourcepage={props.sourcepage}
isOneChartPerQuery={props.isOneChartPerQuery}
splitedQueries={props.splitedQueries}
signalSource={props.signalSource}
/>,
undefined,
{ appContextOverrides: { featureFlags: defaultFeatureFlags } },
);
};
describe('ExplorerOptionWrapper', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetExplorerToolBarVisibility.mockReturnValue(true);
});
it('should navigate to alert creation page when "Create an Alert" is clicked in logs-explorer', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockPush = jest.fn();
mockUseHistory.mockReturnValue({
push: mockPush,
} as unknown as ReturnType<typeof useHistory>);
renderExplorerOptionWrapper({ sourcepage: DataSource.LOGS });
const createAlertButton = screen.getByRole('button', {
name: 'Create an Alert',
});
await user.click(createAlertButton);
expect(mockPush).toHaveBeenCalledTimes(1);
const calledWith = mockPush.mock.calls[0][0] as string;
const [path, search = ''] = calledWith.split('?');
expect(path).toBe('/alerts/new');
const params = new URLSearchParams(search);
expect(params.has('compositeQuery')).toBe(true);
});
describe('onExport functionality', () => {
it('should call onExport when New Dashboard button is clicked in export modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
>;
mockCreateDashboard(NEW_DASHBOARD_ID);
renderExplorerOptionWrapper({ onExport: testOnExport });
// Find and click the "Add to Dashboard" button
const addToDashboardButton = screen.getByRole('button', {
name: ADD_TO_DASHBOARD_BUTTON_NAME,
});
await user.click(addToDashboardButton);
// Wait for the export modal to appear
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Click the "New dashboard" button
const newDashboardButton = screen.getByTestId('export-panel-new-dashboard');
await user.click(newDashboardButton);
// Wait for the API call to complete and onExport to be called
await waitFor(() => {
expect(testOnExport).toHaveBeenCalledWith(
{ id: NEW_DASHBOARD_ID, title: NEW_DASHBOARD_TITLE },
true,
);
});
});
it('should call onExport when selecting existing dashboard and clicking Export button', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
>;
// Mock existing dashboards with unique titles
const dashboards: PickerDashboard[] = [
{ id: 'dashboard-1', title: 'Dashboard 1' },
{ id: 'dashboard-2', title: 'Dashboard 2' },
];
mockDashboardList(dashboards);
renderExplorerOptionWrapper({ onExport: testOnExport });
// Find and click the "Add to Dashboard" button
const addToDashboardButton = screen.getByRole('button', {
name: ADD_TO_DASHBOARD_BUTTON_NAME,
});
await user.click(addToDashboardButton);
// Wait for the export modal to appear
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Wait for the dashboard select to render AND finish loading. antd
// disables the Select while the list request is in flight, and a click
// on a disabled combobox is a no-op — so sync on the ready (enabled)
// state before clicking.
const modal = screen.getByRole('dialog');
await waitFor(() => {
const combobox = modal.querySelector('[role="combobox"]');
expect(combobox).toBeTruthy();
expect(combobox).not.toBeDisabled();
});
const dashboardSelect = modal.querySelector(
'[role="combobox"]',
) as HTMLElement;
expect(dashboardSelect).toBeInTheDocument();
await user.click(dashboardSelect);
// Wait for the dropdown options to appear and select the first dashboard
await waitFor(() => {
expect(screen.getByText(dashboards[0].title)).toBeInTheDocument();
});
// Click on the first dashboard option
const dashboardOption = screen.getByText(dashboards[0].title);
await user.click(dashboardOption);
// Wait for the selection to be made and the export button to be enabled
await waitFor(() => {
expect(screen.getByTestId('export-panel-export')).not.toHaveAttribute(
'aria-disabled',
'true',
);
});
// Click the export button
const exportButton = screen.getByTestId('export-panel-export');
await user.click(exportButton);
// Wait for onExport to be called with the selected dashboard
await waitFor(() => {
expect(testOnExport).toHaveBeenCalledWith(
{ id: dashboards[0].id, title: dashboards[0].title },
false,
);
});
});
it('should navigate to the panel editor via the export link without writing the dashboard', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
// Mock the safeNavigate function
const mockSafeNavigate = jest.fn();
const panelTypeParam = PANEL_TYPES.TIME_SERIES;
const query = mockQuery;
// Create a real handleExport function similar to LogsExplorerViews
// Export navigates only; it must not write the dashboard (PR #8029).
const handleExport = (dashboard: ExportDashboard | null): void => {
if (!dashboard) {
return;
}
// Call the real link builder (not mocked)
const dashboardEditView = buildExportPanelLink({
query,
panelType: panelTypeParam,
dashboardId: dashboard.id,
});
// Simulate navigation
mockSafeNavigate(dashboardEditView);
};
// Mock existing dashboards
const dashboards: PickerDashboard[] = [
{ id: TEST_DASHBOARD_ID, title: TEST_DASHBOARD_TITLE_2 },
];
mockDashboardList(dashboards);
renderExplorerOptionWrapper({ onExport: handleExport });
// Find and click the "Add to Dashboard" button
const addToDashboardButton = screen.getByRole('button', {
name: ADD_TO_DASHBOARD_BUTTON_NAME,
});
await user.click(addToDashboardButton);
// Wait for the export modal to appear
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Wait for the dashboard select to render AND finish loading (see note above).
const modal = screen.getByRole('dialog');
await waitFor(() => {
const combobox = modal.querySelector('[role="combobox"]');
expect(combobox).toBeTruthy();
expect(combobox).not.toBeDisabled();
});
const dashboardSelect = modal.querySelector(
'[role="combobox"]',
) as HTMLElement;
expect(dashboardSelect).toBeInTheDocument();
await user.click(dashboardSelect);
// Wait for the dropdown options to appear and select the dashboard
await waitFor(() => {
expect(screen.getByText(dashboards[0].title)).toBeInTheDocument();
});
// Click on the dashboard option
const dashboardOption = screen.getByText(dashboards[0].title);
await user.click(dashboardOption);
// Wait for the selection to be made and the export button to be enabled
await waitFor(() => {
expect(screen.getByTestId('export-panel-export')).not.toHaveAttribute(
'aria-disabled',
'true',
);
});
// Click the export button
const exportButton = screen.getByTestId('export-panel-export');
await user.click(exportButton);
// Wait for the handleExport function to be called and navigation to occur
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
// V2 panel-editor link: compositeQuery is double-encoded (see newPanelRoute).
expect(mockSafeNavigate).toHaveBeenCalledWith(
`/dashboard/${TEST_DASHBOARD_ID}/panel/new?panelKind=signoz%2FTimeSeriesPanel&compositeQuery=${encodeURIComponent(
encodeURIComponent(JSON.stringify(query)),
)}`,
);
});
});
});
it('should not show export buttons when component is disabled', () => {
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
>;
renderExplorerOptionWrapper({ disabled: true, onExport: testOnExport });
// The "Add to Dashboard" button should be disabled
const addToDashboardButton = screen.getByRole('button', {
name: ADD_TO_DASHBOARD_BUTTON_NAME,
});
expect(addToDashboardButton).toBeDisabled();
});
});

View File

@@ -1,6 +0,0 @@
export enum PreservedViewsTypes {
LOGS = 'logs',
TRACES = 'traces',
METRICS = 'metrics',
METER = 'meter',
}

View File

@@ -1,47 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { UseMutateAsyncFunction } from 'react-query';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { SaveViewWithNameProps } from 'components/ExplorerCard/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import { PreservedViewsTypes } from './constants';
export interface SaveNewViewHandlerProps {
viewName: string;
compositeQuery: ICompositeMetricQuery;
sourcePage: DataSource | 'meter';
extraData: SaveViewProps['extraData'];
panelType: PANEL_TYPES | null;
notifications: NotificationInstance;
refetchAllView: SaveViewWithNameProps['refetchAllView'];
saveViewAsync: UseMutateAsyncFunction<
AxiosResponse<SaveViewPayloadProps>,
Error,
SaveViewProps,
SaveViewPayloadProps
>;
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
setNewViewName: Dispatch<SetStateAction<string>>;
}
export type PreservedViewType =
| PreservedViewsTypes.LOGS
| PreservedViewsTypes.TRACES
| PreservedViewsTypes.METRICS
| PreservedViewsTypes.METER;
export type PreservedViewsInLocalStorage = Partial<
Record<PreservedViewType, { key: string; value: string }>
>;
export type ChangeViewFunctionType = (
view: ExplorerViews,
querySearchParameters?: ICurrentQueryData,
) => void;

View File

@@ -1,123 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { DataSource } from 'types/common/queryBuilder';
import { SaveNewViewHandlerProps } from './types';
export const getRandomColor = (): string => {
const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
return Color[randomKey];
};
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER_EXPLORER,
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
};
export const saveNewViewHandler = ({
saveViewAsync,
refetchAllView,
notifications,
handlePopOverClose,
viewName,
compositeQuery,
sourcePage,
extraData,
redirectWithQueryBuilderData,
panelType,
setNewViewName,
}: SaveNewViewHandlerProps): void => {
saveViewAsync(
{
viewName,
compositeQuery,
sourcePage: sourcePage as DataSource,
extraData,
},
{
onSuccess: (data) => {
refetchAllView();
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
[QueryParams.panelTypes]: panelType,
[QueryParams.viewName]: viewName,
[QueryParams.viewKey]: data.data.data,
});
notifications.success({
message: 'View Saved Successfully',
});
},
onError: (err) => {
showErrorNotification(notifications, err);
},
onSettled: () => {
handlePopOverClose();
setNewViewName('');
},
},
);
};
export const generateRGBAFromHex = (hex: string, opacity: number): string =>
`rgba(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(
hex.slice(3, 5),
16,
)}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`;
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
try {
const showExplorerToolbar = getLocalStorageKey(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar === null) {
const parsedShowExplorerToolbar: {
[DataSource.LOGS]: boolean;
[DataSource.TRACES]: boolean;
[DataSource.METRICS]: boolean;
} = {
[DataSource.METRICS]: true,
[DataSource.TRACES]: true,
[DataSource.LOGS]: true,
};
setLocalStorageKey(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
return true;
}
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar || '{}');
return parsedShowExplorerToolbar[dataSource];
} catch (error) {
console.error(error);
return false;
}
};
export const setExplorerToolBarVisibility = (
value: boolean,
dataSource: string,
): void => {
try {
const showExplorerToolbar = getLocalStorageKey(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar) {
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
parsedShowExplorerToolbar[dataSource] = value;
setLocalStorageKey(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
return;
}
} catch (error) {
console.error(error);
}
};

View File

@@ -26,7 +26,11 @@ const COLUMNS = [
function SlackActions(): JSX.Element {
const { t } = useTranslation('channels');
const { control, register } = useFormContext<ChannelSpecFormValues>();
const {
control,
register,
formState: { errors },
} = useFormContext<ChannelSpecFormValues>();
const { fields, append, remove } = useFieldArray({ control, name: 'actions' });
const keyFor = (column: ActionColumn): string => column.replace('.text', '');
@@ -51,6 +55,9 @@ function SlackActions(): JSX.Element {
key={column}
label={t(`label_slack_action_${keyFor(column)}`)}
tooltip={t(`tooltip_slack_action_${keyFor(column)}`)}
error={
column === 'url' ? errors.actions?.[index]?.url?.message : undefined
}
>
<Input
{...register(`actions.${index}.${column}` as const)}

View File

@@ -14,7 +14,11 @@ import styles from './RowEditor.module.scss';
// Slack renders these as the attachment's table of short or full-width entries.
function SlackFields(): JSX.Element {
const { t } = useTranslation('channels');
const { control, register } = useFormContext<ChannelSpecFormValues>();
const {
control,
register,
formState: { errors },
} = useFormContext<ChannelSpecFormValues>();
const { fields, append, remove } = useFieldArray({ control, name: 'fields' });
return (
@@ -46,6 +50,7 @@ function SlackFields(): JSX.Element {
<FormField
label={t('label_slack_field_value')}
tooltip={t('tooltip_slack_field_value')}
error={errors.fields?.[index]?.value?.message}
>
<Input
{...register(`fields.${index}.value`)}

View File

@@ -0,0 +1,3 @@
.auth {
--radio-group-display: flex;
}

View File

@@ -1,17 +1,30 @@
import { useTranslation } from 'react-i18next';
import { useFormContext } from 'react-hook-form';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { Input } from '@signozhq/ui/input';
import { RadioGroup } from '@signozhq/ui/radio-group';
import { ChannelSpecFormValues } from '../../CreateAlertChannels/types';
import {
ChannelFormValues,
WebhookAuth,
} from '../../CreateAlertChannels/types';
import { webhookAuthOf } from '../../CreateAlertChannels/utils';
import FormCard from '../FormCard';
import FormField from '../FormField';
import styles from './Webhook.module.scss';
function WebhookSettings(): JSX.Element {
const { t } = useTranslation('channels');
const {
register,
control,
formState: { errors },
} = useFormContext<ChannelSpecFormValues>();
} = useFormContext<ChannelFormValues>();
const [webhookAuth, username, password, bearerToken] = useWatch({
control,
name: ['webhookAuth', 'username', 'password', 'bearerToken'],
});
const auth = webhookAuthOf({ webhookAuth, username, password, bearerToken });
return (
<>
@@ -28,35 +41,75 @@ function WebhookSettings(): JSX.Element {
data-testid="webhook-url-textbox"
/>
</FormField>
<FormField
label={t('field_webhook_username')}
help={t('help_webhook_username')}
error={errors.username?.message}
>
<Input {...register('username')} data-testid="webhook-username-textbox" />
</FormField>
<FormField
label="Password (optional)"
help={t('help_webhook_password')}
error={errors.password?.message}
>
<Input
{...register('password')}
type="password"
data-testid="webhook-password-textbox"
/>
</FormField>
<FormField
label={t('field_webhook_bearer_token')}
help={t('help_webhook_bearer_token')}
error={errors.bearerToken?.message}
>
<Input
{...register('bearerToken')}
type="password"
data-testid="webhook-bearer-token-textbox"
<FormField label={t('field_webhook_auth')}>
<Controller
control={control}
name="webhookAuth"
render={({ field }): JSX.Element => (
<div className={styles.auth}>
<RadioGroup
color="primary"
aria-label={t('field_webhook_auth')}
value={auth}
onChange={field.onChange}
testId="webhook-auth-radio"
items={[
{
value: WebhookAuth.none,
label: t('option_webhook_auth_none'),
},
{
value: WebhookAuth.basic,
label: t('option_webhook_auth_basic'),
},
{
value: WebhookAuth.bearer,
label: t('option_webhook_auth_bearer'),
},
]}
/>
</div>
)}
/>
</FormField>
{auth === WebhookAuth.basic && (
<>
<FormField
label={t('field_webhook_username')}
error={errors.username?.message}
>
<Input
{...register('username')}
data-testid="webhook-username-textbox"
/>
</FormField>
<FormField
label={t('field_webhook_password')}
error={errors.password?.message}
>
<Input
{...register('password')}
type="password"
data-testid="webhook-password-textbox"
/>
</FormField>
</>
)}
{auth === WebhookAuth.bearer && (
<FormField
label={t('field_webhook_bearer_token')}
help={t('help_webhook_bearer_token')}
error={errors.bearerToken?.message}
required
>
<Input
{...register('bearerToken')}
type="password"
aria-required
data-testid="webhook-bearer-token-textbox"
/>
</FormField>
)}
</FormCard>
</>
);

View File

@@ -10,9 +10,8 @@ import {
import ROUTES from 'constants/routes';
import { getSavedViewQuery } from 'container/SavedViews/utils/getSavedViewQuery';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
import { SOURCEPAGE_VS_ROUTES } from 'container/SavedViews/constants';
import Card from 'periscope/components/Card/Card';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
@@ -36,7 +35,6 @@ export default function SavedViews({
loadingUserPreferences: boolean;
}): JSX.Element {
const { user } = useAppContext();
const isSavedViewEnabled = useSavedViewEnabled();
const [selectedEntity, setSelectedEntity] = useState<string>('logs');
const [selectedEntityViews, setSelectedEntityViews] = useState<
SavedviewtypesSavedViewDTO[]
@@ -104,7 +102,6 @@ export default function SavedViews({
view.spec.panelType,
{
query: getSavedViewQuery(view),
viewName: view.spec.displayName,
viewKey: view.id,
},
SOURCEPAGE_VS_ROUTES[selectedEntity],
@@ -117,19 +114,6 @@ export default function SavedViews({
}
}, [hasSavedViews, onUpdateChecklistDoneItem, loadingUserPreferences]);
const footerLink = useMemo(() => {
if (selectedEntity === 'logs') {
return ROUTES.LOGS_SAVE_VIEWS;
}
if (selectedEntity === 'traces') {
return ROUTES.TRACES_SAVE_VIEWS;
}
if (selectedEntity === 'metrics') {
return ROUTES.METRICS_EXPLORER_VIEWS;
}
return '';
}, [selectedEntity]);
const getStartedLink = useMemo(() => {
if (selectedEntity === 'logs') {
return ROUTES.LOGS_EXPLORER;
@@ -352,26 +336,6 @@ export default function SavedViews({
<Card.Content>
{selectedEntityViews.length > 0 ? renderSavedViews() : emptyStateCard()}
</Card.Content>
{!isSavedViewEnabled && selectedEntityViews.length > 0 && (
<Card.Footer>
<div className="services-footer home-data-card-footer">
<Link to={footerLink}>
<Button
type="link"
className="periscope-btn link learn-more-link"
onClick={(): void => {
logEvent('Homepage: All saved views clicked', {
entity: selectedEntity,
});
}}
>
All Views <ArrowRight size={12} />
</Button>
</Link>
</div>
</Card.Footer>
)}
</Card>
);
}

View File

@@ -23,7 +23,6 @@
.traces-explorer-views {
padding: 8px;
padding-bottom: 60px;
margin-bottom: 24px;
.ant-tabs-tabpane {

View File

@@ -3,7 +3,6 @@ import { useQueryClient } from 'react-query';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
@@ -225,11 +224,9 @@ function Explorer(): JSX.Element {
}
/>
</div>
<ExplorerCard sourcepage={DataSource.TRACES}>
<div className="query-section-container">
<QuerySection />
</div>
</ExplorerCard>
<div className="query-section-container">
<QuerySection />
</div>
<div className="traces-explorer-views">
{isCancelled && (

View File

@@ -6,7 +6,7 @@ import FieldsSelector from 'components/FieldsSelector';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import GoToTop from 'container/GoToTop';
import { useOptionsMenu } from 'container/OptionsMenu';
import { LOGS_REQUIRED_COLUMNS } from 'container/OptionsMenu/constants';

View File

@@ -1,4 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { ILog } from 'types/api/logs/log';
export interface ILiveLogsLog {

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { DataViewer } from 'periscope/components/DataViewer';
import { ILog } from 'types/api/logs/log';

View File

@@ -2,9 +2,11 @@ import { useCallback, useMemo } from 'react';
import { CircleMinus, CirclePlus, Layers, RefreshCw } from '@signozhq/icons';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { FeatureKeys } from 'constants/features';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import {
ChangeViewFunctionType,
ICurrentQueryData,
} from 'hooks/useHandleExplorerTabChange';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import {
FieldContext,

View File

@@ -1,4 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import APIError from 'types/api/error';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';

View File

@@ -16,7 +16,6 @@ import setToLocalstorage from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
import { QueryParams } from 'constants/query';
import {
initialFilters,
@@ -24,8 +23,7 @@ import {
PANEL_TYPES,
} from 'constants/queryBuilder';
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import GoToTop from 'container/GoToTop';
import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList';
@@ -39,13 +37,9 @@ import {
} from 'container/LogsExplorerViews/explorerUtils';
import ExplorerActions from 'container/ExplorerActions/ExplorerActions';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import useUrlQueryData from 'hooks/useUrlQueryData';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { isEmpty, isUndefined } from 'lodash-es';
@@ -59,7 +53,6 @@ import { Filter } from 'types/api/v5/queryRange';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 } from 'uuid';
import { getListOrderBy, setListOrderBy } from 'utils/explorerUtils';
import LogsActionsContainer from './LogsActionsContainer';
@@ -82,10 +75,6 @@ function LogsExplorerViewsContainer({
showLiveLogs: boolean;
handleChangeSelectedView: ChangeViewFunctionType;
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const isSavedViewEnabled = useSavedViewEnabled();
const getExportToDashboardLink = useGetExportToDashboardLink();
const [showFrequencyChart, setShowFrequencyChart] = useState(
() => getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART) === 'true',
);
@@ -276,49 +265,6 @@ function LogsExplorerViewsContainer({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload]);
const handleExport = useCallback(
(dashboard: ExportDashboard | null, isNewDashboard?: boolean): void => {
if (!dashboard || !selectedPanelType) {
return;
}
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(
selectedPanelType,
)
? selectedPanelType
: PANEL_TYPES.TIME_SERIES;
const widgetId = v4();
if (!exportDefaultQuery) {
return;
}
logEvent('Logs Explorer: Add to dashboard successful', {
panelType: selectedPanelType,
isNewDashboard,
dashboardName: dashboard?.title,
});
const dashboardEditView = getExportToDashboardLink({
query: exportDefaultQuery,
panelType: panelTypeParam,
dashboardId: dashboard.id,
widgetId,
});
if (dashboardEditView) {
safeNavigate(dashboardEditView);
}
},
[
safeNavigate,
exportDefaultQuery,
selectedPanelType,
getExportToDashboardLink,
],
);
useEffect(() => {
const currentData = data?.payload?.data?.newResult?.data?.result || [];
if (currentData.length > 0 && currentData[0].list) {
@@ -517,16 +463,6 @@ function LogsExplorerViewsContainer({
</div>
<GoToTop />
{!isSavedViewEnabled && (
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
onExport={handleExport}
sourcepage={DataSource.LOGS}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</div>
);
}

View File

@@ -138,13 +138,6 @@ jest.mock(
return <div>MockQueryBuilderSearchV2</div>;
},
);
jest.mock(
'container/ExplorerOptions/ExplorerOptionWrapper',
() =>
function MockExplorerOptionWrapper(): JSX.Element {
return <div>MockExplorerOptionWrapper</div>;
},
);
jest.mock(
'components/QuickFilters/QuickFilters',
() =>
@@ -416,7 +409,6 @@ function LogsExplorerWithMockContext({
updateQueriesData: (): Query => initialQueriesMap.logs,
initQueryBuilderData: noop,
handleOnUnitsChange: noop,
isStagedQueryUpdated: (): boolean => false,
}),
[currentQuery, stagedQuery, handleRunQuery],
);

View File

@@ -200,5 +200,4 @@ export const mockQueryBuilderContextValue = {
updateQueriesData: (): Query => initialQueriesMap.logs,
initQueryBuilderData: noop,
handleOnUnitsChange: noop,
isStagedQueryUpdated: (): boolean => false,
};

View File

@@ -1,8 +1,5 @@
.meter-explorer-container {
.meter-explorer-content-section {
// Clearance for the fixed ExplorerOptions bar.
padding-bottom: 80px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);

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