Compare commits

..

63 Commits

Author SHA1 Message Date
Abhi Kumar
a380c7d194 chore: pr review changes greptile 2026-10-29 12:10:19 +05:30
Abhi Kumar
6d3e52329d fix(dashboard): hide the trace operator on scatter plots 2026-10-28 16:48:27 +05:30
Abhi Kumar
f98df739e4 fix(dashboard): count disabled queries in the scatter plot group-by check 2026-10-28 16:48:27 +05:30
Abhi Kumar
d7a6b1a7fc test(dashboard): find range slider thumbs within its test ID 2026-10-28 16:48:27 +05:30
Abhi Kumar
71dab81602 perf(dashboard): prepare scatter plot data once per response 2026-10-28 16:48:27 +05:30
Abhi Kumar
1b5c88d085 refactor(dashboard): rename scatter click helper and type legend series mocks 2026-10-28 16:48:27 +05:30
Abhi Kumar
322f10b0b2 chore: pr review changes 2026-10-28 16:48:27 +05:30
Abhi Kumar
394722b6c4 fix(dashboard): drop PromQL from scatter plot query types 2026-10-28 16:48:27 +05:30
Abhi Kumar
8342796491 fix(dashboard): declare scatter plot query modes 2026-10-28 16:48:27 +05:30
Abhi Kumar
35327249e2 refactor(dashboard): trim scatter plot panel comments and share the joined table helper 2026-10-28 16:48:27 +05:30
Abhi Kumar
6f69e9bfdb fix(dashboard): warn of the scatter plot cap only when groups were capped 2026-10-28 16:48:27 +05:30
Abhi Kumar
e5571d044a feat(dashboard): let scatter plot thresholds mark the x or y axis 2026-10-28 16:48:27 +05:30
Abhi Kumar
71b872e910 feat(dashboard): use colorBy and sizeBy in the scatter plot, labelled Color by and Size by 2026-10-28 16:47:52 +05:30
Abhi Kumar
3865d9d733 feat(dashboard): explain mismatched group bys in the scatter plot 2026-10-28 16:47:52 +05:30
Abhi Kumar
2f31e49c7e refactor(dashboard): move scatter plot panel helpers into utils 2026-10-28 16:47:52 +05:30
Abhi Kumar
a24fefe06a style(charts): match scatter tooltip spacing and marker to the heatmap tooltip and legend 2026-10-28 16:47:52 +05:30
Abhi Kumar
5ec9b967f4 fix(charts): drop the scatter tooltip size row when it repeats x or y 2026-10-28 16:47:52 +05:30
Abhi Kumar
3399a862bd feat(dashboard): sync scatter plot group by across queries
Panel kinds declare which builder fields to keep equal across queries. A query
added or switched to another signal takes the others' values, since a signal
switch reloads its own.
2026-10-28 16:47:52 +05:30
Abhi Kumar
031edcd556 feat(dashboard): warn when a scatter plot dimension isn't in the results 2026-10-28 16:47:52 +05:30
Abhi Kumar
3cac873abc feat(query-builder): show the scatter plot's default limit as the limit placeholder 2026-10-28 16:47:52 +05:30
Abhi Kumar
1c8b73b810 feat(dashboard): move scatter plot notes from the footer into the panel warning 2026-10-28 16:47:52 +05:30
Abhi Kumar
303b7b17a5 fix(dashboard): limit scatter plot queries to 10,000 groups on the server
Only a lone builder query gets the default: per-query limits on several
queries can keep different groups and break the join.
2026-10-28 16:47:51 +05:30
Abhi Kumar
659dbca2d1 fix(dashboard): cap the scatter plot at 10,000 dots
A canvas path of a few hundred thousand dots fails to paint and takes the later series with it.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
5fa4a2f37c fix(dashboard): redraw the scatter plot after an empty state
The chart area stays mounted, since the size observer binds to its node only once.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
13a15ed49c refactor(dashboard): build scatter plot axes from the shared axis fields
Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
c34c87f805 feat(dashboard): lay out scatter plot axes with label, range and scale rows
Range help says when a log axis ignores a bound at or below zero.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
504be5afbe feat(dashboard): restyle scatter plot dimensions with field info and auto labels
Colour is a multi-select of group-by keys; Auto options name the column they resolve to.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
ccabe0dce1 feat(dashboard): hide legend format for scatter plot
It only renames a value column here; axis labels name what each axis measures.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
618f2f1b91 fix(dashboard): send reduceTo with scatter plot metric queries
The V5 conversion kept reduceTo only for table, pie and value panels, so the
API rejected a scatter plot's scalar metric query.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
d3a17da729 feat(query-builder): treat scatter plot like table in the builder
Reduce to for metrics, and no step interval for traces or logs, since both
send the same scalar request.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
af3f6ae82c feat(dashboard): drill down and download CSV from a scatter plot
A dot drills into its group through Y's query, or X's when Y has none. A
breakout opens as a table, since regrouping one query would break the join.
CSV keeps both of two columns that share a name.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
dd241f5219 fix(dashboard): carry builder fields on a switch to scatter plot
Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
6f3985a53b fix(dashboard): keep column units across a Table/Scatter Plot switch
Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
3ce1c1d00e feat(dashboard): edit scatter plot point size and opacity
A single size slider until a size column is bound, then a min/max range.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
750c91302e feat(dashboard): add scatter plot panel
One dot per group from the joined scalar table. Dimensions bind to Table's
column keys so column units carry across a Table/Scatter switch.

Assisted-by: Claude Opus 5.5
2026-10-28 16:47:51 +05:30
Abhi Kumar
edf0726912 refactor(charts): move scatter story data helpers into a mocks file 2026-10-28 16:47:47 +05:30
Abhi Kumar
312c3ce104 fix(charts): keep the path builder doc on getPathBuilder 2026-10-28 16:45:24 +05:30
Abhi Kumar
29630edf54 chore: pr review changes 2026-10-28 16:45:24 +05:30
Abhi Kumar
8dc0d42b65 style(charts): format scatter chart data with oxfmt 0.73 2026-10-28 16:45:24 +05:30
Abhi Kumar
a1ec46c20d refactor(charts): trim scatter chart layer comments and dead code 2026-10-28 16:45:24 +05:30
Abhi Kumar
d053a9415f fix(charts): pick the scatter auto scale by how the points spread
Max/min decades let one outlier decide and never left linear with zeros.
Compare middle-half coverage instead, falling back to symlog.
2026-10-28 16:45:24 +05:30
Abhi Kumar
056c05bb17 fix(charts): restore the default log axis grid
Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
77a91d78da fix(dashboard): honour scatter plot soft limits on every axis scale
Only Auto weighs the soft limits when choosing a scale; an explicit scale keeps its choice.

Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
8e24d18919 feat(charts): draw axis lines in the grid's style by default
Padding the scatter range moved the plot edges off the outer grid lines, which
had stood in for the axes.

Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
d505402b41 fix(charts): keep scatter dots whole at the plot edges
uPlot clips series to the plot area, so a dot at an extreme value lost the half
past the edge. Each scatter scale now pads by the largest dot's radius in
pixels. Log axes are no longer snapped to powers of ten, since snapping after
the pad could add a whole empty decade; they place 1/2/5 ticks themselves.

Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
2dc7da77c8 fix(charts): draw log axis grid lines at powers of ten only
uPlot splits a log axis at every 1–9 × 10ⁿ, and a line on each buried the
points under a mesh.

Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
e7c3bd7756 feat(charts): report the scatter point a click lands on
The shared click data finds its series by x position, which a faceted plot has
no axis for; the hit is read off the cursor instead.

Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
7372b4e08e fix(charts): forward plotRef and skip x lookups on faceted clicks
ChartWrapper dropped its plotRef prop, and a click on a faceted plot threw in
posToIdx, which reads the null data[0].

Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
169267e9a5 fix(charts): outline the hovered scatter point
Snap the ring to whole pixels so uPlot's ceil on left/top doesn't shift it
off the disc.

Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
db59e27e94 feat(charts): scatter axis scales and titles
auto picks log only for positive values spanning three decades; log falls
back to symlog when given values at or below zero.

Assisted-by: Claude Opus 5.5
2026-10-28 16:45:24 +05:30
Abhi Kumar
ccc5f55fb7 chore: scatter plot base work 2026-10-28 16:45:24 +05:30
Naman Verma
6f9ad48079 fix: dont allow trace operator 2026-10-28 14:54:52 +05:30
Abhi Kumar
5b6477900b fix(dashboards): key metric aggregations by query name and reject unknown point fields 2026-10-27 21:58:58 +05:30
Naman Verma
1ce461d47f feat: add validation to ensure queries have same group by 2026-10-27 17:50:49 +05:30
Abhi Kumar
c0778e426a feat(dashboards): add an axis to scatter plot thresholds 2026-10-27 17:50:49 +05:30
Naman Verma
5266f089b9 chore: add more validations for scatter plot 2026-10-27 17:50:49 +05:30
Abhi Kumar
d736f62033 feat(dashboards): rename scatter plot dimensions to colorBy and sizeBy 2026-10-27 17:50:49 +05:30
Abhi Kumar
bf7a6b44f7 chore(openapi): regenerate specs for scatter plot panel
Assisted-by: Claude Opus 5.5
2026-10-27 17:50:49 +05:30
Abhi Kumar
b41df346db feat(dashboards): add point size and opacity to scatter plot spec
The fixed size and the min/max range are both stored so binding or unbinding
the size dimension restores the other's last setting.

Assisted-by: Claude Opus 5.5
2026-10-27 17:50:49 +05:30
Abhi Kumar
77d31cb122 chore(openapi): regenerate specs for scatter plot panel
Assisted-by: Claude Opus 5.5
2026-10-27 17:50:49 +05:30
Abhi Kumar
82ee9953de feat(dashboards): add axis labels and multi-key colour to scatter plot spec
An empty colour list colours by every group-by label.

Assisted-by: Claude Opus 5.5
2026-10-27 17:50:49 +05:30
Abhi Kumar
05d7dd4e92 chore(openapi): regenerate specs for scatter plot panel
Assisted-by: Claude Opus 5.5
2026-10-27 17:50:49 +05:30
Abhi Kumar
16a8a850d2 feat(dashboards): add plugin schema for scatter plot panel
Assisted-by: Claude Opus 5.5
2026-10-27 17:50:49 +05:30
552 changed files with 24347 additions and 16225 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/v2/saved_views',
'http://localhost/api/v1/explorer/views',
response.json((req) =>
savedViews(values.savedViews, req.url.searchParams.get('source') ?? 'logs'),
savedViews(values.savedViews, req.url.searchParams.get('sourcePage') ?? 'logs'),
),
),
```

View File

@@ -3932,8 +3932,6 @@ components:
- logs-field
- traces-field
- metrics-field
- span-mapper-group
- span-mapper
type: string
CoretypesObject:
properties:
@@ -4226,6 +4224,13 @@ components:
nullable: true
type: number
type: object
DashboardtypesAxisScale:
enum:
- auto
- linear
- log
- symlog
type: string
DashboardtypesBarChartPanelSpec:
properties:
axes:
@@ -5252,6 +5257,7 @@ components:
signoz/ListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
signoz/NumberPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
signoz/PieChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec'
signoz/ScatterPlotPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpec'
signoz/TablePanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
signoz/TextPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
signoz/TimeSeriesPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
@@ -5267,6 +5273,7 @@ components:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpec'
type: object
DashboardtypesPanelPluginKind:
enum:
@@ -5280,6 +5287,7 @@ components:
- signoz/ListPanel
- signoz/TextPanel
- signoz/HeatmapPanel
- signoz/ScatterPlotPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec:
properties:
@@ -5386,6 +5394,18 @@ components:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpec:
properties:
kind:
enum:
- signoz/ScatterPlotPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesScatterPlotPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec:
properties:
kind:
@@ -5496,6 +5516,16 @@ components:
- $ref: '#/components/schemas/DashboardtypesBasicVisualization'
description: The time range the slices are computed over.
type: object
DashboardtypesPointDiameter:
maximum: 40
minimum: 2
nullable: true
type: integer
DashboardtypesPointOpacity:
maximum: 1
minimum: 0.1
nullable: true
type: number
DashboardtypesPostableDashboardV2:
properties:
generateName:
@@ -5750,6 +5780,104 @@ components:
required:
- queryValue
type: object
DashboardtypesScatterPlotAxes:
properties:
x:
$ref: '#/components/schemas/DashboardtypesScatterPlotAxis'
"y":
$ref: '#/components/schemas/DashboardtypesScatterPlotAxis'
type: object
DashboardtypesScatterPlotAxis:
properties:
label:
description: Axis title. Empty draws none.
type: string
scale:
$ref: '#/components/schemas/DashboardtypesAxisScale'
softMax:
nullable: true
type: number
softMin:
nullable: true
type: number
type: object
DashboardtypesScatterPlotChartAppearance:
properties:
points:
$ref: '#/components/schemas/DashboardtypesScatterPlotPoints'
type: object
DashboardtypesScatterPlotDimensions:
properties:
colorBy:
description: Group-by label names (e.g. k8s.namespace.name) whose combined
values colour dots and drive the legend. Empty colours by every group-by
label.
items:
type: string
nullable: true
type: array
sizeBy:
description: Value column key that scales dot size. Empty draws every dot
at the default size.
type: string
x:
description: Value column key (queryName, or queryName.expression for a
multi-aggregation query) plotted on the x axis. Empty uses the first value
column.
type: string
"y":
description: Value column key plotted on the y axis. Empty uses the second
value column.
type: string
type: object
DashboardtypesScatterPlotPanelSpec:
properties:
axes:
$ref: '#/components/schemas/DashboardtypesScatterPlotAxes'
chartAppearance:
$ref: '#/components/schemas/DashboardtypesScatterPlotChartAppearance'
dimensions:
$ref: '#/components/schemas/DashboardtypesScatterPlotDimensions'
formatting:
$ref: '#/components/schemas/DashboardtypesTableFormatting'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
thresholds:
items:
$ref: '#/components/schemas/DashboardtypesScatterPlotThreshold'
nullable: true
type: array
visualization:
$ref: '#/components/schemas/DashboardtypesBasicVisualization'
type: object
DashboardtypesScatterPlotPoints:
properties:
maxSize:
$ref: '#/components/schemas/DashboardtypesPointDiameter'
minSize:
$ref: '#/components/schemas/DashboardtypesPointDiameter'
opacity:
$ref: '#/components/schemas/DashboardtypesPointOpacity'
size:
$ref: '#/components/schemas/DashboardtypesPointDiameter'
type: object
DashboardtypesScatterPlotThreshold:
properties:
axis:
$ref: '#/components/schemas/DashboardtypesThresholdAxis'
color:
type: string
label:
type: string
unit:
type: string
value:
format: double
type: number
required:
- value
- color
type: object
DashboardtypesSource:
enum:
- user
@@ -5916,6 +6044,11 @@ components:
- value
- name
type: object
DashboardtypesThresholdAxis:
enum:
- x
- "y"
type: string
DashboardtypesThresholdFormat:
enum:
- text
@@ -11391,7 +11524,8 @@ components:
evaluation:
allOf:
- $ref: '#/components/schemas/RuletypesEvaluationEnvelope'
description: The evaluation window, as a kind and spec.
description: The evaluation window, as a kind and spec. Required when schemaVersion
is v2alpha1; derived from evalWindow and frequency for v1.
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
@@ -11407,7 +11541,8 @@ components:
notificationSettings:
allOf:
- $ref: '#/components/schemas/RuletypesNotificationSettings'
description: Grouping, re-notification and routing behaviour.
description: Grouping, re-notification and routing behaviour. Required when
schemaVersion is v2alpha1; derived from the v1 fields otherwise.
preferredChannels:
description: The displayName of each notification channel to notify, for
schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries
@@ -11420,30 +11555,26 @@ 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 and clickhouse_sql
queries and is forced to promql_rule for promql queries.'
from seasonal history. Defaults to threshold_rule for builder queries
and is forced to promql_rule for promql queries.'
schemaVersion:
description: The rule payload schema.
enum:
- v2alpha1
description: 'The rule payload schema: v1 (the default when omitted) uses
the flat condition fields, v2alpha1 uses condition.thresholds, evaluation
and notificationSettings.'
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:
deprecated: true
description: The query payload version. Nothing reads it.
description: The query payload version. Only v5 is accepted and it is filled
in when omitted.
type: string
required:
- alert
- alertType
- ruleType
- condition
- evaluation
- schemaVersion
- notificationSettings
type: object
RuletypesPostableRuleView:
properties:
@@ -11532,7 +11663,8 @@ components:
evaluation:
allOf:
- $ref: '#/components/schemas/RuletypesEvaluationEnvelope'
description: The evaluation window, as a kind and spec.
description: The evaluation window, as a kind and spec. Required when schemaVersion
is v2alpha1; derived from evalWindow and frequency for v1.
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
@@ -11551,7 +11683,8 @@ components:
notificationSettings:
allOf:
- $ref: '#/components/schemas/RuletypesNotificationSettings'
description: Grouping, re-notification and routing behaviour.
description: Grouping, re-notification and routing behaviour. Required when
schemaVersion is v2alpha1; derived from the v1 fields otherwise.
preferredChannels:
description: The displayName of each notification channel to notify, for
schemaVersion v1 rules. Ignored for v2alpha1, where each threshold carries
@@ -11564,15 +11697,14 @@ 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 and clickhouse_sql
queries and is forced to promql_rule for promql queries.'
from seasonal history. Defaults to threshold_rule for builder queries
and is forced to promql_rule for promql queries.'
schemaVersion:
description: The rule payload schema.
enum:
- v2alpha1
description: 'The rule payload schema: v1 (the default when omitted) uses
the flat condition fields, v2alpha1 uses condition.thresholds, evaluation
and notificationSettings.'
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
@@ -11587,8 +11719,8 @@ components:
updatedBy:
type: string
version:
deprecated: true
description: The query payload version. Nothing reads it.
description: The query payload version. Only v5 is accepted and it is filled
in when omitted.
type: string
required:
- id
@@ -11597,9 +11729,6 @@ components:
- alertType
- ruleType
- condition
- evaluation
- schemaVersion
- notificationSettings
type: object
RuletypesRuleCondition:
properties:
@@ -11649,7 +11778,8 @@ 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.
or F1. Must match a query in compositeQuery. When omitted, F1 is used
if present, otherwise the enabled query with the highest name.
type: string
target:
description: 'schemaVersion v1 only: the value compared against, in targetUnit.
@@ -11665,11 +11795,10 @@ components:
thresholds:
allOf:
- $ref: '#/components/schemas/RuletypesRuleThresholdData'
description: The thresholds to evaluate, as a kind and spec.
description: The thresholds to evaluate, as a kind and spec. Required when
schemaVersion is v2alpha1; derived from op, target and matchType for v1.
required:
- compositeQuery
- selectedQueryName
- thresholds
type: object
RuletypesRuleThresholdData:
discriminator:
@@ -17816,9 +17945,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper-group:list
- VIEWER
- tokenizer:
- span-mapper-group:list
- VIEWER
summary: List span attribute mapping groups
tags:
- spanmapper
@@ -17879,9 +18008,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper-group:create
- ADMIN
- tokenizer:
- span-mapper-group:create
- ADMIN
summary: Create a span attribute mapping group
tags:
- spanmapper
@@ -17926,9 +18055,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper-group:delete
- ADMIN
- tokenizer:
- span-mapper-group:delete
- ADMIN
summary: Delete a span attribute mapping group
tags:
- spanmapper
@@ -17984,9 +18113,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper-group:update
- ADMIN
- tokenizer:
- span-mapper-group:update
- ADMIN
summary: Update a span attribute mapping group
tags:
- spanmapper
@@ -18049,9 +18178,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper-group:read
- VIEWER
- tokenizer:
- span-mapper-group:read
- VIEWER
summary: List span mappers for a group
tags:
- spanmapper
@@ -18124,11 +18253,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper:create
- span-mapper-group:attach
- ADMIN
- tokenizer:
- span-mapper:create
- span-mapper-group:attach
- ADMIN
summary: Create a span mapper
tags:
- spanmapper
@@ -18178,11 +18305,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper:delete
- span-mapper-group:detach
- ADMIN
- tokenizer:
- span-mapper:delete
- span-mapper-group:detach
- ADMIN
summary: Delete a span mapper
tags:
- spanmapper
@@ -18243,9 +18368,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper:update
- ADMIN
- tokenizer:
- span-mapper:update
- ADMIN
summary: Update a span mapper
tags:
- spanmapper
@@ -18253,8 +18378,7 @@ paths:
/api/v1/span_mapper_groups/test:
post:
deprecated: false
description: Tests how span mappers would transform sample spans. Groups sent
without mappers are read from the store by name.
description: Tests how span mappers would transform sample spans
operationId: TestSpanMappers
requestBody:
content:
@@ -18308,9 +18432,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- span-mapper-group:read
- VIEWER
- tokenizer:
- span-mapper-group:read
- VIEWER
summary: Test span mappers against sample spans
tags:
- spanmapper
@@ -25323,13 +25447,13 @@ paths:
schemaVersion: v2alpha1
version: v5
metric_anomaly:
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
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)
value:
alert: Anomalous drop in ingested spans
alertType: METRIC_BASED_ALERT
@@ -25364,36 +25488,22 @@ paths:
stepInterval: 21600
type: builder_query
queryType: builder
matchType: all_the_times
op: below
requireMinPoints: true
requiredNumPoints: 3
seasonality: daily
selectedQueryName: A
thresholds:
kind: basic
spec:
- channels:
- slack-ingestion
matchType: all_the_times
name: warning
op: below
target: -2
target: 2
description: Detect an abrupt drop in span ingestion using a z-score
anomaly function
evaluation:
kind: rolling
spec:
evalWindow: 24h
frequency: 3h
evalWindow: 24h
frequency: 3h
labels:
severity: warning
notificationSettings:
renotify:
alertStates:
- firing
enabled: true
interval: 4h
preferredChannels:
- slack-ingestion
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
@@ -26289,13 +26399,13 @@ paths:
schemaVersion: v2alpha1
version: v5
metric_anomaly:
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
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)
value:
alert: Anomalous drop in ingested spans
alertType: METRIC_BASED_ALERT
@@ -26330,36 +26440,22 @@ paths:
stepInterval: 21600
type: builder_query
queryType: builder
matchType: all_the_times
op: below
requireMinPoints: true
requiredNumPoints: 3
seasonality: daily
selectedQueryName: A
thresholds:
kind: basic
spec:
- channels:
- slack-ingestion
matchType: all_the_times
name: warning
op: below
target: -2
target: 2
description: Detect an abrupt drop in span ingestion using a z-score
anomaly function
evaluation:
kind: rolling
spec:
evalWindow: 24h
frequency: 3h
evalWindow: 24h
frequency: 3h
labels:
severity: warning
notificationSettings:
renotify:
alertStates:
- firing
enabled: true
interval: 4h
preferredChannels:
- slack-ingestion
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
@@ -27156,13 +27252,13 @@ paths:
schemaVersion: v2alpha1
version: v5
metric_anomaly:
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
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)
value:
alert: Anomalous drop in ingested spans
alertType: METRIC_BASED_ALERT
@@ -27197,36 +27293,22 @@ paths:
stepInterval: 21600
type: builder_query
queryType: builder
matchType: all_the_times
op: below
requireMinPoints: true
requiredNumPoints: 3
seasonality: daily
selectedQueryName: A
thresholds:
kind: basic
spec:
- channels:
- slack-ingestion
matchType: all_the_times
name: warning
op: below
target: -2
target: 2
description: Detect an abrupt drop in span ingestion using a z-score
anomaly function
evaluation:
kind: rolling
spec:
evalWindow: 24h
frequency: 3h
evalWindow: 24h
frequency: 3h
labels:
severity: warning
notificationSettings:
renotify:
alertStates:
- firing
enabled: true
interval: 4h
preferredChannels:
- slack-ingestion
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
@@ -28729,13 +28811,13 @@ paths:
schemaVersion: v2alpha1
version: v5
metric_anomaly:
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
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)
value:
alert: Anomalous drop in ingested spans
alertType: METRIC_BASED_ALERT
@@ -28770,36 +28852,22 @@ paths:
stepInterval: 21600
type: builder_query
queryType: builder
matchType: all_the_times
op: below
requireMinPoints: true
requiredNumPoints: 3
seasonality: daily
selectedQueryName: A
thresholds:
kind: basic
spec:
- channels:
- slack-ingestion
matchType: all_the_times
name: warning
op: below
target: -2
target: 2
description: Detect an abrupt drop in span ingestion using a z-score
anomaly function
evaluation:
kind: rolling
spec:
evalWindow: 24h
frequency: 3h
evalWindow: 24h
frequency: 3h
labels:
severity: warning
notificationSettings:
renotify:
alertStates:
- firing
enabled: true
interval: 4h
preferredChannels:
- slack-ingestion
ruleType: anomaly_rule
schemaVersion: v2alpha1
version: v5
metric_promql:
description: PromQL expression instead of the builder. Dotted OTEL
@@ -32037,132 +32105,6 @@ paths:
content:
application/json:
examples:
ai_traces_raw:
summary: 'Raw: LLM, tool, and agent spans of one service''s AI traces
with many tool calls'
value:
compositeQuery:
queries:
- spec:
filter:
expression: service.name = 'ai-agent' AND trace.tool_call_count
> 10
limit: 100
name: A
order:
- direction: desc
key:
fieldContext: span
name: timestamp
signal: traces
type: builder_ai_query
end: 1640998800000
requestType: raw
schemaVersion: v1
start: 1640995200000
ai_traces_raw_select_fields:
summary: 'Raw: the same spans with the span name and gen_ai model,
tool, and token fields selected'
value:
compositeQuery:
queries:
- spec:
filter:
expression: service.name = 'ai-agent' AND trace.tool_call_count
> 10
limit: 100
name: A
order:
- direction: desc
key:
fieldContext: span
name: timestamp
selectFields:
- fieldContext: span
name: name
- fieldContext: attribute
name: gen_ai.request.model
- fieldContext: attribute
name: gen_ai.tool.name
- fieldContext: attribute
name: gen_ai.usage.input_tokens
- fieldContext: attribute
name: gen_ai.usage.output_tokens
signal: traces
type: builder_ai_query
end: 1640998800000
requestType: raw
schemaVersion: v1
start: 1640995200000
ai_traces_scalar:
summary: 'Scalar: average per-trace AI cost by model'
value:
compositeQuery:
queries:
- spec:
aggregations:
- expression: avg(trace.estimated_total_cost)
groupBy:
- fieldContext: attribute
name: gen_ai.request.model
limit: 10
name: A
order:
- direction: desc
key:
name: avg(trace.estimated_total_cost)
signal: traces
type: builder_ai_query
end: 1640998800000
requestType: scalar
schemaVersion: v1
start: 1640995200000
ai_traces_time_series:
summary: 'Time series: per-trace AI cost by model'
value:
compositeQuery:
queries:
- spec:
aggregations:
- expression: sum(trace.estimated_total_cost)
groupBy:
- fieldContext: attribute
name: gen_ai.request.model
limit: 10
name: A
order:
- direction: desc
key:
name: sum(trace.estimated_total_cost)
signal: traces
stepInterval: 60s
type: builder_ai_query
end: 1640998800000
requestType: time_series
schemaVersion: v1
start: 1640995200000
ai_traces_trace:
summary: 'Trace list: AI traces with many tool calls, most expensive
first'
value:
compositeQuery:
queries:
- spec:
filter:
expression: trace.tool_call_count > 10 AND trace.distinct_tool_count
< 3
limit: 10
name: A
order:
- direction: desc
key:
name: trace.estimated_total_cost
signal: traces
type: builder_ai_query
end: 1640998800000
requestType: trace
schemaVersion: v1
start: 1640995200000
clickhouse_sql_logs_raw:
summary: 'ClickHouse SQL: raw logs with resource filter'
value:

View File

@@ -93,9 +93,9 @@ tests/
│ └── auth.ts
├── helpers/ # function helpers + the constants they share with tests
│ ├── auth.ts
│ └── logs-explorer.ts
│ └── dashboards.ts
├── testdata/ # static data files (JSON) used by helpers and tests
│ └── traces/large-trace.json # (example)
│ └── apm-metrics.json # (example)
├── tests/ # Playwright .spec.ts files, one dir per feature area
│ └── alerts/
│ └── alerts.spec.ts # (example)
@@ -110,8 +110,8 @@ tests/
These two folders look similar but mean different things:
- **`fixtures/`** holds *Playwright test fixtures* (created via `test.extend({...})`). By the canonical definition, a fixture is "a consistent, predefined set of data, objects, or environmental conditions used to ensure tests run in a stable state" — i.e. setup/teardown that runs *automatically* around each test or worker. `auth.ts` matches: it extends Playwright's `test` with an `authedPage` that's logged-in before every test runs and torn down after. If the only thing in this folder ever is `auth.ts`, that's fine — fixtures are a deliberately small surface.
- **`helpers/`** holds plain function helpers that you call *explicitly* from a test or hook — they don't extend Playwright's `test`. This covers both behaviour helpers (e.g. `gotoLogsExplorer(page)`) and the constants those helpers and the tests both refer to (e.g. `LINKED_ROW_CLASS`). Constants live next to the helpers that use them so a single import line in a test covers both.
- **`testdata/`** holds static data files (typically JSON / YAML) consumed by the helpers — for example, `traces/large-trace.json`, a real trace the trace-details helpers seed.
- **`helpers/`** holds plain function helpers that you call *explicitly* from a test or hook — they don't extend Playwright's `test`. This covers both behaviour helpers (e.g. `gotoDashboardsList(page)`) and the constants those helpers and the tests both refer to (e.g. `SEARCH_PLACEHOLDER`). Constants live next to the helpers that use them so a single import line in a test covers both.
- **`testdata/`** holds static data files (typically JSON / YAML) consumed by the helpers — for example, `apm-metrics.json`, a real dashboard payload uploaded through the UI by an importer helper.
Rule of thumb: if it's a `test.extend` fixture, put it in `fixtures/`. If it's a function you call explicitly (or a constant the function uses), put it in `helpers/`. If it's a static file the helpers read, put it in `testdata/`.
@@ -128,7 +128,7 @@ test('TC-09 RouteTab switches the url', async ({ routing }) => {
});
```
- `driver(name, build, dispose?)` adds a test-scoped fixture built on `authedPage`. `dispose` runs after the test on the same page, so a driver can delete what it seeded.
- `driver(name, build)` adds a test-scoped fixture built on `authedPage`.
- `seeded(name, open, close)` adds a worker-scoped seed. `open` and `close` run on a throwaway admin page.
- A driver reuses helpers by calling them, never by reaching for another fixture.
- `drivers/` is AI-owned: coding agents generate and maintain it with the specs, and `.gitattributes` collapses it in diffs.
@@ -187,7 +187,7 @@ Each spec follows these principles:
});
```
This is faster and more reliable than a multi-step UI seed. Reach for the UI flow only when the test's *purpose* is asserting that flow.
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `traces/large-trace.json` is a real trace that `loadLargeTrace` (in `helpers/trace-details.ts`) seeds for the trace-details tests.
6. **Reusable static data lives in `tests/e2e/testdata/`.** For example, `apm-metrics.json` is a real dashboard payload that `importApmMetricsDashboardViaUI` (in `helpers/dashboards.ts`) uploads through the actual Import JSON UI flow to seed a richly-tagged dashboard for search/list tests.
## How to write an E2E test?

View File

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

View File

@@ -57,6 +57,7 @@ const mockPaths = {
const mockTzDate = jest.fn(
(date: Date, _timezone: string) => new Date(date.getTime()),
);
const mockOrient = jest.fn();
// Mock uPlot constructor - this needs to be a proper constructor function
function MockUPlot(
@@ -70,6 +71,7 @@ function MockUPlot(
// Add static methods to the constructor
MockUPlot.tzDate = mockTzDate;
MockUPlot.paths = mockPaths;
MockUPlot.orient = mockOrient;
// Pinned so canvas-space maths in draw hooks is deterministic under jsdom.
MockUPlot.pxRatio = 1;

View File

@@ -17,9 +17,11 @@
"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.",
"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",
"field_jira_site": "Site URL",
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
"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",
"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",
@@ -80,6 +82,7 @@
"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",
@@ -89,8 +92,8 @@
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",
"field_slack_description": "Description",
"field_webhook_username": "Username",
"field_webhook_password": "Password",
"field_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)",
"field_pager_routing_key": "Routing Key",
"field_pager_description": "Description",
"field_pager_severity": "Severity",
@@ -109,10 +112,13 @@
"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

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

View File

@@ -47,6 +47,7 @@
"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,9 +25,11 @@
"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.",
"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",
"field_jira_site": "Site URL",
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net, or a proxy in front of it.",
"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",
"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",
@@ -88,8 +90,7 @@
"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_token_bearer_prefix": "Paste the token only, without the Bearer prefix",
"incidentio_token_whitespace": "Remove the spaces around the token",
"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",
@@ -115,7 +116,6 @@
"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,7 +133,6 @@
"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",
@@ -144,15 +143,9 @@
"help_email_html": "Send email in html format",
"field_email_html": "Email body template",
"placeholder_email_html": "Email body template",
"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_webhook_username": "User Name (optional)",
"field_webhook_password": "Password (optional)",
"field_webhook_bearer_token": "Bearer token (optional)",
"field_pager_routing_key": "Routing Key",
"field_pager_description": "Description",
"field_pager_severity": "Severity",
@@ -175,7 +168,9 @@
"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_bearer_token": "Sent as an Authorization: Bearer header.",
"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_pager_description": "Shows up as description in pagerduty",
"help_opsgenie_message": "Shows up as message in opsgenie",
"help_opsgenie_priority": "Priority of the incident",
@@ -183,6 +178,7 @@
"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

@@ -0,0 +1,4 @@
{
"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,6 +50,8 @@
"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",
@@ -64,8 +66,10 @@
"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],
['/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],
[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],
])('should redirect the old views route %s to %s', (from, to) => {
renderPrivateRoute({ initialRoute: from });

View File

@@ -18,6 +18,7 @@ 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';
@@ -65,6 +66,7 @@ function App(): JSX.Element {
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isSavedViewEnabled = useSavedViewEnabled();
const chatSupport = useChatSupport();
const { hostname } = window.location;
@@ -254,8 +256,18 @@ function App(): JSX.Element {
useEffect(() => {
// The bottom strip carries Support, so the floating bubble goes entirely.
window.Pylon?.('hideChatBubble');
}, [pathname]);
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]);
// 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,6 +26,10 @@ 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(
@@ -33,6 +37,11 @@ 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,6 +26,7 @@ import {
Login,
Logs,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
@@ -47,6 +48,7 @@ import {
TracesExplorer,
TracesFunnelDetails,
TracesFunnels,
TracesSaveViews,
UnAuthorized,
UsageExplorerPage,
WorkspaceAccessRestricted,
@@ -118,6 +120,13 @@ 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,
@@ -209,6 +218,13 @@ 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,
@@ -398,6 +414,13 @@ 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,
@@ -420,6 +443,13 @@ 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

@@ -3579,8 +3579,6 @@ export enum CoretypesKindDTO {
'logs-field' = 'logs-field',
'traces-field' = 'traces-field',
'metrics-field' = 'metrics-field',
'span-mapper-group' = 'span-mapper-group',
'span-mapper' = 'span-mapper',
}
export enum CoretypesTypeDTO {
user = 'user',
@@ -5586,6 +5584,12 @@ export interface DashboardtypesAreaChartPanelSpecDTO {
visualization?: DashboardtypesAreaChartVisualizationDTO;
}
export enum DashboardtypesAxisScaleDTO {
auto = 'auto',
linear = 'linear',
log = 'log',
symlog = 'symlog',
}
export interface DashboardtypesBarChartVisualizationDTO {
/**
* @type boolean
@@ -6718,6 +6722,126 @@ export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDa
spec: DashboardtypesHeatmapPanelSpecDTO;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpecDTOKind {
'signoz/ScatterPlotPanel' = 'signoz/ScatterPlotPanel',
}
export interface DashboardtypesScatterPlotAxisDTO {
/**
* @type string
* @description Axis title. Empty draws none.
*/
label?: string;
scale?: DashboardtypesAxisScaleDTO;
/**
* @type number,null
*/
softMax?: number | null;
/**
* @type number,null
*/
softMin?: number | null;
}
export interface DashboardtypesScatterPlotAxesDTO {
x?: DashboardtypesScatterPlotAxisDTO;
y?: DashboardtypesScatterPlotAxisDTO;
}
/**
* @minimum 2
* @maximum 40
* @nullable
*/
export type DashboardtypesPointDiameterDTO = number | null;
/**
* @minimum 0.1
* @maximum 1
* @nullable
*/
export type DashboardtypesPointOpacityDTO = number | null;
export interface DashboardtypesScatterPlotPointsDTO {
maxSize?: DashboardtypesPointDiameterDTO | null;
minSize?: DashboardtypesPointDiameterDTO | null;
opacity?: DashboardtypesPointOpacityDTO | null;
size?: DashboardtypesPointDiameterDTO | null;
}
export interface DashboardtypesScatterPlotChartAppearanceDTO {
points?: DashboardtypesScatterPlotPointsDTO;
}
export interface DashboardtypesScatterPlotDimensionsDTO {
/**
* @type array,null
* @description Group-by label names (e.g. k8s.namespace.name) whose combined values colour dots and drive the legend. Empty colours by every group-by label.
*/
colorBy?: string[] | null;
/**
* @type string
* @description Value column key that scales dot size. Empty draws every dot at the default size.
*/
sizeBy?: string;
/**
* @type string
* @description Value column key (queryName, or queryName.expression for a multi-aggregation query) plotted on the x axis. Empty uses the first value column.
*/
x?: string;
/**
* @type string
* @description Value column key plotted on the y axis. Empty uses the second value column.
*/
y?: string;
}
export enum DashboardtypesThresholdAxisDTO {
x = 'x',
y = 'y',
}
export interface DashboardtypesScatterPlotThresholdDTO {
axis?: DashboardtypesThresholdAxisDTO;
/**
* @type string
*/
color: string;
/**
* @type string
*/
label?: string;
/**
* @type string
*/
unit?: string;
/**
* @type number
* @format double
*/
value: number;
}
export interface DashboardtypesScatterPlotPanelSpecDTO {
axes?: DashboardtypesScatterPlotAxesDTO;
chartAppearance?: DashboardtypesScatterPlotChartAppearanceDTO;
dimensions?: DashboardtypesScatterPlotDimensionsDTO;
formatting?: DashboardtypesTableFormattingDTO;
legend?: DashboardtypesLegendDTO;
/**
* @type array,null
*/
thresholds?: DashboardtypesScatterPlotThresholdDTO[] | null;
visualization?: DashboardtypesBasicVisualizationDTO;
}
export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpecDTO {
/**
* @enum signoz/ScatterPlotPanel
* @type string
*/
kind: DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpecDTOKind;
spec: DashboardtypesScatterPlotPanelSpecDTO;
}
export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpecDTO
@@ -6728,7 +6852,8 @@ export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTextPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpecDTO;
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHeatmapPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesScatterPlotPanelSpecDTO;
export enum Querybuildertypesv5RequestTypeDTO {
scalar = 'scalar',
@@ -8008,6 +8133,7 @@ export enum DashboardtypesPanelPluginKindDTO {
'signoz/ListPanel' = 'signoz/ListPanel',
'signoz/TextPanel' = 'signoz/TextPanel',
'signoz/HeatmapPanel' = 'signoz/HeatmapPanel',
'signoz/ScatterPlotPanel' = 'signoz/ScatterPlotPanel',
}
/**
* The RFC 6902 operations, applied in order against the dashboard's postable shape.
@@ -12586,12 +12712,6 @@ 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',
@@ -12668,9 +12788,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.
* @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.
*/
selectedQueryName: string;
selectedQueryName?: string;
/**
* @type number,null
* @format double
@@ -12683,9 +12803,9 @@ export interface RuletypesRuleConditionDTO {
*/
targetUnit?: string;
/**
* @description The thresholds to evaluate, as a kind and spec.
* @description The thresholds to evaluate, as a kind and spec. Required when schemaVersion is v2alpha1; derived from op, target and matchType for v1.
*/
thresholds: RuletypesRuleThresholdDataDTO;
thresholds?: RuletypesRuleThresholdDataDTO;
}
export interface RuletypesPostableRuleDTO {
@@ -12723,9 +12843,9 @@ export interface RuletypesPostableRuleDTO {
*/
evalWindow?: string;
/**
* @description The evaluation window, as a kind and spec.
* @description The evaluation window, as a kind and spec. Required when schemaVersion is v2alpha1; derived from evalWindow and frequency for v1.
*/
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.
@@ -12737,24 +12857,23 @@ export interface RuletypesPostableRuleDTO {
*/
labels?: RuletypesPostableRuleDTOLabels;
/**
* @description Grouping, re-notification and routing behaviour.
* @description Grouping, re-notification and routing behaviour. Required when schemaVersion is v2alpha1; derived from the v1 fields otherwise.
*/
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 and clickhouse_sql 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 queries and is forced to promql_rule for promql queries.
*/
ruleType: RuletypesRuleTypeDTO;
/**
* @enum v2alpha1
* @type string
* @description The rule payload schema.
* @description The rule payload schema: v1 (the default when omitted) uses the flat condition fields, v2alpha1 uses condition.thresholds, evaluation and notificationSettings.
*/
schemaVersion: RuletypesPostableRuleDTOSchemaVersion;
schemaVersion?: string;
/**
* @type string
* @description The URL of the page the rule was created from. Informational; it is not used for evaluation.
@@ -12762,7 +12881,7 @@ export interface RuletypesPostableRuleDTO {
source?: string;
/**
* @type string
* @description The query payload version. Nothing reads it.
* @description The query payload version. Only v5 is accepted and it is filled in when omitted.
*/
version?: string;
}
@@ -12785,12 +12904,6 @@ 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
@@ -12835,9 +12948,9 @@ export interface RuletypesRuleDTO {
*/
evalWindow?: string;
/**
* @description The evaluation window, as a kind and spec.
* @description The evaluation window, as a kind and spec. Required when schemaVersion is v2alpha1; derived from evalWindow and frequency for v1.
*/
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.
@@ -12854,24 +12967,23 @@ export interface RuletypesRuleDTO {
*/
labels?: RuletypesRuleDTOLabels;
/**
* @description Grouping, re-notification and routing behaviour.
* @description Grouping, re-notification and routing behaviour. Required when schemaVersion is v2alpha1; derived from the v1 fields otherwise.
*/
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 and clickhouse_sql 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 queries and is forced to promql_rule for promql queries.
*/
ruleType: RuletypesRuleTypeDTO;
/**
* @enum v2alpha1
* @type string
* @description The rule payload schema.
* @description The rule payload schema: v1 (the default when omitted) uses the flat condition fields, v2alpha1 uses condition.thresholds, evaluation and notificationSettings.
*/
schemaVersion: RuletypesRuleDTOSchemaVersion;
schemaVersion?: string;
/**
* @type string
* @description The URL of the page the rule was created from. Informational; it is not used for evaluation.
@@ -12892,7 +13004,7 @@ export interface RuletypesRuleDTO {
updatedBy?: string;
/**
* @type string
* @description The query payload version. Nothing reads it.
* @description The query payload version. Only v5 is accepted and it is filled in when omitted.
*/
version?: string;
}

View File

@@ -803,7 +803,7 @@ export const useUpdateSpanMapper = <
return useMutation(getUpdateSpanMapperMutationOptions(options));
};
/**
* Tests how span mappers would transform sample spans. Groups sent without mappers are read from the store by name.
* Tests how span mappers would transform sample spans
* @summary Test span mappers against sample spans
*/
export const testSpanMappers = (

View File

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,19 @@
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

@@ -21,6 +21,7 @@ import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import {
convertBuilderQueriesToV5,
mapPanelTypeToRequestType,
prepareQueryRangePayloadV5,
} from './prepareQueryRangePayloadV5';
@@ -904,6 +905,12 @@ describe('prepareQueryRangePayloadV5', () => {
});
});
describe('mapPanelTypeToRequestType', () => {
it('sends a scatter plot as scalar, like a table', () => {
expect(mapPanelTypeToRequestType(PANEL_TYPES.SCATTER)).toBe('scalar');
});
});
describe('convertBuilderQueriesToV5 having normalization', () => {
const buildSpec = (having: unknown): MetricBuilderQuery => {
const [envelope] = convertBuilderQueriesToV5(

View File

@@ -51,6 +51,7 @@ export function mapPanelTypeToRequestType(panelType: PANEL_TYPES): RequestType {
case PANEL_TYPES.BAR:
return 'time_series';
case PANEL_TYPES.TABLE:
case PANEL_TYPES.SCATTER:
case PANEL_TYPES.PIE:
case PANEL_TYPES.VALUE:
return 'scalar';
@@ -274,6 +275,7 @@ export function createAggregation(
queryData.dataSource === DataSource.METRICS &&
panelType &&
(panelType === PANEL_TYPES.TABLE ||
panelType === PANEL_TYPES.SCATTER ||
panelType === PANEL_TYPES.PIE ||
panelType === PANEL_TYPES.VALUE);

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 758 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,43 @@
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

@@ -0,0 +1,252 @@
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

@@ -0,0 +1,103 @@
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

@@ -0,0 +1,79 @@
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

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,9 @@
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

@@ -0,0 +1,29 @@
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

@@ -0,0 +1,96 @@
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

@@ -0,0 +1,63 @@
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

@@ -0,0 +1,63 @@
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

@@ -0,0 +1,92 @@
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

@@ -0,0 +1,204 @@
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 'hooks/useHandleExplorerTabChange';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
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 'hooks/useHandleExplorerTabChange';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import {
getBodyDisplayString,
getSanitizedLogBody,

View File

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

View File

@@ -1,9 +1,14 @@
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 { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { getBuilderVariant } from 'lib/query/queryMode';
import {
BuilderVariant,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderField } from './queryBuilderFields.types';
@@ -59,8 +64,20 @@ 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);
}
}, [
@@ -69,6 +86,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
initialDataSource,
currentDataSource,
newPanelType,
isAIQuery,
]);
useEffect(() => {
@@ -106,9 +124,17 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
[resolvedConfig],
);
const traceOperatorField = useMemo(
() =>
resolveQueryBuilderField(QueryBuilderField.TraceOperator, resolvedConfig),
[resolvedConfig],
);
const canShowTraceOperator = showTraceOperator && !traceOperatorField.hidden;
const isMultiQueryAllowed = useMemo(
() => !additionalQueries.hidden && (!isRawQuery || showTraceOperator),
[additionalQueries.hidden, showTraceOperator, isRawQuery],
() => !additionalQueries.hidden && (!isRawQuery || canShowTraceOperator),
[additionalQueries.hidden, canShowTraceOperator, isRawQuery],
);
const queryDataSources = useMemo(
@@ -146,13 +172,14 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
);
const hasTraceOperator = useMemo(
() => showTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
() =>
canShowTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
[canShowTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
const showQueryList = useMemo(
() => (!showOnlyWhereClause && !isRawQuery) || showTraceOperator,
[isRawQuery, showOnlyWhereClause, showTraceOperator],
() => (!showOnlyWhereClause && !isRawQuery) || canShowTraceOperator,
[isRawQuery, showOnlyWhereClause, canShowTraceOperator],
);
const showFormula = useMemo(() => {
@@ -168,8 +195,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
}, [formula.hidden, isRawQuery, currentDataSource]);
const showAddTraceOperator = useMemo(
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
() => canShowTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
[canShowTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
// Nothing left to add means no footer at all, rather than an empty bar under the
@@ -178,13 +205,13 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
() =>
(!additionalQueries.hidden || showFormula || showAddTraceOperator) &&
((!showOnlyWhereClause && !isRawQuery) ||
(currentDataSource === DataSource.TRACES && showTraceOperator)),
(currentDataSource === DataSource.TRACES && canShowTraceOperator)),
[
additionalQueries.hidden,
showFormula,
showAddTraceOperator,
isRawQuery,
showTraceOperator,
canShowTraceOperator,
showOnlyWhereClause,
currentDataSource,
],
@@ -222,7 +249,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
fieldsConfig={fieldsConfig}
allowedDataSources={queryDataSources}
isMultiQueryAllowed={isMultiQueryAllowed}
showTraceOperator={showTraceOperator}
showTraceOperator={canShowTraceOperator}
hasTraceOperator={hasTraceOperator}
version={version}
isAvailableToDisable={false}
@@ -247,7 +274,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
version={version}
isMultiQueryAllowed={isMultiQueryAllowed}
isAvailableToDisable={false}
showTraceOperator={showTraceOperator}
showTraceOperator={canShowTraceOperator}
hasTraceOperator={hasTraceOperator}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}

View File

@@ -6,7 +6,8 @@ export type NonAddOnField =
| QueryBuilderField.StepInterval
| QueryBuilderField.Functions
| QueryBuilderField.Formula
| QueryBuilderField.AdditionalQueries;
| QueryBuilderField.AdditionalQueries
| QueryBuilderField.TraceOperator;
export type AddOnField = Exclude<QueryBuilderField, NonAddOnField>;

View File

@@ -18,6 +18,7 @@ import {
STEP_INTERVAL_PLACEHOLDER,
} from '../../queryBuilderFields.utils';
import { isScalarPanelType } from '../panelTypes';
import QueryAggregationSelect from './QueryAggregationSelect';
import './QueryAggregation.styles.scss';
@@ -45,10 +46,7 @@ function QueryAggregationOptions({
const isNonMetricSource =
dataSource === DataSource.TRACES || dataSource === DataSource.LOGS;
if (
isNonMetricSource &&
(panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.PIE)
) {
if (isNonMetricSource && isScalarPanelType(panelType)) {
return { hidden: true, disabled: false, pinned: false };
}

View File

@@ -9,7 +9,6 @@ import {
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
import { QueryProps } from 'container/QueryBuilder/type';
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
@@ -27,6 +26,7 @@ import {
resolveQueryBuilderField,
} from '../queryBuilderFields.utils';
import { isScalarPanelType } from './panelTypes';
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
import QueryAddOns from './QueryAddOns/QueryAddOns';
@@ -112,11 +112,7 @@ export const QueryV2 = forwardRef(function QueryV2(
}
const showReduceTo = useMemo(
() =>
dataSource === DataSource.METRICS &&
(panelType === PANEL_TYPES.TABLE ||
panelType === PANEL_TYPES.PIE ||
panelType === PANEL_TYPES.VALUE),
() => dataSource === DataSource.METRICS && isScalarPanelType(panelType),
[dataSource, panelType],
);

View File

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

View File

@@ -0,0 +1,13 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
/** Panels that reduce each query to one value per group over the whole window. */
const SCALAR_PANEL_TYPES: ReadonlySet<string> = new Set<string>([
PANEL_TYPES.TABLE,
PANEL_TYPES.SCATTER,
PANEL_TYPES.PIE,
PANEL_TYPES.VALUE,
]);
export function isScalarPanelType(panelType?: string | null): boolean {
return panelType != null && SCALAR_PANEL_TYPES.has(panelType);
}

View File

@@ -14,6 +14,17 @@ jest.mock('../QueryV2/QueryAggregation/QueryAggregationSelect', () => ({
const queryData = initialQueriesMap.traces.builder
.queryData[0] as IBuilderQuery;
function renderFor(panelType: PANEL_TYPES, dataSource: DataSource): void {
render(
<QueryAggregationOptions
dataSource={dataSource}
panelType={panelType}
onAggregationIntervalChange={jest.fn()}
queryData={queryData}
/>,
);
}
describe('QueryAggregationOptions step interval placeholder', () => {
it('shows the configured placeholder', () => {
render(
@@ -34,14 +45,27 @@ describe('QueryAggregationOptions step interval placeholder', () => {
});
it('falls back to Auto', () => {
render(
<QueryAggregationOptions
dataSource={DataSource.TRACES}
panelType={PANEL_TYPES.TIME_SERIES}
onAggregationIntervalChange={jest.fn()}
queryData={queryData}
/>,
);
renderFor(PANEL_TYPES.TIME_SERIES, DataSource.TRACES);
expect(screen.getByPlaceholderText('Auto')).toBeInTheDocument();
});
});
describe('QueryAggregationOptions step interval', () => {
it.each([PANEL_TYPES.TABLE, PANEL_TYPES.SCATTER, PANEL_TYPES.PIE])(
'is hidden for a %s panel over traces, which reduces the whole window',
(panelType) => {
renderFor(panelType, DataSource.TRACES);
expect(screen.queryByText('every')).not.toBeInTheDocument();
},
);
it('stays for a scatter plot over metrics, where it sets the series step', () => {
renderFor(PANEL_TYPES.SCATTER, DataSource.METRICS);
expect(screen.getByText('every')).toBeInTheDocument();
});
it('stays for a time series over traces', () => {
renderFor(PANEL_TYPES.TIME_SERIES, DataSource.TRACES);
expect(screen.getByText('every')).toBeInTheDocument();
});
});

View File

@@ -136,6 +136,7 @@ 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),
});
});
@@ -292,6 +293,7 @@ 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

@@ -1,109 +0,0 @@
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

@@ -20,6 +20,7 @@ export enum QueryBuilderField {
// Builder level
Formula = 'formula',
AdditionalQueries = 'additional_queries',
TraceOperator = 'trace_operator',
}
/** Per-query builder fields that can be kept equal across a panel's queries. */

View File

@@ -65,13 +65,13 @@ export function resolveQueryBuilderFields(
/**
* The surface a raw-row builder starts from, layered under a caller's own config.
* `AdditionalQueries` is deliberately absent — a raw trace builder still takes several
* queries when trace matching is on. Omit rather than Partial, so a field added to the
* enum has to be placed on one side.
* `AdditionalQueries` and `TraceOperator` are deliberately absent — a raw trace builder
* still takes several queries, and their trace operator, when trace matching is on.
* Omit rather than Partial, so a field added to the enum has to be placed on one side.
*/
export const RAW_QUERY_FIELDS: Omit<
Record<QueryBuilderField, QueryBuilderFieldRule>,
QueryBuilderField.AdditionalQueries
QueryBuilderField.AdditionalQueries | QueryBuilderField.TraceOperator
> = {
[QueryBuilderField.Aggregation]: { state: 'hidden' },
[QueryBuilderField.StepInterval]: { state: 'hidden' },

View File

@@ -97,7 +97,7 @@ interface ToggleAction {
isOnlyOrAllClicked?: boolean;
previousState?: CheckedState;
sectionType?: SectionType;
quickFilterSource?: QuickFiltersSource;
source?: QuickFiltersSource;
attributeValues?: string[];
}
@@ -117,8 +117,7 @@ function runToggle(c: ToggleCase): { items: SimpleItem[]; expression: string } {
currentQuery: buildQuery(initialItems, initialExpression),
activeQueryIndex: 0,
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
quickFilterSource:
c.action.quickFilterSource ?? QuickFiltersSource.LOGS_EXPLORER,
source: c.action.source ?? QuickFiltersSource.LOGS_EXPLORER,
attributeValues: c.action.attributeValues ?? ['a', 'b', 'c'],
value: c.action.value,
checked: c.action.checked,
@@ -163,7 +162,7 @@ const TOGGLE_CASES: ToggleCase[] = [
action: {
value: 'a',
checked: false,
quickFilterSource: QuickFiltersSource.INFRA_MONITORING,
source: QuickFiltersSource.INFRA_MONITORING,
},
// `nin` is what the source asks for, but re-deriving the expression
// normalises it. Nothing observes the difference: both infra pages send
@@ -314,7 +313,7 @@ const TOGGLE_CASES: ToggleCase[] = [
action: {
value: 'b',
checked: false,
quickFilterSource: QuickFiltersSource.INFRA_MONITORING,
source: QuickFiltersSource.INFRA_MONITORING,
},
expected: {
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],

View File

@@ -46,18 +46,14 @@ export function removeManagedClauses(expression: string, key: string): string {
}
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
const SOURCES_WITH_SHORT_OPERATORS: QuickFiltersSource[] = [
QuickFiltersSource.INFRA_MONITORING,
];
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
/**
* Returns the correct NOT_IN operator value based on source.
* InfraMonitoring backend expects 'nin', others expect 'not in'.
*/
export function getNotInOperator(
quickFilterSource: QuickFiltersSource,
): string {
if (SOURCES_WITH_SHORT_OPERATORS.includes(quickFilterSource)) {
export function getNotInOperator(source: QuickFiltersSource): string {
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
return 'nin';
}
return getOperatorValue('NOT_IN');
@@ -138,7 +134,7 @@ export function applyCheckboxToggle({
currentQuery,
activeQueryIndex,
filter,
quickFilterSource,
source,
attributeValues,
value,
checked,
@@ -149,7 +145,7 @@ export function applyCheckboxToggle({
currentQuery: Query;
activeQueryIndex: number;
filter: IQuickFiltersConfig;
quickFilterSource: QuickFiltersSource;
source: QuickFiltersSource;
attributeValues: string[];
value: string;
checked: boolean;
@@ -244,7 +240,7 @@ export function applyCheckboxToggle({
if (sectionType === SectionType.RELATED) {
const newFilter: TagFilterItem = {
id: uuid(),
op: getNotInOperator(quickFilterSource),
op: getNotInOperator(source),
key: filter.attributeKey,
value,
};
@@ -384,7 +380,7 @@ export function applyCheckboxToggle({
if (!checked) {
const newFilter = {
...currentFilter,
op: getNotInOperator(quickFilterSource),
op: getNotInOperator(source),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
@@ -408,9 +404,7 @@ export function applyCheckboxToggle({
// checked=true → user wants to select (IN), checked=false → exclude (NOT IN)
const newFilterItem: TagFilterItem = {
id: uuid(),
op: checked
? getOperatorValue(OPERATORS.IN)
: getNotInOperator(quickFilterSource),
op: checked ? getOperatorValue(OPERATORS.IN) : getNotInOperator(source),
key: filter.attributeKey,
value,
};

View File

@@ -14,7 +14,7 @@ import { SectionType } from './v2/itemRules';
interface UseCheckboxFilterActionsProps {
filter: IQuickFiltersConfig;
quickFilterSource: QuickFiltersSource;
source: QuickFiltersSource;
attributeValues: string[];
activeQueryIndex: number;
onFilterChange?: ((query: Query) => void) | null;
@@ -38,7 +38,7 @@ interface UseCheckboxFilterActionsReturn {
*/
function useCheckboxFilterActions({
filter,
quickFilterSource,
source,
attributeValues,
activeQueryIndex,
onFilterChange,
@@ -65,7 +65,7 @@ function useCheckboxFilterActions({
currentQuery,
activeQueryIndex,
filter,
quickFilterSource,
source,
attributeValues,
value,
checked,

View File

@@ -1,6 +1,7 @@
import { render, RenderResult } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
@@ -20,6 +21,7 @@ export const DEFAULT_FILTER: IQuickFiltersConfig = {
dataType: DataTypes.String,
type: 'tag',
},
dataSource: DataSource.TRACES,
defaultOpen: true,
};
@@ -156,7 +158,7 @@ export function renderWithFilter(
return render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',

View File

@@ -33,7 +33,7 @@ import styles from './CheckboxFilterV2.module.scss';
interface CheckboxFilterV2Props {
filter: IQuickFiltersConfig;
quickFilterSource: QuickFiltersSource;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
useFieldApis: QuickFilterCheckboxUseFieldApis;
@@ -42,18 +42,13 @@ interface CheckboxFilterV2Props {
export default function CheckboxFilterV2(
props: CheckboxFilterV2Props,
): JSX.Element {
const {
quickFilterSource,
filter,
onFilterChange,
onQuickFilterChange,
useFieldApis,
} = props;
const { source, filter, onFilterChange, onQuickFilterChange, useFieldApis } =
props;
const [searchText, setSearchText] = useState<string>('');
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
const { currentQuery } = useQueryBuilder();
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
const activeQueryIndex = useActiveQueryIndex(source);
const {
isOpen,
@@ -80,7 +75,7 @@ export default function CheckboxFilterV2(
searchText,
existingQuery,
metricNamespace: useFieldApis.metricNamespace,
quickFilterSource,
source,
startUnixMilli: useFieldApis.startUnixMilli,
endUnixMilli: useFieldApis.endUnixMilli,
enabled: isOpen,
@@ -109,7 +104,7 @@ export default function CheckboxFilterV2(
const { onChange, onClear } = useCheckboxFilterActions({
filter,
quickFilterSource,
source,
attributeValues,
activeQueryIndex,
onFilterChange,

View File

@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.AI_OBSERVABILITY}
source={QuickFiltersSource.AI_OBSERVABILITY}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -44,7 +44,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.AI_OBSERVABILITY}
source={QuickFiltersSource.AI_OBSERVABILITY}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -59,8 +59,6 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
expect(params.get('endUnixMilli')).toBe(
String(DEFAULT_USE_FIELD_APIS.endUnixMilli),
);
expect(params.get('signal')).toBeNull();
expect(params.get('source')).toBeNull();
});
it('keeps non-AI sources on the signal-wide endpoint', async () => {
@@ -72,7 +70,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);

View File

@@ -50,7 +50,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'custom.query = "value"',
@@ -82,7 +82,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: null,
@@ -116,7 +116,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
@@ -154,7 +154,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
@@ -185,7 +185,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
@@ -222,7 +222,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
@@ -264,7 +264,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,

View File

@@ -1,48 +0,0 @@
import { screen } from '@testing-library/react';
import {
TelemetrytypesSignalDTO,
TelemetrytypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { rest, server } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - field APIs', () => {
it('sends the signal and source of the quick filter source to the signal-wide endpoint', async () => {
const requests: URLSearchParams[] = [];
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
requests.push(req.url.searchParams);
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: { values: { stringValues: ['ingest'] } },
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.METER_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await expect(screen.findByText('ingest')).resolves.toBeInTheDocument();
expect(requests[0].get('signal')).toBe(TelemetrytypesSignalDTO.metrics);
expect(requests[0].get('source')).toBe(TelemetrytypesSourceDTO.meter);
});
});

View File

@@ -72,7 +72,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -101,7 +101,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -119,7 +119,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -173,7 +173,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -240,7 +240,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -288,7 +288,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -330,7 +330,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -394,7 +394,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -440,7 +440,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -468,7 +468,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -490,7 +490,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
@@ -516,7 +516,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
@@ -568,7 +568,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -601,7 +601,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -627,7 +627,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -652,7 +652,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
@@ -691,7 +691,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -714,7 +714,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
@@ -761,7 +761,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
@@ -837,7 +837,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
@@ -971,7 +971,7 @@ describe('CheckboxFilterV2 - interactions', () => {
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, customRendererForValue: customRenderer }}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);

View File

@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -55,7 +55,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
@@ -106,7 +106,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -140,7 +140,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -185,7 +185,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -221,7 +221,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -273,7 +273,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
@@ -321,7 +321,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
@@ -373,7 +373,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -424,7 +424,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -466,7 +466,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
@@ -521,7 +521,7 @@ describe('CheckboxFilterV2 - item rules', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',

View File

@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - states', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -47,7 +47,7 @@ describe('CheckboxFilterV2 - states', () => {
render(
<CheckboxFilterV2
filter={closedFilter}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -104,7 +104,7 @@ describe('CheckboxFilterV2 - states', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -133,7 +133,7 @@ describe('CheckboxFilterV2 - states', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -152,7 +152,7 @@ describe('CheckboxFilterV2 - states', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -172,7 +172,7 @@ describe('CheckboxFilterV2 - states', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
@@ -195,7 +195,7 @@ describe('CheckboxFilterV2 - states', () => {
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);

View File

@@ -1,20 +1,20 @@
import { useMemo } from 'react';
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { FieldValuesConfig } from 'api/querySuggestions/types';
import { QUICK_FILTER_SOURCE_TO_SIGNAL } from 'components/QuickFilters/constants';
import {
IQuickFiltersConfig,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
import { BuilderQueryType } from 'types/api/v5/queryRange';
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
interface UseFieldValuesProps {
filter: IQuickFiltersConfig;
searchText: string;
existingQuery?: string;
metricNamespace?: string;
quickFilterSource: QuickFiltersSource;
source?: QuickFiltersSource;
startUnixMilli?: number;
endUnixMilli?: number;
enabled: boolean;
@@ -38,13 +38,12 @@ export function useFieldValues({
searchText,
existingQuery,
metricNamespace,
quickFilterSource,
source,
startUnixMilli,
endUnixMilli,
enabled,
}: UseFieldValuesProps): UseFieldValuesReturn {
const isAIObservability =
quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY;
const isAIObservability = source === QuickFiltersSource.AI_OBSERVABILITY;
const builderQueryType: BuilderQueryType | undefined = isAIObservability
? 'builder_ai_query'
@@ -60,12 +59,14 @@ export function useFieldValues({
endUnixMilli,
}
: {
signal: QUICK_FILTER_SOURCE_TO_SIGNAL[quickFilterSource],
signal: filter.dataSource
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
: undefined,
name: filter.attributeKey.key,
searchText,
existingQuery,
metricNamespace,
source: QUICK_FILTERS_SOURCE_TO_SOURCE[quickFilterSource],
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
startUnixMilli,
// This field does not affect the backend but I wanted to keep it here
// in case we add the support in the future

View File

@@ -31,11 +31,11 @@ export type { FilterType };
function Duration({
filter,
onFilterChange,
quickFilterSource,
source,
}: {
filter: IQuickFiltersConfig;
onFilterChange?: (query: Query) => void;
quickFilterSource: QuickFiltersSource;
source: QuickFiltersSource;
}): JSX.Element {
const [selectedFilters, setSelectedFilters] =
useState<
@@ -52,7 +52,7 @@ function Duration({
const compositeQuery = useGetCompositeQueryParam();
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
const activeQueryIndex = useActiveQueryIndex(source);
// eslint-disable-next-line sonarjs/cognitive-complexity
const syncSelectedFilters = useMemo((): FilterType => {

View File

@@ -49,18 +49,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
className,
config,
handleFilterVisibilityChange,
quickFilterSource,
source,
onFilterChange,
onQuickFilterChange,
signal,
showFilterCollapse = true,
showQueryName = true,
useFieldApis,
savedViewsHeader,
} = props;
const savedFiltersSource =
quickFilterSource === QuickFiltersSource.INFRA_MONITORING
? undefined
: quickFilterSource;
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [params, setParams] = useApiMonitoringParams();
const showIP = params.showIP ?? true;
@@ -71,7 +68,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
customFilters,
refetchCustomFilters,
isCustomFiltersLoading,
} = useFilterConfig({ savedFiltersSource, config });
} = useFilterConfig({ signal, config });
const {
deniedPermissions: deniedSettingsPermissions,
isLoading: isSettingsChecking,
@@ -115,15 +112,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
// Show dropdown in ListView only for TRACES_EXPLORER source
const shouldShowDropdownInListView =
isListView && quickFilterSource === QuickFiltersSource.TRACES_EXPLORER;
isListView && source === QuickFiltersSource.TRACES_EXPLORER;
// AI observability builds a single query in the row-level views, so there is
// no query for the selector to switch between.
const isAIObservabilityRowView =
quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY &&
source === QuickFiltersSource.AI_OBSERVABILITY &&
(isListView || panelType === PANEL_TYPES.TRACE);
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
const activeQueryIndex = useActiveQueryIndex(source);
// clear all the filters for the query which is in sync with filters
const handleReset = (): void => {
@@ -293,7 +290,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
const renderContent = (): JSX.Element => (
<>
{quickFilterSource === QuickFiltersSource.API_MONITORING && (
{source === QuickFiltersSource.API_MONITORING && (
<div className="api-quick-filters-header">
<Typography.Text>Show IP addresses</Typography.Text>
<Switch
@@ -316,7 +313,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
return (
<Duration
key={filter.attributeKey.key}
quickFilterSource={quickFilterSource}
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
@@ -327,7 +324,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
return (
<CheckboxV2
key={filter.attributeKey.key}
quickFilterSource={quickFilterSource}
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
@@ -366,14 +363,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
<div className="quick-filters-container">
<div className="quick-filters">
{savedViewsHeader}
{quickFilterSource !== QuickFiltersSource.INFRA_MONITORING && (
{source !== QuickFiltersSource.INFRA_MONITORING && (
<section className="header">
{renderLeftActions()}
{renderRightActions()}
</section>
)}
{savedFiltersSource ? (
{signal ? (
<AuthZGuardContent
checks={[QuickFilterReadPermission]}
fallbackOnLoading={filtersSkeleton}
@@ -394,9 +391,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
className,
)}
>
{isSettingsOpen && savedFiltersSource && (
{isSettingsOpen && (
<QuickFiltersSettings
savedFiltersSource={savedFiltersSource}
signal={signal}
setIsSettingsOpen={setIsSettingsOpen}
customFilters={customFilters}
refetchCustomFilters={refetchCustomFilters}
@@ -410,6 +407,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
QuickFilters.defaultProps = {
onFilterChange: null,
signal: '',
config: [],
showFilterCollapse: true,
showQueryName: true,

View File

@@ -5,6 +5,7 @@ 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';
@@ -49,9 +50,10 @@ function QuickFiltersLayout({
children,
}: QuickFiltersLayoutProps): JSX.Element {
const [isViewsListOpen, setIsViewsListOpen] = useState(false);
const isSavedViewEnabled = useSavedViewEnabled();
const hasQuickFilters = !!quickFilterProps;
const hasSavedViews = !!savedViewProps;
const hasSavedViews = !!savedViewProps && isSavedViewEnabled;
const showSidebar = showFilters && (hasQuickFilters || hasSavedViews);

View File

@@ -1,5 +1,6 @@
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';
@@ -19,6 +20,10 @@ 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 => (
@@ -29,17 +34,17 @@ jest.mock('container/SavedViews/SavedViewsRestore', () => ({
jest.mock('../../QuickFilters', () => ({
__esModule: true,
default: ({
quickFilterSource,
source,
savedViewsHeader,
handleFilterVisibilityChange,
}: {
quickFilterSource: string;
source: string;
savedViewsHeader?: React.ReactNode;
handleFilterVisibilityChange?: () => void;
}): JSX.Element => (
<div data-testid="quick-filters">
{savedViewsHeader}
{quickFilterSource}
{source}
{handleFilterVisibilityChange && (
<Button
size="md"
@@ -57,7 +62,7 @@ jest.mock('../../QuickFilters', () => ({
}));
const quickFilterProps = {
quickFilterSource: QuickFiltersSource.TRACES_EXPLORER,
source: QuickFiltersSource.TRACES_EXPLORER,
useFieldApis: { startUnixMilli: 0, endUnixMilli: 0 },
};
const savedViewProps = { source: SavedviewtypesSourceDTO.traces };
@@ -66,6 +71,10 @@ 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}>
@@ -140,6 +149,35 @@ 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

@@ -1,12 +1,10 @@
import { useMemo } from 'react';
import { Button, Skeleton } from 'antd';
import {
QuickfiltertypesSourceDTO,
TelemetrytypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { FieldKeysConfig } from 'api/querySuggestions/types';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { QUICK_FILTER_SOURCE_TO_SIGNAL } from 'components/QuickFilters/constants';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SignalType } from 'components/QuickFilters/types';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
import {
@@ -15,6 +13,7 @@ import {
FieldDataType,
TelemetryFieldKey,
} from 'types/api/v5/queryRange';
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
function OtherFiltersSkeleton(): JSX.Element {
return (
@@ -33,20 +32,18 @@ function OtherFiltersSkeleton(): JSX.Element {
}
function OtherFilters({
savedFiltersSource,
signal,
inputValue,
addedFilters,
setAddedFilters,
}: {
savedFiltersSource: QuickfiltertypesSourceDTO;
signal: SignalType | undefined;
inputValue: string;
addedFilters: TelemetryFieldKey[];
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
}): JSX.Element {
const isMeterDataSource =
savedFiltersSource === QuickfiltertypesSourceDTO.meter;
const isAIObservability =
savedFiltersSource === QuickfiltertypesSourceDTO.ai_observability;
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
const isAIObservability = signal === SignalType.AI_OBSERVABILITY;
const builderQueryType: BuilderQueryType | undefined = isAIObservability
? 'builder_ai_query'
@@ -56,7 +53,9 @@ function OtherFilters({
? { searchText: inputValue }
: {
searchText: inputValue,
signal: QUICK_FILTER_SOURCE_TO_SIGNAL[savedFiltersSource],
signal: signal
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
: undefined,
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
};

View File

@@ -2,9 +2,9 @@ import { Button as SignozButton } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { SignalType } from '../types';
import AddedFilters from './AddedFilters';
import useQuickFilterSettings from './hooks/useQuickFilterSettings';
import OtherFilters from './OtherFilters';
@@ -12,12 +12,12 @@ import OtherFilters from './OtherFilters';
import './QuickFiltersSettings.styles.scss';
function QuickFiltersSettings({
savedFiltersSource,
signal,
setIsSettingsOpen,
customFilters,
refetchCustomFilters,
}: {
savedFiltersSource: QuickfiltertypesSourceDTO;
signal: SignalType | undefined;
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
customFilters: TelemetryFieldKey[];
refetchCustomFilters: () => void;
@@ -37,7 +37,7 @@ function QuickFiltersSettings({
setIsSettingsOpen,
customFilters,
refetchCustomFilters,
savedFiltersSource,
signal,
});
return (
@@ -73,7 +73,7 @@ function QuickFiltersSettings({
setAddedFilters={setAddedFilters}
/>
<OtherFilters
savedFiltersSource={savedFiltersSource}
signal={signal}
inputValue={debouncedInputValue}
addedFilters={addedFilters}
setAddedFilters={setAddedFilters}

View File

@@ -3,8 +3,8 @@ import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render } from 'tests/test-utils';
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { SignalType } from '../../types';
import OtherFilters from '../OtherFilters';
const BASE_URL = ENVIRONMENT.baseURL;
@@ -43,12 +43,10 @@ describe('OtherFilters - AI observability keys', () => {
);
});
function renderOtherFilters(
savedFiltersSource: QuickfiltertypesSourceDTO,
): void {
function renderOtherFilters(signal: SignalType): void {
render(
<OtherFilters
savedFiltersSource={savedFiltersSource}
signal={signal}
inputValue=""
addedFilters={[]}
setAddedFilters={jest.fn()}
@@ -57,7 +55,7 @@ describe('OtherFilters - AI observability keys', () => {
}
it('reads AI observability keys from their own endpoint', async () => {
renderOtherFilters(QuickfiltertypesSourceDTO.ai_observability);
renderOtherFilters(SignalType.AI_OBSERVABILITY);
await expect(
screen.findByText('gen_ai.request.model'),
@@ -66,7 +64,7 @@ describe('OtherFilters - AI observability keys', () => {
});
it('does not narrow the AI keys by fieldContext', async () => {
renderOtherFilters(QuickfiltertypesSourceDTO.ai_observability);
renderOtherFilters(SignalType.AI_OBSERVABILITY);
// A `trace` context would return only the computed per-trace aggregates,
// which cannot be filtered on.
@@ -75,7 +73,7 @@ describe('OtherFilters - AI observability keys', () => {
});
it('keeps other signals on the signal-wide keys endpoint', async () => {
renderOtherFilters(QuickfiltertypesSourceDTO.traces);
renderOtherFilters(SignalType.TRACES);
await expect(screen.findByText('http.route')).resolves.toBeInTheDocument();
await waitFor(() => expect(aiKeysParams).toBeUndefined());

View File

@@ -0,0 +1,11 @@
import { SignalType } from 'components/QuickFilters/types';
import { DataSource } from 'types/common/queryBuilder';
export const SIGNAL_DATA_SOURCE_MAP = {
[SignalType.LOGS]: DataSource.LOGS,
[SignalType.TRACES]: DataSource.TRACES,
[SignalType.EXCEPTIONS]: DataSource.TRACES,
[SignalType.API_MONITORING]: DataSource.TRACES,
[SignalType.METER_EXPLORER]: DataSource.METRICS,
[SignalType.AI_OBSERVABILITY]: DataSource.TRACES,
};

View File

@@ -4,8 +4,8 @@ import logEvent from 'api/common/logEvent';
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesFieldDataTypeDTO,
QuickfiltertypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SignalType } from 'components/QuickFilters/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import useDebouncedFn from 'hooks/useDebouncedFunction';
@@ -16,7 +16,7 @@ interface UseQuickFilterSettingsProps {
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
customFilters: TelemetryFieldKey[];
refetchCustomFilters: () => void;
savedFiltersSource: QuickfiltertypesSourceDTO;
signal?: SignalType;
}
interface UseQuickFilterSettingsReturn {
@@ -37,7 +37,7 @@ const useQuickFilterSettings = ({
customFilters,
setIsSettingsOpen,
refetchCustomFilters,
savedFiltersSource,
signal,
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
const [inputValue, setInputValue] = useState<string>('');
const [debouncedInputValue, setDebouncedInputValue] = useState<string>('');
@@ -113,19 +113,21 @@ const useQuickFilterSettings = ({
);
const handleSaveChanges = useCallback((): void => {
updateCustomFilters({
pathParams: { source: savedFiltersSource },
data: {
// Send only the stored TelemetryFieldKey fields; the composite `key`
// is UI-only.
filters: addedFilters.map((filter) => ({
name: filter.name,
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
})),
},
});
}, [addedFilters, savedFiltersSource, updateCustomFilters]);
if (signal) {
updateCustomFilters({
pathParams: { source: signal },
data: {
// Send only the stored TelemetryFieldKey fields; the composite `key`
// is UI-only.
filters: addedFilters.map((filter) => ({
name: filter.name,
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
})),
},
});
}
}, [addedFilters, signal, updateCustomFilters]);
return {
handleSettingsClose,

View File

@@ -1,16 +0,0 @@
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { QuickFiltersSource } from './types';
export const QUICK_FILTER_SOURCE_TO_SIGNAL: Record<
QuickFiltersSource,
TelemetrytypesSignalDTO
> = {
[QuickFiltersSource.LOGS_EXPLORER]: TelemetrytypesSignalDTO.logs,
[QuickFiltersSource.TRACES_EXPLORER]: TelemetrytypesSignalDTO.traces,
[QuickFiltersSource.EXCEPTIONS]: TelemetrytypesSignalDTO.traces,
[QuickFiltersSource.API_MONITORING]: TelemetrytypesSignalDTO.traces,
[QuickFiltersSource.AI_OBSERVABILITY]: TelemetrytypesSignalDTO.traces,
[QuickFiltersSource.METER_EXPLORER]: TelemetrytypesSignalDTO.metrics,
[QuickFiltersSource.INFRA_MONITORING]: TelemetrytypesSignalDTO.metrics,
};

View File

@@ -10,26 +10,26 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
* In ListView most sources use index 0; TRACES_EXPLORER and every non-ListView
* mode track the last focused query.
*/
function useActiveQueryIndex(quickFilterSource: QuickFiltersSource): number {
function useActiveQueryIndex(source: QuickFiltersSource): number {
const { lastUsedQuery, panelType } = useQueryBuilder();
const isListView = panelType === PANEL_TYPES.LIST;
return useMemo(() => {
// AI observability builds a single query in the row-level views, so its
// filters always drive the first one there.
if (quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY) {
if (source === QuickFiltersSource.AI_OBSERVABILITY) {
return isListView || panelType === PANEL_TYPES.TRACE
? 0
: lastUsedQuery || 0;
}
if (isListView) {
return quickFilterSource === QuickFiltersSource.TRACES_EXPLORER
return source === QuickFiltersSource.TRACES_EXPLORER
? lastUsedQuery || 0
: 0;
}
return lastUsedQuery || 0;
}, [isListView, panelType, quickFilterSource, lastUsedQuery]);
}, [isListView, panelType, source, lastUsedQuery]);
}
export default useActiveQueryIndex;

View File

@@ -1,13 +1,12 @@
import { useMemo } from 'react';
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { IQuickFiltersConfig } from '../types';
import { IQuickFiltersConfig, SignalType } from '../types';
import { getFilterConfig } from '../utils';
interface UseFilterConfigProps {
savedFiltersSource?: QuickfiltertypesSourceDTO;
signal?: SignalType;
config: IQuickFiltersConfig[];
}
interface UseFilterConfigReturn {
@@ -19,7 +18,7 @@ interface UseFilterConfigReturn {
}
const useFilterConfig = ({
savedFiltersSource,
signal,
config,
}: UseFilterConfigProps): UseFilterConfigReturn => {
const {
@@ -27,8 +26,8 @@ const useFilterConfig = ({
data,
refetch,
} = useGetQuickFilters(
{ source: savedFiltersSource ?? '' },
{ query: { enabled: !!savedFiltersSource } },
{ source: signal ?? '' },
{ query: { enabled: !!signal } },
);
const customFilters = useMemo<TelemetryFieldKey[]>(
@@ -36,11 +35,11 @@ const useFilterConfig = ({
[data],
);
const isDynamicFilters = !!savedFiltersSource;
const isDynamicFilters = !!signal;
const filterConfig = useMemo(
() => getFilterConfig(customFilters, config),
[config, customFilters],
() => getFilterConfig(signal, customFilters, config),
[config, customFilters, signal],
);
return {

View File

@@ -89,15 +89,6 @@ export const handlers = [
fieldValuesHandler(['checkout', 'frontend', 'payments']),
];
export const noSavedFiltersHandler = rest.get(
'http://localhost/api/v2/quick_filters/logs',
(_req, res, ctx) =>
res(
ctx.status(200),
ctx.json(quickFiltersResponse(QuickfiltertypesSourceDTO.logs, [])),
),
);
export const loadingFiltersHandlers = [
rest.get('http://localhost/api/v2/quick_filters/logs', (_req, res, ctx) =>
res(ctx.delay('infinite')),

View File

@@ -15,9 +15,8 @@ import {
loadingFiltersHandlers,
queryBuilder,
selectedServiceQueryBuilder,
noSavedFiltersHandler,
} from './QuickFilters.stories.mocks';
import { QuickFiltersSource } from '../types';
import { QuickFiltersSource, SignalType } from '../types';
const meta = {
title: 'Components/Quick Filters',
@@ -34,7 +33,8 @@ const meta = {
args: {
config: checkboxConfig,
handleFilterVisibilityChange: (): void => undefined,
quickFilterSource: QuickFiltersSource.LOGS_EXPLORER,
signal: SignalType.LOGS,
source: QuickFiltersSource.LOGS_EXPLORER,
useFieldApis: fieldApis,
},
parameters: {
@@ -94,16 +94,13 @@ export const LoadingFilters: Story = {
/** Empty: a loaded quick-filter configuration with no filters has no result rows. */
export const NoResults: Story = {
args: { config: [] },
parameters: {
msw: { handlers: [noSavedFiltersHandler, ...handlers] },
},
args: { config: [], signal: undefined },
};
/** Selection: an expanded checkbox shows the actual selected service values. */
export const SelectedExpandedCheckbox: Story = {
args: { signal: undefined },
parameters: {
msw: { handlers: [noSavedFiltersHandler, ...handlers] },
signoz: {
queryBuilder: selectedServiceQueryBuilder,
},
@@ -114,18 +111,12 @@ export const SelectedExpandedCheckbox: Story = {
* Every tooltip the panel renders, held open: the reveal on each truncated
* filter value. Nothing bounds those values, so the panel is answered with
* service names long enough to be cut. The Service name filter carries them, so
* the workspace's own saved filter list is answered empty.
* the signal that would add the workspace's own dynamic filters is left off.
*/
export const Tooltips: TooltipsStory = {
args: { tooltipsOpen: true },
args: { signal: undefined, tooltipsOpen: true },
parameters: {
msw: {
handlers: [
noSavedFiltersHandler,
fieldValuesHandler(LONG_FILTER_VALUES),
...handlers,
],
},
msw: { handlers: [fieldValuesHandler(LONG_FILTER_VALUES), ...handlers] },
},
};

View File

@@ -24,7 +24,7 @@ import { rest } from 'msw';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import QuickFilters from '../QuickFilters';
import { QuickFiltersSource } from '../types';
import { QuickFiltersSource, SignalType } from '../types';
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
@@ -35,7 +35,8 @@ jest.mock('container/ApiMonitoring/queryParams');
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
const BASE_URL = ENVIRONMENT.baseURL;
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/logs`;
const SIGNAL = SignalType.LOGS;
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
const fieldsKeysURL = `${BASE_URL}/api/v1/fields/keys`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
@@ -80,10 +81,11 @@ afterEach(() => {
jest.clearAllMocks();
});
function renderWithSavedList(): void {
function renderWithSignal(): void {
render(
<QuickFilters
quickFilterSource={QuickFiltersSource.LOGS_EXPLORER}
source={QuickFiltersSource.LOGS_EXPLORER}
signal={SIGNAL}
handleFilterVisibilityChange={jest.fn()}
useFieldApis={QuickFiltersFieldApis}
/>,
@@ -93,7 +95,7 @@ function renderWithSavedList(): void {
function renderStaticConfig(): void {
render(
<QuickFilters
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
source={QuickFiltersSource.EXCEPTIONS}
config={QuickFiltersConfig}
handleFilterVisibilityChange={jest.fn()}
useFieldApis={QuickFiltersFieldApis}
@@ -106,7 +108,7 @@ describe('QuickFilters - AuthZ', () => {
it('shows the inline denial instead of the filters, header stays', async () => {
server.use(setupAuthzDeny(QuickFilterReadPermission));
renderWithSavedList();
renderWithSignal();
await expect(
screen.findByText(NOT_AUTHORIZED_TEXT),
@@ -131,7 +133,7 @@ describe('QuickFilters - AuthZ', () => {
it('renders the filters but disables the settings trigger', async () => {
server.use(setupAuthzDeny(QuickFilterUpdatePermission));
renderWithSavedList();
renderWithSignal();
await expect(
screen.findByText(FILTER_SERVICE_NAME),
@@ -156,7 +158,7 @@ describe('QuickFilters - AuthZ', () => {
it('renders the filters and opens settings from the trigger', async () => {
server.use(setupAuthzAdmin());
renderWithSavedList();
renderWithSignal();
await expect(
screen.findByText(FILTER_SERVICE_NAME),
@@ -174,7 +176,7 @@ describe('QuickFilters - AuthZ', () => {
});
});
describe('static config pages (no saved list)', () => {
describe('static config pages (no signal)', () => {
it('is not gated even when every permission is denied', async () => {
server.use(setupAuthzDenyAll());
@@ -193,7 +195,7 @@ describe('QuickFilters - AuthZ', () => {
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => res(ctx.delay('infinite'))),
);
renderWithSavedList();
renderWithSignal();
await waitFor(() =>
// eslint-disable-next-line testing-library/no-node-access

View File

@@ -1,11 +1,8 @@
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { ENVIRONMENT } from 'constants/env';
import {
ApiMonitoringParams,
useApiMonitoringParams,
} from 'container/ApiMonitoring/queryParams';
import { getHostsQuickFiltersConfig } from 'container/InfraMonitoringHostsV2/utils';
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
otherFiltersResponse,
@@ -19,7 +16,7 @@ import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import '@testing-library/jest-dom';
import QuickFilters from '../QuickFilters';
import { IQuickFiltersConfig, QuickFiltersSource } from '../types';
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
@@ -36,9 +33,9 @@ const mockSetApiMonitoringParams = jest.fn() as jest.MockedFunction<
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
const BASE_URL = ENVIRONMENT.baseURL;
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/logs`;
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/logs`;
const anyQuickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/:source`;
const SIGNAL = SignalType.LOGS;
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v1/fields/keys`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
@@ -57,9 +54,6 @@ const setupServer = (): void => {
rest.get(quickFiltersListURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersListResponse)),
),
rest.get(anyQuickFiltersListURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success', data: { filters: [] } })),
),
rest.get(quickFiltersSuggestionsURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json(otherFiltersResponse)),
),
@@ -74,24 +68,48 @@ const setupServer = (): void => {
};
function TestQuickFilters({
quickFilterSource = QuickFiltersSource.EXCEPTIONS,
signal = SignalType.LOGS,
config = QuickFiltersConfig,
}: {
quickFilterSource?: QuickFiltersSource;
signal?: SignalType;
config?: IQuickFiltersConfig[];
}): JSX.Element {
return (
<QuickFilters
quickFilterSource={quickFilterSource}
source={QuickFiltersSource.EXCEPTIONS}
config={config}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={QuickFiltersFieldApis}
signal={signal}
/>
);
}
TestQuickFilters.defaultProps = {
quickFilterSource: QuickFiltersSource.EXCEPTIONS,
signal: '',
config: QuickFiltersConfig,
};
function TestQuickFiltersApiMonitoring({
signal = SignalType.LOGS,
config = QuickFiltersConfig,
}: {
signal?: SignalType;
config?: IQuickFiltersConfig[];
}): JSX.Element {
return (
<QuickFilters
source={QuickFiltersSource.API_MONITORING}
config={config}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={QuickFiltersFieldApis}
signal={signal}
/>
);
}
TestQuickFiltersApiMonitoring.defaultProps = {
signal: '',
config: QuickFiltersConfig,
};
@@ -269,14 +287,10 @@ describe('Quick Filters', () => {
it('toggles Show IP addresses and updates API Monitoring params', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.API_MONITORING} />,
);
render(<TestQuickFiltersApiMonitoring />);
// Switch should be rendered and initially checked
await expect(
screen.findByText('Show IP addresses'),
).resolves.toBeInTheDocument();
expect(screen.getByText('Show IP addresses')).toBeInTheDocument();
const toggle = screen.getByRole('switch');
expect(toggle).toHaveAttribute('aria-checked', 'true');
@@ -294,9 +308,7 @@ describe('Quick Filters with custom filters', () => {
it('loads the custom filters correctly', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
expect(screen.getByText('Filters for')).toBeInTheDocument();
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
@@ -356,9 +368,7 @@ describe('Quick Filters with custom filters', () => {
),
);
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
@@ -386,9 +396,7 @@ describe('Quick Filters with custom filters', () => {
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
@@ -409,9 +417,7 @@ describe('Quick Filters with custom filters', () => {
it('removes a filter from ADDED FILTERS and moves it to OTHER FILTERS', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
@@ -440,9 +446,7 @@ describe('Quick Filters with custom filters', () => {
it('restores original filter state on Discard', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
@@ -484,9 +488,7 @@ describe('Quick Filters with custom filters', () => {
it('saves the updated filters by calling PUT with correct payload', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
@@ -523,9 +525,7 @@ describe('Quick Filters with custom filters', () => {
pointerEventsCheck: 0,
});
const { getByTestId } = render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
const { getByTestId } = render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
expect(screen.getByText('Duration')).toBeInTheDocument();
@@ -579,7 +579,7 @@ describe('Quick Filters with custom filters', () => {
});
describe('Quick Filters refetch behavior', () => {
it('fetches custom filters on every mount for a page with a saved list', async () => {
it('fetches custom filters on every mount when signal is provided', async () => {
let getCalls = 0;
server.use(
@@ -589,18 +589,14 @@ describe('Quick Filters refetch behavior', () => {
}),
);
const { unmount } = render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
const { unmount } = render(<TestQuickFilters signal={SIGNAL} />);
await expect(
screen.findByText(FILTER_SERVICE_NAME),
).resolves.toBeInTheDocument();
unmount();
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
await expect(
screen.findByText(FILTER_SERVICE_NAME),
).resolves.toBeInTheDocument();
@@ -608,24 +604,19 @@ describe('Quick Filters refetch behavior', () => {
expect(getCalls).toBe(2);
});
it('does not fetch custom filters for a page without a saved list', async () => {
it('does not fetch custom filters when signal is undefined', async () => {
let getCalls = 0;
server.use(
rest.get(anyQuickFiltersListURL, (_req, res, ctx) => {
rest.get(quickFiltersListURL, (_req, res, ctx) => {
getCalls += 1;
return res(ctx.status(200), ctx.json(quickFiltersListResponse));
}),
);
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.INFRA_MONITORING} />,
);
render(<TestQuickFilters signal={undefined} />);
await expect(
screen.findByText(FILTER_SERVICE_NAME),
).resolves.toBeInTheDocument();
expect(getCalls).toBe(0);
await waitFor(() => expect(getCalls).toBe(0));
});
it('refetches custom filters after saving settings', async () => {
@@ -644,9 +635,7 @@ describe('Quick Filters refetch behavior', () => {
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
await expect(
screen.findByText(FILTER_SERVICE_NAME),
@@ -698,9 +687,7 @@ describe('Quick Filters refetch behavior', () => {
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
);
render(<TestQuickFilters signal={SIGNAL} />);
await expect(
screen.findByText(FILTER_SERVICE_NAME),
@@ -731,47 +718,10 @@ describe('Quick Filters refetch behavior', () => {
),
);
render(
<TestQuickFilters
quickFilterSource={QuickFiltersSource.LOGS_EXPLORER}
config={[]}
/>,
);
render(<TestQuickFilters signal={SIGNAL} config={[]} />);
await expect(
screen.findByText('No filters found'),
).resolves.toBeInTheDocument();
});
});
describe('Quick Filters field APIs', () => {
it('sends the page signal for every filter, the infra Environment filter included', async () => {
const requests: URLSearchParams[] = [];
server.use(
rest.get(fieldsValuesURL, (req, res, ctx) => {
requests.push(req.url.searchParams);
return res(ctx.status(200), ctx.json(quickFiltersFieldValuesResponse));
}),
);
render(
<QuickFilters
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig()}
handleFilterVisibilityChange={handleFilterVisibilityChange}
useFieldApis={QuickFiltersFieldApis}
/>,
);
await waitFor(() =>
expect(requests.map((params) => params.get('name'))).toContain(
INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
),
);
const environment = requests.find(
(params) =>
params.get('name') === INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
);
expect(environment?.get('signal')).toBe(TelemetrytypesSignalDTO.metrics);
});
});

View File

@@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
export enum FiltersType {
SLIDER = 'SLIDER',
@@ -19,6 +19,15 @@ export enum SpecficFilterOperations {
ONLY = 'ONLY',
}
export enum SignalType {
TRACES = 'traces',
LOGS = 'logs',
API_MONITORING = 'api_monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
AI_OBSERVABILITY = 'ai_observability',
}
/**
* Missing export from signozhq/ui/checkbox, TODO(H4ad): Add and remove this type definition
*/
@@ -30,6 +39,7 @@ export interface IQuickFiltersConfig {
attributeKey: BaseAutocompleteData;
aggregateOperator?: string;
aggregateAttribute?: string;
dataSource?: DataSource;
customRendererForValue?: (value: string) => JSX.Element;
defaultOpen: boolean;
}
@@ -43,9 +53,11 @@ export interface QuickFilterChangeEventData {
export interface IQuickFiltersProps {
config: IQuickFiltersConfig[];
handleFilterVisibilityChange?: () => void;
quickFilterSource: QuickFiltersSource;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
/** Pass to fetch quick filters for this signal; omit to use `config` as-is */
signal?: SignalType;
className?: string;
showFilterCollapse?: boolean;
showQueryName?: boolean;
@@ -53,19 +65,15 @@ export interface IQuickFiltersProps {
savedViewsHeader?: ReactNode;
}
export const QuickFiltersSource = {
LOGS_EXPLORER: QuickfiltertypesSourceDTO.logs,
TRACES_EXPLORER: QuickfiltertypesSourceDTO.traces,
EXCEPTIONS: QuickfiltertypesSourceDTO.exceptions,
API_MONITORING: QuickfiltertypesSourceDTO.api_monitoring,
METER_EXPLORER: QuickfiltertypesSourceDTO.meter,
AI_OBSERVABILITY: QuickfiltertypesSourceDTO.ai_observability,
/** No saved filter list: renders `config` as given. */
INFRA_MONITORING: 'infra-monitoring',
} as const;
export type QuickFiltersSource =
(typeof QuickFiltersSource)[keyof typeof QuickFiltersSource];
export enum QuickFiltersSource {
LOGS_EXPLORER = 'logs-explorer',
INFRA_MONITORING = 'infra-monitoring',
TRACES_EXPLORER = 'traces-explorer',
API_MONITORING = 'api-monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
AI_OBSERVABILITY = 'ai-observability',
}
export type QuickFilterCheckboxUseFieldApis = {
startUnixMilli: number;

View File

@@ -1,8 +1,9 @@
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
import { FiltersType, IQuickFiltersConfig } from './types';
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
const FILTER_TITLE_MAP: Record<string, string> = {
duration_nano: 'Duration',
@@ -49,10 +50,11 @@ const getFilterType = (att: TelemetryFieldKey): FiltersType => {
};
export const getFilterConfig = (
signal?: SignalType,
customFilters?: TelemetryFieldKey[],
config?: IQuickFiltersConfig[],
): IQuickFiltersConfig[] => {
if (!customFilters?.length) {
if (!customFilters?.length || !signal) {
return config || [];
}
@@ -61,6 +63,7 @@ export const getFilterConfig = (
({
type: getFilterType(att),
title: getFilterName(att.name),
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
attributeKey: {
id: att.name,
key: att.name,

View File

@@ -21,6 +21,7 @@ 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',
@@ -46,4 +47,5 @@ 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

@@ -36,6 +36,7 @@ export const getComponentForPanelType = (
[PANEL_TYPES.HEATMAP]: null,
// Dashboards v2 renders this kind; nothing reaches the V1 chart map for it.
[PANEL_TYPES.TEXT]: null,
[PANEL_TYPES.SCATTER]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
};

View File

@@ -337,6 +337,7 @@ export enum PANEL_TYPES {
TRACE = 'trace',
BAR = 'bar',
AREA = 'area',
SCATTER = 'scatter',
PIE = 'pie',
HISTOGRAM = 'histogram',
HEATMAP = 'heatmap',

View File

@@ -50,6 +50,8 @@ 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',
@@ -67,6 +69,7 @@ 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',
@@ -74,6 +77,7 @@ 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

@@ -0,0 +1,15 @@
.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

@@ -0,0 +1,61 @@
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

@@ -0,0 +1,2 @@
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('/metrics-explorer/views', '')).toBe(
expect(resolvePageType(ROUTES.METRICS_EXPLORER_VIEWS, '')).toBe(
PageTypeDTO.metrics_explorer,
);
});

View File

@@ -235,6 +235,7 @@ describe('buildExplorerNavigationUrl', () => {
{ queryType: 'builder' } as never,
{
[QueryParams.panelTypes]: PANEL_TYPES.LIST,
[QueryParams.viewName]: 'My view',
[QueryParams.viewKey]: 'view-1',
},
);
@@ -258,7 +259,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)).toBeNull();
expect(params.get(QueryParams.viewName)).toBe('"View view-logs"');
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 'container/SavedViews/constants';
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { History } from 'history';
@@ -108,6 +108,7 @@ 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`, `viewKey`,
* (`compositeQuery`, `startTime`/`endTime`, `viewName`/`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).
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer, views).
if (
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
) {
@@ -361,6 +361,10 @@ 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

@@ -334,7 +334,6 @@ function AnomalyAlertEvaluationView({
<Checkbox
color="primary"
key={seriesKey}
testId="anomaly-series-option"
name="series"
value={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}

View File

@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import DomainList from './Domains/DomainList';
@@ -24,7 +24,8 @@ function Explorer(): JSX.Element {
showFilters
quickFilterProps={{
className: 'qf-api-monitoring',
quickFilterSource: QuickFiltersSource.API_MONITORING,
source: QuickFiltersSource.API_MONITORING,
signal: SignalType.API_MONITORING,
showQueryName: false,
useFieldApis: quickFilterFieldApis,
}}

View File

@@ -91,6 +91,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
dataType: DataTypes.String,
type: 'resource',
},
dataSource: DataSource.TRACES,
defaultOpen: true,
},
{
@@ -101,6 +102,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
dataType: DataTypes.String,
type: 'resource',
},
dataSource: DataSource.TRACES,
defaultOpen: true,
},
{
@@ -111,6 +113,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
dataType: DataTypes.String,
type: 'tag',
},
dataSource: DataSource.TRACES,
defaultOpen: true,
},
];

View File

@@ -72,6 +72,40 @@
}
}
.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,6 +28,7 @@ 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';
@@ -50,6 +51,8 @@ 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';
@@ -398,6 +401,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}, [pathname]);
const isToDisplayLayout = isLoggedIn;
const isSavedViewEnabled = useSavedViewEnabled();
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
const pageTitle = t(routeKey);
@@ -485,6 +489,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [isDarkMode]);
const chatSupport = useChatSupport();
// Listen for API warnings
const handleWarning = (
isSlow: boolean,
@@ -828,7 +834,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</LayoutContent>
</Sentry.ErrorBoundary>
{isToDisplayLayout && !renderFullScreen && <BottomStrip />}
{isSavedViewEnabled && isToDisplayLayout && !renderFullScreen && (
<BottomStrip />
)}
</div>
{isLoggedIn && isAIAssistantEnabled && (
@@ -839,6 +847,9 @@ 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, WebhookAuth } from './types';
import { ChannelFormValues, ChannelKind } from './types';
const slackValues: ChannelFormValues = {
name: 'prod alerts',
@@ -56,22 +56,6 @@ 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, {

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