mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-10 02:10:42 +00:00
Compare commits
17 Commits
main
...
refactor/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
520d0c3853 | ||
|
|
04c7fadd45 | ||
|
|
d89d001532 | ||
|
|
52fcd31cdb | ||
|
|
8379e5e42f | ||
|
|
7ff90cc084 | ||
|
|
da4cf2ab2b | ||
|
|
612a515bd7 | ||
|
|
51012284af | ||
|
|
bfe5d3c531 | ||
|
|
2119418c3a | ||
|
|
0bce326175 | ||
|
|
0d8a471757 | ||
|
|
84a87bf8bb | ||
|
|
277db9e2f4 | ||
|
|
baf5bcd687 | ||
|
|
52e4c5a28f |
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@@ -209,10 +209,6 @@ go.mod @therealpandey
|
||||
/frontend/src/types/api/alerts/ @SigNoz/pulse-frontend
|
||||
/frontend/src/providers/Alert.tsx @SigNoz/pulse-frontend
|
||||
/frontend/src/constants/alerts.ts @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PlannedDowntime/ @SigNoz/pulse-frontend
|
||||
/frontend/src/hooks/alerts/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/Home/AlertRules/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/Home/__tests__/AlertRules.test.tsx @SigNoz/pulse-frontend
|
||||
|
||||
## Notification Channels
|
||||
/frontend/src/pages/ChannelsEdit/ @SigNoz/pulse-frontend
|
||||
@@ -221,11 +217,6 @@ go.mod @therealpandey
|
||||
/frontend/src/container/FormAlertChannels/ @SigNoz/pulse-frontend
|
||||
/frontend/src/hooks/notificationChannels/ @SigNoz/pulse-frontend
|
||||
|
||||
## List filter + views (dashboards and alerts lists)
|
||||
/frontend/src/components/ListFilter/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/ListViews/ @SigNoz/pulse-frontend
|
||||
/frontend/src/hooks/listViews/ @SigNoz/pulse-frontend
|
||||
|
||||
## OpenAPI Schema - Generated
|
||||
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv
|
||||
/docs/api/openapi.yml @therealpandey @vikrantgupta25 @srikanthccv
|
||||
|
||||
2
.github/workflows/build-community.yaml
vendored
2
.github/workflows/build-community.yaml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.25
|
||||
GO_VERSION: 1.24
|
||||
GO_NAME: signoz-community
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
|
||||
2
.github/workflows/build-enterprise.yaml
vendored
2
.github/workflows/build-enterprise.yaml
vendored
@@ -89,7 +89,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.25
|
||||
GO_VERSION: 1.24
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./cmd/enterprise
|
||||
|
||||
2
.github/workflows/build-staging.yaml
vendored
2
.github/workflows/build-staging.yaml
vendored
@@ -95,7 +95,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.25
|
||||
GO_VERSION: 1.24
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./cmd/enterprise
|
||||
|
||||
20
.github/workflows/goci.yaml
vendored
20
.github/workflows/goci.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_TEST_CONTEXT: ./...
|
||||
GO_VERSION: 1.25
|
||||
GO_VERSION: 1.24
|
||||
fmt:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.25
|
||||
GO_VERSION: 1.24
|
||||
lint:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.25
|
||||
GO_VERSION: 1.24
|
||||
deps:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_VERSION: 1.25
|
||||
GO_VERSION: 1.24
|
||||
semconv-generated:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
- name: check-semconv-generated-files
|
||||
run: make semconv-check
|
||||
build:
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
- name: qemu-install
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: aarch64-install
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
- name: generate-openapi
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate openapi
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
- name: generate-authz
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate authz
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
- name: generate-web-settings
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate config web-settings
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
- name: generate-transaction-groups
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate config transaction-groups
|
||||
|
||||
4
.github/workflows/gor-signoz-community.yaml
vendored
4
.github/workflows/gor-signoz-community.yaml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
- name: cross-compilation-tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
|
||||
# copy the caches from build
|
||||
- name: get-sha
|
||||
|
||||
4
.github/workflows/gor-signoz.yaml
vendored
4
.github/workflows/gor-signoz.yaml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
- name: cross-compilation-tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.24"
|
||||
|
||||
# copy the caches from build
|
||||
- name: get-sha
|
||||
|
||||
@@ -10231,10 +10231,8 @@ components:
|
||||
having:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5Having'
|
||||
description: A filter on the formula values, referencing the formula by
|
||||
its name or as __result, for example F1 > 0. A time series keeps only
|
||||
the points that match and drops a series with none left; a scalar result
|
||||
keeps only the rows that match.
|
||||
description: Accepted but currently ignored; the computed values are returned
|
||||
unfiltered.
|
||||
legend:
|
||||
description: The display name template for each series; {{field}} placeholders
|
||||
are replaced by label values.
|
||||
@@ -32731,305 +32729,6 @@ paths:
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_anomaly_request_rate:
|
||||
description: 'anomaly is the one function with named arguments. seasonality
|
||||
says which past periods predict the current value: hourly, daily
|
||||
or weekly, daily when absent. z_score_threshold is how many standard
|
||||
deviations from the prediction count as an anomaly. It has no default:
|
||||
a request without it gets bounds of zero width, so send it. 3 is
|
||||
the value the UI sends. The response carries, next to the values,
|
||||
a predicted series, an upper and a lower bound series and an anomaly
|
||||
score series. The scoring runs in the enterprise build; the community
|
||||
build returns the series unchanged.'
|
||||
summary: 'anomaly: request rate scored against its daily pattern'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- metricName: http.server.duration.count
|
||||
spaceAggregation: sum
|
||||
timeAggregation: rate
|
||||
functions:
|
||||
- args:
|
||||
- name: seasonality
|
||||
value: daily
|
||||
- name: z_score_threshold
|
||||
value: 3
|
||||
name: anomaly
|
||||
groupBy:
|
||||
- fieldContext: resource
|
||||
name: service.name
|
||||
name: A
|
||||
signal: metrics
|
||||
stepInterval: 300s
|
||||
type: builder_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_cumulativesum_errors_so_far:
|
||||
description: cumulativeSum replaces each point with the sum of every
|
||||
point up to and including it, so a per-step count becomes a running
|
||||
total whose first point is the first count. It takes no arguments.
|
||||
The total resets with the window, so a longer time range gives a
|
||||
larger final value; it is a view of the window, not a counter.
|
||||
summary: 'cumulativeSum: 5xx responses since the start of the window'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- alias: server_errors
|
||||
expression: count()
|
||||
filter:
|
||||
expression: http.response.status_code >= 500
|
||||
functions:
|
||||
- name: cumulativeSum
|
||||
groupBy:
|
||||
- fieldContext: resource
|
||||
name: service.name
|
||||
name: A
|
||||
signal: traces
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_cutoffmin_hide_idle_hosts:
|
||||
description: The four threshold functions take one number. cutOffMin
|
||||
and cutOffMax remove the points below or above the threshold, so
|
||||
an idle host disappears from the chart instead of drawing a line
|
||||
along the bottom. clampMin and clampMax keep the points and replace
|
||||
the value with the threshold, which caps spikes without hiding them.
|
||||
system.cpu.utilization is a ratio, so 0.05 is 5%.
|
||||
summary: 'cutOffMin: drop hosts below 5% CPU from the chart'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- metricName: system.cpu.utilization
|
||||
spaceAggregation: avg
|
||||
timeAggregation: avg
|
||||
functions:
|
||||
- args:
|
||||
- value: 0.05
|
||||
name: cutOffMin
|
||||
groupBy:
|
||||
- fieldContext: resource
|
||||
name: host.name
|
||||
name: A
|
||||
signal: metrics
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_ewma_smoothed_latency:
|
||||
description: 'ewma3, ewma5 and ewma7 replace each point with an exponentially
|
||||
weighted average of the point and the points before it. The number
|
||||
in the name is the window the default weight is derived from: alpha
|
||||
= 2 / (n + 1), so 0.5, 0.333 and 0.25. One optional argument overrides
|
||||
alpha; a value close to 1 follows the raw series, a value close
|
||||
to 0 smooths harder. 0.3 here smooths a little more than ewma5''s
|
||||
default.'
|
||||
summary: 'ewma5: smoothed p95 latency'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- metricName: http.server.duration.bucket
|
||||
spaceAggregation: p95
|
||||
functions:
|
||||
- args:
|
||||
- value: 0.3
|
||||
name: ewma5
|
||||
groupBy:
|
||||
- fieldContext: resource
|
||||
name: service.name
|
||||
name: A
|
||||
signal: metrics
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_fillzero_sparse_errors:
|
||||
description: A count over sparse events returns a point only for steps
|
||||
that had a match, so a chart of failed spans has gaps where nothing
|
||||
failed. fillZero adds a zero at every missing step. With no arguments
|
||||
the server fills the request window at the query's step, which is
|
||||
the common case. The three optional arguments are start, end and
|
||||
step in milliseconds, for callers that want to fill a different
|
||||
grid.
|
||||
summary: 'fillZero: a zero at every step with no failed spans'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- alias: failed_spans
|
||||
expression: count()
|
||||
filter:
|
||||
expression: has_error = true
|
||||
functions:
|
||||
- name: fillZero
|
||||
groupBy:
|
||||
- fieldContext: resource
|
||||
name: service.name
|
||||
name: A
|
||||
signal: traces
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_log10_network_bytes:
|
||||
description: log2 and log10 replace each value with its logarithm,
|
||||
so a loopback device moving kilobytes and an uplink moving gigabytes
|
||||
fit on one chart. They take no arguments. Zero and negative values
|
||||
have no logarithm and are not drawn. absolute, also without arguments,
|
||||
is the usual companion for a signed series before a logarithm.
|
||||
summary: 'log10: network throughput across devices that differ by
|
||||
orders of magnitude'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- metricName: system.network.io
|
||||
spaceAggregation: sum
|
||||
timeAggregation: rate
|
||||
filter:
|
||||
expression: direction = 'transmit'
|
||||
functions:
|
||||
- name: log10
|
||||
groupBy:
|
||||
- fieldContext: attribute
|
||||
name: device
|
||||
name: A
|
||||
signal: metrics
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_median_despiked_request_rate:
|
||||
description: median3, median5 and median7 replace each point with
|
||||
the median of the surrounding 3, 5 or 7 points. A spike that lasts
|
||||
one step vanishes, a shift that lasts longer than half the window
|
||||
stays. The first and last half window, 2 points each for median5,
|
||||
have no full window and keep their values, so the newest spike stays
|
||||
visible; a series shorter than the window is returned unchanged.
|
||||
They take no arguments. Use them when the raw series is noisy and
|
||||
the chart is for a trend, not for catching the spike.
|
||||
summary: 'median5: request rate without one-step spikes'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- metricName: http.server.duration.count
|
||||
spaceAggregation: sum
|
||||
timeAggregation: rate
|
||||
functions:
|
||||
- name: median5
|
||||
groupBy:
|
||||
- fieldContext: resource
|
||||
name: service.name
|
||||
name: A
|
||||
signal: metrics
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_runningdiff_disk_growth:
|
||||
description: 'runningDiff turns a gauge into the change between consecutive
|
||||
points, so a filesystem usage gauge becomes bytes written per step.
|
||||
It takes no arguments and drops the first point of the window, since
|
||||
that point has nothing to difference against; the server reads one
|
||||
step before the window to keep the first interval. Functions apply
|
||||
in order: clampMin with 0 runs on the differences and replaces the
|
||||
negative value a cleanup or a volume reset produces with zero.'
|
||||
summary: 'runningDiff then clampMin: bytes written to disk per step'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- metricName: system.filesystem.usage
|
||||
spaceAggregation: sum
|
||||
timeAggregation: latest
|
||||
filter:
|
||||
expression: state = 'used'
|
||||
functions:
|
||||
- name: runningDiff
|
||||
- args:
|
||||
- value: 0
|
||||
name: clampMin
|
||||
groupBy:
|
||||
- fieldContext: resource
|
||||
name: host.name
|
||||
name: A
|
||||
signal: metrics
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
function_timeshift_week_over_week:
|
||||
description: timeShift moves every point of its query forward by the
|
||||
argument, in seconds, so a series read from the past lands on the
|
||||
current timestamps. B here reads the hour exactly seven days (604800
|
||||
s) before the requested window and comes back on this week's timestamps,
|
||||
so it overlays A on a chart and the formula can subtract it point
|
||||
by point. The argument is positive; it says how far back to read.
|
||||
summary: 'timeShift: this week''s error logs next to last week''s'
|
||||
value:
|
||||
compositeQuery:
|
||||
queries:
|
||||
- spec:
|
||||
aggregations:
|
||||
- alias: errors
|
||||
expression: count()
|
||||
filter:
|
||||
expression: severity_text = 'ERROR'
|
||||
name: A
|
||||
signal: logs
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
- spec:
|
||||
aggregations:
|
||||
- alias: errors
|
||||
expression: count()
|
||||
filter:
|
||||
expression: severity_text = 'ERROR'
|
||||
functions:
|
||||
- args:
|
||||
- value: 604800
|
||||
name: timeShift
|
||||
name: B
|
||||
signal: logs
|
||||
stepInterval: 60s
|
||||
type: builder_query
|
||||
- spec:
|
||||
expression: A - B
|
||||
name: F1
|
||||
type: builder_formula
|
||||
end: 1640998800000
|
||||
requestType: time_series
|
||||
schemaVersion: v1
|
||||
start: 1640995200000
|
||||
logs_raw:
|
||||
summary: 'Raw: fetch raw log records'
|
||||
value:
|
||||
|
||||
@@ -7079,7 +7079,7 @@ export interface Querybuildertypesv5QueryBuilderFormulaDTO {
|
||||
*/
|
||||
functions?: Querybuildertypesv5FunctionDTO[] | null;
|
||||
/**
|
||||
* @description A filter on the formula values, referencing the formula by its name or as __result, for example F1 > 0. A time series keeps only the points that match and drops a series with none left; a scalar result keeps only the rows that match.
|
||||
* @description Accepted but currently ignored; the computed values are returned unfiltered.
|
||||
*/
|
||||
having?: Querybuildertypesv5HavingDTO;
|
||||
/**
|
||||
|
||||
@@ -903,6 +903,40 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toStrictEqual({ expression: '' });
|
||||
});
|
||||
|
||||
it('sends the AI slot as builder_ai_query envelopes, ignoring builder', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
id: 'q-ai',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [baseBuilderQuery({ queryName: 'M' })],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
builder_ai_query: {
|
||||
queryData: [
|
||||
baseBuilderQuery({ queryName: 'A', dataSource: DataSource.TRACES }),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const { queries } =
|
||||
prepareQueryRangePayloadV5(props).queryPayload.compositeQuery;
|
||||
expect(
|
||||
queries.map((q) => [q.type, (q.spec as { name: string }).name]),
|
||||
).toStrictEqual([['builder_ai_query', 'A']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapPanelTypeToRequestType', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { toWireQuery } from 'lib/query/builderSlot';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
@@ -559,9 +560,15 @@ export const prepareQueryRangePayloadV5 = (
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
|
||||
switch (query.queryType) {
|
||||
const wireQuery = toWireQuery(query);
|
||||
|
||||
switch (wireQuery.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
|
||||
const {
|
||||
queryData: data,
|
||||
queryFormulas,
|
||||
queryTraceOperator,
|
||||
} = wireQuery.builder;
|
||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||
|
||||
@@ -623,13 +630,13 @@ export const prepareQueryRangePayloadV5 = (
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
const promQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
const promQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
|
||||
queries = convertPromQueriesToV5(promQueries.queries);
|
||||
legendMap = promQueries.legends;
|
||||
break;
|
||||
}
|
||||
case EQueryType.CLICKHOUSE: {
|
||||
const chQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
const chQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
|
||||
queries = convertClickHouseQueriesToV5(chQueries.queries);
|
||||
legendMap = chQueries.legends;
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
@@ -49,6 +50,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
initialDataSource,
|
||||
handleRunQuery,
|
||||
} = useQueryBuilder();
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
@@ -129,28 +131,28 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
const renderedQueries = useMemo(
|
||||
() =>
|
||||
isMultiQueryAllowed
|
||||
? currentQuery.builder.queryData
|
||||
: currentQuery.builder.queryData.slice(0, 1),
|
||||
[isMultiQueryAllowed, currentQuery.builder.queryData],
|
||||
? activeBuilder.queryData
|
||||
: activeBuilder.queryData.slice(0, 1),
|
||||
[isMultiQueryAllowed, activeBuilder.queryData],
|
||||
);
|
||||
|
||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||
if (
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0
|
||||
activeBuilder.queryTraceOperator &&
|
||||
activeBuilder.queryTraceOperator.length > 0
|
||||
) {
|
||||
return currentQuery.builder.queryTraceOperator[0];
|
||||
return activeBuilder.queryTraceOperator[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [currentQuery.builder.queryTraceOperator]);
|
||||
}, [activeBuilder.queryTraceOperator]);
|
||||
|
||||
const hasAtLeastOneTraceQuery = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.some(
|
||||
activeBuilder.queryData.some(
|
||||
(query) => query.dataSource === DataSource.TRACES,
|
||||
),
|
||||
[currentQuery.builder.queryData],
|
||||
[activeBuilder.queryData],
|
||||
);
|
||||
|
||||
const hasTraceOperator = useMemo(
|
||||
@@ -225,9 +227,9 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
{!isMultiQueryAllowed ? (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
key={activeBuilder.queryData[0].queryName}
|
||||
index={0}
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
query={activeBuilder.queryData[0]}
|
||||
fieldsConfig={fieldsConfig}
|
||||
allowedDataSources={queryDataSources}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
@@ -238,7 +240,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isRawQuery={isRawQuery}
|
||||
signalSource={currentQuery.builder.queryData[0].source as 'meter' | ''}
|
||||
signalSource={activeBuilder.queryData[0].source as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={1}
|
||||
@@ -264,28 +266,26 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
signalSource={query.source as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={currentQuery.builder.queryData.length}
|
||||
queriesCount={activeBuilder.queryData.length}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
currentQuery.builder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
currentQuery.builder.queryData[index] ||
|
||||
currentQuery.builder.queryData[0];
|
||||
{!showOnlyWhereClause && activeBuilder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{activeBuilder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
activeBuilder.queryData[index] || activeBuilder.queryData[0];
|
||||
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula query={query} formula={formula} index={index} isQBV2 />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula query={query} formula={formula} index={index} isQBV2 />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
@@ -319,7 +319,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{currentQuery.builder.queryFormulas?.map((formula) => (
|
||||
{activeBuilder.queryFormulas?.map((formula) => (
|
||||
<div key={formula.queryName} className="formula-name">
|
||||
{formula.queryName}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
@@ -15,28 +16,25 @@ function TraceOperatorSection({
|
||||
addTraceOperator?: () => void;
|
||||
}): JSX.Element {
|
||||
const { currentQuery, panelType } = useQueryBuilder();
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
|
||||
const showTraceOperatorWarning = useMemo(() => {
|
||||
const isRawQueryPanel =
|
||||
panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE;
|
||||
const hasMultipleQueries = currentQuery.builder.queryData.length > 1;
|
||||
const hasMultipleQueries = activeBuilder.queryData.length > 1;
|
||||
const hasTraceOperator =
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0;
|
||||
activeBuilder.queryTraceOperator &&
|
||||
activeBuilder.queryTraceOperator.length > 0;
|
||||
return isRawQueryPanel && hasMultipleQueries && !hasTraceOperator;
|
||||
}, [
|
||||
currentQuery?.builder?.queryData,
|
||||
currentQuery?.builder?.queryTraceOperator,
|
||||
panelType,
|
||||
]);
|
||||
}, [activeBuilder.queryData, activeBuilder.queryTraceOperator, panelType]);
|
||||
|
||||
const traceOperatorWarning = useMemo(() => {
|
||||
if (currentQuery.builder.queryData.length === 0) {
|
||||
if (activeBuilder.queryData.length === 0) {
|
||||
return '';
|
||||
}
|
||||
const firstQuery = currentQuery.builder.queryData[0];
|
||||
const firstQuery = activeBuilder.queryData[0];
|
||||
return `Currently, you are only seeing results from query ${firstQuery.queryName}. Add a trace operator to combine results of multiple queries.`;
|
||||
}, [currentQuery]);
|
||||
}, [activeBuilder.queryData]);
|
||||
return (
|
||||
<div className="qb-trace-operator-button-container">
|
||||
<Tooltip
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
@@ -132,6 +133,7 @@ function QuerySearch({
|
||||
metricNamespace,
|
||||
valueSuggestionsOverride,
|
||||
}: QuerySearchProps): JSX.Element {
|
||||
const builderQueryType = useBuilderQueryType(queryData);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||
const [activeKey, setActiveKey] = useState<string>('');
|
||||
@@ -330,7 +332,7 @@ function QuerySearch({
|
||||
source: signalSource as TelemetrytypesSourceDTO,
|
||||
metricNamespace,
|
||||
},
|
||||
queryData.builderQueryType,
|
||||
builderQueryType,
|
||||
);
|
||||
|
||||
if (response.data.keys) {
|
||||
@@ -368,7 +370,7 @@ function QuerySearch({
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
metricNamespace,
|
||||
queryData.builderQueryType,
|
||||
builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -510,7 +512,7 @@ function QuerySearch({
|
||||
source: signalSource as TelemetrytypesSourceDTO,
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
},
|
||||
queryData.builderQueryType,
|
||||
builderQueryType,
|
||||
).then((response) => {
|
||||
const responseData = response.data;
|
||||
const responseDataValues = responseData.values;
|
||||
@@ -613,7 +615,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
toggleSuggestions,
|
||||
valueSuggestionsOverride,
|
||||
queryData.builderQueryType,
|
||||
builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
|
||||
import { QueryProps } from 'container/QueryBuilder/type';
|
||||
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Copy, Ellipsis, Trash } from '@signozhq/icons';
|
||||
@@ -62,7 +63,8 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const hasQueryFunctions = query?.functions?.length > 0;
|
||||
const { dataSource, builderQueryType } = query;
|
||||
const { dataSource } = query;
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import {
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
@@ -87,17 +88,18 @@ function TraceOperatorEditor({
|
||||
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
||||
const [lastExternalValue, setLastExternalValue] = useState<string>('');
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
|
||||
const queryOptions = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData
|
||||
activeBuilder.queryData
|
||||
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
|
||||
.map((query) => ({
|
||||
label: query.queryName,
|
||||
type: 'atom',
|
||||
apply: query.queryName,
|
||||
})),
|
||||
[currentQuery.builder.queryData],
|
||||
[activeBuilder.queryData],
|
||||
);
|
||||
|
||||
const toggleSuggestions = useCallback(
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import type {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('../QueryV2/QueryV2', () => ({
|
||||
QueryV2: ({ query }: { query: IBuilderQuery }): JSX.Element => (
|
||||
<div data-testid="query-v2">{query.queryName}</div>
|
||||
),
|
||||
}));
|
||||
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
function renderWith(currentQuery: Query): void {
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
currentQuery,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
initialDataSource: null,
|
||||
handleSetConfig: jest.fn(),
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useQueryBuilder>);
|
||||
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TIME_SERIES} version="v3" />);
|
||||
}
|
||||
|
||||
const builderWithQuery = (
|
||||
builder: Query['builder'],
|
||||
queryName: string,
|
||||
): Query['builder'] => ({
|
||||
...builder,
|
||||
queryData: [{ ...builder.queryData[0], queryName }],
|
||||
});
|
||||
|
||||
const query: Query = {
|
||||
...initialQueriesMap.metrics,
|
||||
builder: builderWithQuery(initialQueriesMap.metrics.builder, 'MAIN'),
|
||||
builder_ai_query: builderWithQuery(
|
||||
getActiveBuilder(initialQueryAIWithType),
|
||||
'AI',
|
||||
),
|
||||
};
|
||||
|
||||
describe('QueryBuilderV2 builder slot', () => {
|
||||
it('renders the AI slot when AI is the query type', () => {
|
||||
renderWith({ ...query, queryType: EQueryType.AI_QUERY_BUILDER });
|
||||
|
||||
expect(screen.getByTestId('query-v2')).toHaveTextContent('AI');
|
||||
});
|
||||
|
||||
it('renders builder when Query Builder is the query type', () => {
|
||||
renderWith(query);
|
||||
|
||||
expect(screen.getByTestId('query-v2')).toHaveTextContent('MAIN');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
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 { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
@@ -19,7 +19,7 @@ const aiQuery: Query = {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
builderQueryType: EQueryType.AI_QUERY_BUILDER,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
adjustQueryForV5,
|
||||
convertAggregationToExpression,
|
||||
convertExpressionToFilters,
|
||||
convertFiltersToExpression,
|
||||
@@ -1684,3 +1687,31 @@ describe('formatValueForExpression', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('adjustQueryForV5', () => {
|
||||
it('adjusts the AI builder slot for AI query builder queries', () => {
|
||||
const emptyBuilder = {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
const query: Query = {
|
||||
id: 'q',
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: emptyBuilder,
|
||||
builder_ai_query: {
|
||||
...emptyBuilder,
|
||||
queryData: [initialQueryBuilderFormValuesMap.logs],
|
||||
},
|
||||
};
|
||||
|
||||
const result = adjustQueryForV5(query);
|
||||
|
||||
expect(result.builder).toBe(emptyBuilder);
|
||||
expect(result.builder_ai_query?.queryData[0]).not.toHaveProperty(
|
||||
'aggregateOperator',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -20,7 +21,8 @@ export function useSyncQueryBuilderFields(
|
||||
fields: SyncedQueryBuilderField[] = NO_FIELDS,
|
||||
): void {
|
||||
const { currentQuery, handleSetQueryData } = useQueryBuilder();
|
||||
const queries = currentQuery.builder.queryData;
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
const queries = activeBuilder.queryData;
|
||||
const previousRef = useRef<Map<string, IBuilderQuery>>();
|
||||
const enabled = fields.length > 0;
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderSlotKey,
|
||||
isBuilderQueryType,
|
||||
} from 'lib/query/builderSlot';
|
||||
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
||||
import FilterQueryParser, {
|
||||
AndExpressionContext,
|
||||
@@ -33,7 +38,6 @@ import {
|
||||
MetricAggregation,
|
||||
TraceAggregation,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isQuoted, unquote } from 'utils/stringUtils';
|
||||
@@ -1030,8 +1034,9 @@ export function getQueryLabelWithAggregation(
|
||||
}
|
||||
|
||||
export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const newQueryData = currentQuery.builder.queryData.map((query) => {
|
||||
if (isBuilderQueryType(currentQuery.queryType)) {
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
const newQueryData = activeBuilder.queryData.map((query) => {
|
||||
const aggregations = query.aggregations?.map((aggregation) => {
|
||||
if (query.dataSource === DataSource.METRICS) {
|
||||
const metricAggregation = aggregation as MetricAggregation;
|
||||
@@ -1072,8 +1077,8 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
[getBuilderSlotKey(currentQuery.queryType)]: {
|
||||
...activeBuilder,
|
||||
queryData: newQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ function QueryTypeTag({ queryType }: IQueryTypeTagProps): JSX.Element {
|
||||
switch (queryType) {
|
||||
case EQueryType.QUERY_BUILDER:
|
||||
return <span>Query Builder</span>;
|
||||
case EQueryType.AI_QUERY_BUILDER:
|
||||
return <span>AI Query Builder</span>;
|
||||
|
||||
case EQueryType.CLICKHOUSE:
|
||||
return <span>ClickHouse Query</span>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
Query,
|
||||
TagFilter,
|
||||
@@ -556,3 +557,49 @@ describe('clearFilterFromQuery', () => {
|
||||
expect(active.filter?.expression).toBe(`http.method = 'GET'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quick filters on an AI query', () => {
|
||||
const builderQuery = buildQuery([], '').builder;
|
||||
const aiQuery = {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder: builderQuery,
|
||||
builder_ai_query: buildQuery(
|
||||
[{ key: KEY, op: 'in', value: ['a'] }],
|
||||
`${KEY} IN ['a']`,
|
||||
).builder,
|
||||
} as unknown as Query;
|
||||
|
||||
it('a checkbox toggle writes into the AI slot and leaves builder alone', () => {
|
||||
const result = applyCheckboxToggle({
|
||||
currentQuery: aiQuery,
|
||||
activeQueryIndex: 0,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
quickFilterSource: QuickFiltersSource.AI_OBSERVABILITY,
|
||||
attributeValues: ['a', 'b', 'c'],
|
||||
value: 'b',
|
||||
checked: true,
|
||||
isOnlyOrAllClicked: false,
|
||||
});
|
||||
|
||||
expect(result.builder).toBe(builderQuery);
|
||||
expect(
|
||||
result.builder_ai_query?.queryData[0].filters?.items.map(
|
||||
(item) => item.value,
|
||||
),
|
||||
).toStrictEqual([['a', 'b']]);
|
||||
});
|
||||
|
||||
it('clearing a filter clears it from the AI slot', () => {
|
||||
const result = clearFilterFromQuery({
|
||||
currentQuery: aiQuery,
|
||||
filterKey: KEY,
|
||||
activeQueryIndex: 0,
|
||||
});
|
||||
|
||||
expect(result.builder).toBe(builderQuery);
|
||||
expect(result.builder_ai_query?.queryData[0].filters?.items).toStrictEqual(
|
||||
[],
|
||||
);
|
||||
expect(result.builder_ai_query?.queryData[0].filter?.expression).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { cloneDeep, isArray } from 'lodash-es';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -157,8 +162,9 @@ export function applyCheckboxToggle({
|
||||
previousState?: CheckedState;
|
||||
sectionType?: SectionType;
|
||||
}): Query {
|
||||
const activeItems =
|
||||
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const activeItems = builder.queryData?.[activeQueryIndex]?.filters?.items;
|
||||
|
||||
const isSomeFilterPresentForCurrentAttribute = !!activeItems?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
@@ -173,7 +179,7 @@ export function applyCheckboxToggle({
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
|
||||
const query = cloneDeep(builder.queryData?.[activeQueryIndex]);
|
||||
|
||||
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||
@@ -430,18 +436,15 @@ export function applyCheckboxToggle({
|
||||
query.filters = synced.filters;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
...currentQuery.builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
return withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: [
|
||||
...builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
|
||||
import { DEFAULT_VISIBLE_ITEMS_COUNT } from './constants';
|
||||
import { isKeyMatch } from './utils';
|
||||
@@ -37,12 +38,13 @@ function useCheckboxDisclosure({
|
||||
DEFAULT_VISIBLE_ITEMS_COUNT,
|
||||
);
|
||||
|
||||
const { queryData } = getActiveBuilder(currentQuery);
|
||||
const isSomeFilterPresentForCurrentAttribute = useMemo(
|
||||
() =>
|
||||
!!currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
!!queryData?.[activeQueryIndex]?.filters?.items?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
),
|
||||
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
[queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
);
|
||||
|
||||
const isOpen = useMemo(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEqual, isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -77,7 +78,7 @@ function useCheckboxFilterActions({
|
||||
dispatch(updatedQuery);
|
||||
|
||||
if (onQuickFilterChange) {
|
||||
const queryData = updatedQuery.builder.queryData[activeQueryIndex];
|
||||
const queryData = getActiveBuilder(updatedQuery).queryData[activeQueryIndex];
|
||||
const expression = queryData?.filter?.expression || '';
|
||||
const filterItemKeys = (queryData?.filters?.items || [])
|
||||
.map((item) => item.key?.key)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
|
||||
import { deriveCheckboxState } from './checkboxFilterQuery';
|
||||
import { isKeyMatch } from './utils';
|
||||
@@ -31,33 +32,28 @@ function useCheckboxFilterState({
|
||||
activeQueryIndex,
|
||||
}: UseCheckboxFilterStateProps): UseCheckboxFilterStateReturn {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
|
||||
// derive the state of each filter key here and keep it in sync with current query
|
||||
const currentFilterState = useMemo(
|
||||
() =>
|
||||
deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems:
|
||||
currentQuery?.builder.queryData?.[activeQueryIndex]?.filters?.items,
|
||||
filterItems: activeQueryData?.[activeQueryIndex]?.filters?.items,
|
||||
filterKey: filter.attributeKey.key,
|
||||
}),
|
||||
[
|
||||
attributeValues,
|
||||
currentQuery?.builder.queryData,
|
||||
filter.attributeKey,
|
||||
activeQueryIndex,
|
||||
],
|
||||
[attributeValues, activeQueryData, filter.attributeKey, activeQueryIndex],
|
||||
);
|
||||
|
||||
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||
const isFilterDisabled = useMemo(
|
||||
() =>
|
||||
(currentQuery?.builder?.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items?.filter((item) =>
|
||||
(activeQueryData?.[activeQueryIndex]?.filters?.items?.filter((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)?.length || 0) > 1,
|
||||
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
|
||||
[activeQueryData, activeQueryIndex, filter.attributeKey],
|
||||
);
|
||||
|
||||
// whether the current filter has multiple values to its name in the key op value section
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -128,18 +129,15 @@ export default function CheckboxFilterV2(
|
||||
onToggleOpen();
|
||||
};
|
||||
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
const currentFilterOp = useMemo(() => {
|
||||
const filterSync = currentQuery?.builder.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items.find((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
const filterSync = activeQueryData?.[activeQueryIndex]?.filters?.items.find(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
return filterSync?.op;
|
||||
}, [
|
||||
currentQuery?.builder.queryData,
|
||||
activeQueryIndex,
|
||||
filter.attributeKey.key,
|
||||
]);
|
||||
}, [activeQueryData, activeQueryIndex, filter.attributeKey.key]);
|
||||
|
||||
const isNotInOperator = NON_SELECTED_OPERATORS.includes(currentFilterOp || '');
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { QuickFilterCheckboxUseFieldApis } from 'components/QuickFilters/types';
|
||||
|
||||
@@ -18,6 +19,9 @@ export function useExistingQuery({
|
||||
activeQueryIndex,
|
||||
}: UseExistingQueryParams): UseExistingQueryResult {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const stagedQueryData = stagedQuery
|
||||
? getActiveBuilder(stagedQuery).queryData
|
||||
: undefined;
|
||||
|
||||
const existingQuery = useMemo(() => {
|
||||
if (useFieldApis.existingQuery === null) {
|
||||
@@ -28,7 +32,7 @@ export function useExistingQuery({
|
||||
return useFieldApis.existingQuery;
|
||||
}
|
||||
|
||||
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
|
||||
const queryData = stagedQueryData?.[activeQueryIndex];
|
||||
|
||||
// Prefer V5 filter.expression
|
||||
if (queryData?.filter?.expression) {
|
||||
@@ -41,21 +45,17 @@ export function useExistingQuery({
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [
|
||||
useFieldApis.existingQuery,
|
||||
stagedQuery?.builder.queryData,
|
||||
activeQueryIndex,
|
||||
]);
|
||||
}, [useFieldApis.existingQuery, stagedQueryData, activeQueryIndex]);
|
||||
|
||||
// Check if ANY filters exist in query (V3 items or V5 expression)
|
||||
// This is separate from existingQuery because existingQuery can be explicitly
|
||||
// disabled (null) while filters still exist in the query for UI purposes
|
||||
const hasExistingQuery = useMemo(() => {
|
||||
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
|
||||
const queryData = stagedQueryData?.[activeQueryIndex];
|
||||
const hasV3Items = (queryData?.filters?.items?.length ?? 0) > 0;
|
||||
const hasV5Expression = !!queryData?.filter?.expression;
|
||||
return hasV3Items || hasV5Expression || !!existingQuery;
|
||||
}, [stagedQuery?.builder.queryData, activeQueryIndex, existingQuery]);
|
||||
}, [stagedQueryData, activeQueryIndex, existingQuery]);
|
||||
|
||||
return { existingQuery, hasExistingQuery };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
import { getMs } from 'utils/timeUtils';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
|
||||
import {
|
||||
AllTraceFilterKeys,
|
||||
@@ -57,7 +63,8 @@ function Duration({
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const syncSelectedFilters = useMemo((): FilterType => {
|
||||
const filters =
|
||||
compositeQuery?.builder.queryData?.[activeQueryIndex]?.filters;
|
||||
compositeQuery &&
|
||||
getActiveBuilder(compositeQuery).queryData?.[activeQueryIndex]?.filters;
|
||||
if (!filters) {
|
||||
return {} as FilterType;
|
||||
}
|
||||
@@ -183,49 +190,51 @@ function Duration({
|
||||
};
|
||||
|
||||
const removeFilterItemIds = (query: Query): Query => {
|
||||
const clonedQuery = cloneDeep(query);
|
||||
clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({
|
||||
...data,
|
||||
filters: {
|
||||
...data.filters,
|
||||
items:
|
||||
data.filters?.items?.map((item) => ({
|
||||
...item,
|
||||
id: '',
|
||||
})) || [],
|
||||
op: data.filters?.op || 'AND',
|
||||
},
|
||||
}));
|
||||
return clonedQuery;
|
||||
const slot = getBuilderSlotKey(query.queryType);
|
||||
const builder = getBuilderAt(query, slot);
|
||||
return withBuilderAt(cloneDeep(query), slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((data) => ({
|
||||
...data,
|
||||
filters: {
|
||||
...data.filters,
|
||||
items:
|
||||
data.filters?.items?.map((item) => ({
|
||||
...item,
|
||||
id: '',
|
||||
})) || [],
|
||||
op: data.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
const handleRun = useCallback(
|
||||
(props?: HandleRunProps): void => {
|
||||
const preparedQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: props?.resetAll
|
||||
? []
|
||||
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
|
||||
.map((item) =>
|
||||
item.key?.key === props?.clearByType ? undefined : item,
|
||||
)
|
||||
.filter((i) => i) as TagFilterItem[]),
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const preparedQuery: Query = withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: props?.resetAll
|
||||
? []
|
||||
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
|
||||
.map((item) =>
|
||||
item.key?.key === props?.clearByType ? undefined : item,
|
||||
)
|
||||
.filter((i) => i) as TagFilterItem[]),
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const currentQueryWithoutIds = removeFilterItemIds(currentQuery);
|
||||
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getKeySpellings, isKeyMatch } from '../Checkbox/utils';
|
||||
@@ -20,34 +25,33 @@ export function clearFilterFromQuery({
|
||||
activeQueryIndex: number;
|
||||
operators?: string[];
|
||||
}): Query {
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(
|
||||
item.filter?.expression ?? '',
|
||||
getKeySpellings(filterKey),
|
||||
false,
|
||||
operators,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filterKey),
|
||||
) || [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
return withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(
|
||||
item.filter?.expression ?? '',
|
||||
getKeySpellings(filterKey),
|
||||
false,
|
||||
operators,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filterKey),
|
||||
) || [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ import {
|
||||
QuickFilterReadPermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/quick-filter.permissions';
|
||||
import { isFunction } from 'lodash-es';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import useActiveQueryIndex from './hooks/useActiveQueryIndex';
|
||||
@@ -87,15 +93,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
const [open, setOpen] = useState(false);
|
||||
const activeBuilder = currentQuery
|
||||
? getActiveBuilder(currentQuery)
|
||||
: undefined;
|
||||
|
||||
// Sync lastUsedQuery when queries change (e.g., after deletion)
|
||||
const validQueryIndex = useMemo(
|
||||
() =>
|
||||
Math.min(
|
||||
lastUsedQuery || 0,
|
||||
(currentQuery?.builder?.queryData?.length || 1) - 1,
|
||||
),
|
||||
[lastUsedQuery, currentQuery?.builder?.queryData?.length],
|
||||
Math.min(lastUsedQuery || 0, (activeBuilder?.queryData?.length || 1) - 1),
|
||||
[lastUsedQuery, activeBuilder?.queryData?.length],
|
||||
);
|
||||
|
||||
// Determine if we're in ListView mode
|
||||
@@ -103,15 +109,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// Generate query options based on available queries
|
||||
const queryOptions = useMemo(() => {
|
||||
if (!currentQuery?.builder?.queryData) {
|
||||
if (!activeBuilder?.queryData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return currentQuery.builder.queryData.map((query, index) => ({
|
||||
return activeBuilder.queryData.map((query, index) => ({
|
||||
label: query.queryName || String.fromCharCode(65 + index),
|
||||
value: index,
|
||||
}));
|
||||
}, [currentQuery?.builder?.queryData]);
|
||||
}, [activeBuilder?.queryData]);
|
||||
|
||||
// Show dropdown in ListView only for TRACES_EXPLORER source
|
||||
const shouldShowDropdownInListView =
|
||||
@@ -127,36 +133,35 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// clear all the filters for the query which is in sync with filters
|
||||
const handleReset = (): void => {
|
||||
const activeQuery = currentQuery?.builder.queryData?.[activeQueryIndex];
|
||||
const activeQuery = activeBuilder?.queryData?.[activeQueryIndex];
|
||||
|
||||
if (!activeQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preparedQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const preparedQuery: Query = withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
expression: '',
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
expression: '',
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(preparedQuery);
|
||||
@@ -166,13 +171,12 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
};
|
||||
|
||||
const lastQueryName =
|
||||
showQueryName &&
|
||||
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
showQueryName && activeBuilder?.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
|
||||
// In ListView, always show the 0th query's name; otherwise use the active query's name
|
||||
const displayedQueryName =
|
||||
isListView || isAIObservabilityRowView
|
||||
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
|
||||
? showQueryName && activeBuilder?.queryData?.[0]?.queryName
|
||||
: lastQueryName;
|
||||
|
||||
const handleQueryChange = (value: number): void => {
|
||||
|
||||
@@ -311,14 +311,10 @@ export const initialQueryMeterWithType: Query = {
|
||||
|
||||
export const initialQueryAIWithType: Query = {
|
||||
...initialQueryWithType,
|
||||
builder: {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: {
|
||||
...initialQueryWithType.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
builderQueryType: 'builder_ai_query',
|
||||
},
|
||||
],
|
||||
queryData: [initialQueryBuilderFormValuesMap.traces],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -21,15 +21,14 @@ import {
|
||||
useListDashboardsForUserV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
import {
|
||||
getListRulesV3QueryKey,
|
||||
useListRulesV3,
|
||||
getListRulesQueryKey,
|
||||
useListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import type {
|
||||
DashboardtypesListedDashboardForUserV2DTO,
|
||||
ListDashboardsForUserV2200,
|
||||
ListDashboardsForUserV2Params,
|
||||
ListRulesV3200,
|
||||
ListRulesV3Params,
|
||||
ListRules200,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useQueryService } from 'hooks/useQueryService';
|
||||
@@ -180,9 +179,6 @@ const VOICE_UNAVAILABLE_KEY = 'ai-assistant-voice-unavailable';
|
||||
*/
|
||||
const DASHBOARD_LIST_PARAMS: ListDashboardsForUserV2Params = { limit: 1000 };
|
||||
|
||||
/** Same as dashboards. 200 is the most the rules list returns per call. */
|
||||
const ALERT_RULE_LIST_PARAMS: ListRulesV3Params = { limit: 200 };
|
||||
|
||||
function dashboardTitle(
|
||||
dashboard: DashboardtypesListedDashboardForUserV2DTO,
|
||||
): string {
|
||||
@@ -759,7 +755,7 @@ export default function ChatInput({
|
||||
data: alertsResponse,
|
||||
isLoading: isAlertsLoading,
|
||||
isError: isAlertsError,
|
||||
} = useListRulesV3(ALERT_RULE_LIST_PARAMS, {
|
||||
} = useListRules({
|
||||
query: {
|
||||
enabled: isContextPickerOpen && activeContextCategory === 'Alerts',
|
||||
staleTime: Infinity,
|
||||
@@ -808,10 +804,10 @@ export default function ChatInput({
|
||||
}
|
||||
}
|
||||
if (ctx.type === 'alert' && ctx.resourceId) {
|
||||
const cached = queryClient.getQueryData<ListRulesV3200>(
|
||||
getListRulesV3QueryKey(ALERT_RULE_LIST_PARAMS),
|
||||
const cached = queryClient.getQueryData<ListRules200>(
|
||||
getListRulesQueryKey(),
|
||||
);
|
||||
const rule = cached?.data.rules.find((r) => r.id === ctx.resourceId);
|
||||
const rule = cached?.data?.find((r) => r.id === ctx.resourceId);
|
||||
if (rule?.alert) {
|
||||
return rule.alert;
|
||||
}
|
||||
@@ -839,8 +835,8 @@ export default function ChatInput({
|
||||
value: dashboardTitle(dashboard),
|
||||
})) ?? [],
|
||||
Alerts:
|
||||
alertsResponse?.data.rules
|
||||
.filter((alertRule) => Boolean(alertRule.alert))
|
||||
alertsResponse?.data
|
||||
?.filter((alertRule) => Boolean(alertRule.alert))
|
||||
.map((alertRule) => ({
|
||||
id: alertRule.id,
|
||||
value: alertRule.alert,
|
||||
@@ -1063,7 +1059,6 @@ export default function ChatInput({
|
||||
color="secondary"
|
||||
size="sm"
|
||||
id={`ai-context-tab-${category}`}
|
||||
testId={`ai-context-tab-${category}`}
|
||||
// Single stable panel id shared by every tab: only the
|
||||
// active category's tabpanel is rendered, so per-category
|
||||
// `aria-controls` ids would point at nonexistent nodes
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import {
|
||||
alertRulesFixture,
|
||||
listRulesV3Response,
|
||||
} from 'mocks-server/__mockdata__/alert_rules';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
jest.mock('api/generated/services/dashboard', () => ({
|
||||
useListDashboardsForUserV2: (): unknown => ({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
}),
|
||||
getListDashboardsForUserV2QueryKey: (): string[] => ['dashboards'],
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useQueryService', () => ({
|
||||
useQueryService: (): unknown => ({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useSpeechRecognition', () => ({
|
||||
useSpeechRecognition: (): unknown => ({
|
||||
isListening: false,
|
||||
isSupported: false,
|
||||
permission: 'prompt',
|
||||
start: jest.fn(),
|
||||
discard: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../hooks/useAIAssistantAnalyticsContext', () => ({
|
||||
useAIAssistantAnalyticsContext: (): unknown => ({
|
||||
threadId: undefined,
|
||||
page: '/',
|
||||
mode: 'sidepane',
|
||||
}),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import ChatInput from '../ChatInput';
|
||||
|
||||
describe('ChatInput — alerts context picker', () => {
|
||||
it('lists the rules from one v3 rules request', async () => {
|
||||
const [first, second] = alertRulesFixture;
|
||||
const requests: string[] = [];
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v3/rules', (req, res, ctx) => {
|
||||
requests.push(req.url.search);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(listRulesV3Response([first, second], req.url.searchParams)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(<ChatInput onSend={jest.fn()} />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /add context/i }));
|
||||
await userEvent.click(await screen.findByTestId('ai-context-tab-Alerts'));
|
||||
|
||||
await expect(screen.findByText(first.alert)).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText(second.alert)).toBeInTheDocument();
|
||||
expect(requests).toStrictEqual(['?limit=200']);
|
||||
});
|
||||
});
|
||||
@@ -12,12 +12,8 @@ jest.mock('api/generated/services/dashboard', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/rules', () => ({
|
||||
useListRulesV3: (): unknown => ({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
}),
|
||||
getListRulesV3QueryKey: (): string[] => ['rules'],
|
||||
useListRules: (): unknown => ({ data: [], isLoading: false, isError: false }),
|
||||
getListRulesQueryKey: (): string[] => ['rules'],
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useQueryService', () => ({
|
||||
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
Query,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/** Param shape emitted by the AI for every filter-touching action. */
|
||||
@@ -56,12 +61,11 @@ export function replaceFirstQueryData(
|
||||
query: Query,
|
||||
updated: IBuilderQuery,
|
||||
): Query {
|
||||
const queryData = [...query.builder.queryData];
|
||||
const slot = getBuilderSlotKey(query.queryType);
|
||||
const builder = getBuilderAt(query, slot);
|
||||
const queryData = [...builder.queryData];
|
||||
queryData[0] = updated;
|
||||
return {
|
||||
...query,
|
||||
builder: { ...query.builder, queryData },
|
||||
};
|
||||
return withBuilderAt(query, slot, { ...builder, queryData });
|
||||
}
|
||||
|
||||
/** Page-state deps required by the runQuery / addFilter actions. */
|
||||
|
||||
@@ -40,6 +40,7 @@ export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
[EQueryType.AI_QUERY_BUILDER]: () => [],
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
|
||||
@@ -18,7 +18,7 @@ import { buildCreateAlertRulePayload, validateCreateAlertState } from './utils';
|
||||
import './styles.scss';
|
||||
import {
|
||||
invalidateGetRuleByID,
|
||||
invalidateListRulesV3,
|
||||
invalidateListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
@@ -137,7 +137,7 @@ function Footer(): JSX.Element {
|
||||
{
|
||||
onSuccess: () => {
|
||||
void invalidateGetRuleByID(queryClient, { id: ruleId });
|
||||
void invalidateListRulesV3(queryClient);
|
||||
void invalidateListRules(queryClient);
|
||||
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
|
||||
@@ -218,6 +218,7 @@ function FormAlertRules({
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
[EQueryType.AI_QUERY_BUILDER]: () => [],
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import type { RuletypesListableRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useListRules } from 'api/generated/services/rules';
|
||||
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { AppLink } from 'lib/router/AppLink';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import { useAppLocation } from 'lib/router/useAppLocation';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { ArrowRight, ArrowUpRight, Plus } from '@signozhq/icons';
|
||||
import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { toCompositeMetricQuery } from 'types/api/alerts/convert';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import beaconUrl from '@/assets/Icons/beacon.svg';
|
||||
|
||||
import { getItemIcon } from '../constants';
|
||||
import { useHomeAlertRules } from './useHomeAlertRules';
|
||||
|
||||
export default function AlertRules({
|
||||
onUpdateChecklistDoneItem,
|
||||
@@ -26,19 +28,49 @@ export default function AlertRules({
|
||||
loadingUserPreferences: boolean;
|
||||
}): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const [rulesExist, setRulesExist] = useState(false);
|
||||
|
||||
const [sortedAlertRules, setSortedAlertRules] = useState<RuletypesRuleDTO[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const location = useAppLocation();
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
// Fetch Alerts
|
||||
const { rules: alertRules, isError, isLoading } = useHomeAlertRules();
|
||||
const rulesExist = alertRules.length > 0;
|
||||
const {
|
||||
data: alerts,
|
||||
isError,
|
||||
isLoading,
|
||||
} = useListRules({
|
||||
query: { cacheTime: 0 },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (rulesExist && !loadingUserPreferences) {
|
||||
const rules = alerts?.data ?? [];
|
||||
setRulesExist(rules.length > 0);
|
||||
|
||||
const sortedRules = [...rules].sort((a, b) => {
|
||||
// First, prioritize firing alerts
|
||||
if (a.state === 'firing' && b.state !== 'firing') {
|
||||
return -1;
|
||||
}
|
||||
if (a.state !== 'firing' && b.state === 'firing') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Then sort by updatedAt timestamp
|
||||
return (
|
||||
new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime()
|
||||
);
|
||||
});
|
||||
|
||||
if (sortedRules.length > 0 && !loadingUserPreferences) {
|
||||
onUpdateChecklistDoneItem('SETUP_ALERTS');
|
||||
}
|
||||
}, [rulesExist, onUpdateChecklistDoneItem, loadingUserPreferences]);
|
||||
|
||||
setSortedAlertRules(sortedRules.slice(0, 5));
|
||||
}, [alerts, onUpdateChecklistDoneItem, loadingUserPreferences]);
|
||||
|
||||
const emptyStateCard = (): JSX.Element => (
|
||||
<div className="empty-state-container">
|
||||
@@ -63,7 +95,7 @@ export default function AlertRules({
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={(): void => {
|
||||
void logEvent('Homepage: Create alert rule clicked', {});
|
||||
logEvent('Homepage: Create alert rule clicked', {});
|
||||
}}
|
||||
>
|
||||
Create Alert Rule
|
||||
@@ -74,7 +106,7 @@ export default function AlertRules({
|
||||
type="link"
|
||||
className="learn-more-link"
|
||||
onClick={(): void => {
|
||||
void logEvent('Homepage: Learn more clicked', {
|
||||
logEvent('Homepage: Learn more clicked', {
|
||||
source: 'Alert Rules',
|
||||
});
|
||||
|
||||
@@ -93,13 +125,26 @@ export default function AlertRules({
|
||||
</div>
|
||||
);
|
||||
|
||||
const onEditHandler = (record: RuletypesListableRuleDTO) => (): void => {
|
||||
void logEvent('Homepage: Alert clicked', {
|
||||
const onEditHandler = (record: RuletypesRuleDTO) => (): void => {
|
||||
logEvent('Homepage: Alert clicked', {
|
||||
ruleId: record.id,
|
||||
ruleName: record.alert,
|
||||
ruleState: record.state,
|
||||
});
|
||||
|
||||
const compositeQuery = mapQueryDataFromApi(
|
||||
toCompositeMetricQuery(record.condition.compositeQuery),
|
||||
);
|
||||
params.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const panelType = record.condition.compositeQuery.panelType;
|
||||
if (panelType) {
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
}
|
||||
|
||||
params.set(QueryParams.ruleId, record.id);
|
||||
|
||||
navigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
@@ -108,12 +153,11 @@ export default function AlertRules({
|
||||
const renderAlertRules = (): JSX.Element => (
|
||||
<div className="alert-rules-container home-data-item-container">
|
||||
<div className="alert-rules-list">
|
||||
{alertRules.map((rule) => (
|
||||
{sortedAlertRules.map((rule) => (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="alert-rule-item home-data-item"
|
||||
data-testid={`home-alert-rule-${rule.id}`}
|
||||
key={rule.id}
|
||||
onClick={onEditHandler(rule)}
|
||||
onKeyDown={(e): void => {
|
||||
@@ -195,7 +239,7 @@ export default function AlertRules({
|
||||
type="link"
|
||||
className="periscope-btn link learn-more-link"
|
||||
onClick={(): void => {
|
||||
void logEvent('Homepage: All alert rules clicked', {});
|
||||
logEvent('Homepage: All alert rules clicked', {});
|
||||
}}
|
||||
>
|
||||
All Alert Rules <ArrowRight size={12} />
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useListRulesV3 } from 'api/generated/services/rules';
|
||||
import {
|
||||
type ListRulesV3200,
|
||||
type ListRulesV3Params,
|
||||
RuletypesAlertStateDTO,
|
||||
type RuletypesListableRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
|
||||
|
||||
const HOME_ALERT_RULES_LIMIT = 5;
|
||||
|
||||
const FIRING_RULES_PARAMS: ListRulesV3Params = {
|
||||
states: [RuletypesAlertStateDTO.firing],
|
||||
limit: HOME_ALERT_RULES_LIMIT,
|
||||
};
|
||||
|
||||
interface HomeAlertRules {
|
||||
rules: RuletypesListableRuleDTO[];
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export function useHomeAlertRules(): HomeAlertRules {
|
||||
const firing = useListRulesV3(FIRING_RULES_PARAMS, {
|
||||
query: {
|
||||
cacheTime: 0,
|
||||
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
|
||||
queryFn: ({ signal }) =>
|
||||
GeneratedAPIInstance<ListRulesV3200>({
|
||||
url: '/api/v3/rules',
|
||||
method: 'GET',
|
||||
params: FIRING_RULES_PARAMS,
|
||||
paramsSerializer: { indexes: null },
|
||||
signal,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const firingRules = firing.data?.data.rules;
|
||||
const latest = useListRulesV3(
|
||||
{ limit: HOME_ALERT_RULES_LIMIT },
|
||||
{
|
||||
query: {
|
||||
cacheTime: 0,
|
||||
enabled:
|
||||
firingRules !== undefined && firingRules.length < HOME_ALERT_RULES_LIMIT,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const rules = useMemo(() => {
|
||||
const firingIds = new Set(firingRules?.map(({ id }) => id));
|
||||
const others = (latest.data?.data.rules ?? []).filter(
|
||||
({ id }) => !firingIds.has(id),
|
||||
);
|
||||
return [...(firingRules ?? []), ...others].slice(0, HOME_ALERT_RULES_LIMIT);
|
||||
}, [firingRules, latest.data]);
|
||||
|
||||
return {
|
||||
rules,
|
||||
isLoading: firing.isLoading || latest.isLoading,
|
||||
isError: firing.isError || latest.isError,
|
||||
};
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import {
|
||||
RuletypesAlertStateDTO,
|
||||
type RuletypesRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { navigate } from 'lib/router/navigation';
|
||||
import {
|
||||
alertRulesFixture,
|
||||
listRulesV3Response,
|
||||
} from 'mocks-server/__mockdata__/alert_rules';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import AlertRules from '../AlertRules/AlertRules';
|
||||
|
||||
jest.mock('lib/router/navigation', () => ({
|
||||
...jest.requireActual('lib/router/navigation'),
|
||||
navigate: jest.fn(),
|
||||
}));
|
||||
|
||||
const rule = (id: string, state: RuletypesAlertStateDTO): RuletypesRuleDTO => ({
|
||||
...alertRulesFixture[0],
|
||||
id,
|
||||
alert: `${id} rule`,
|
||||
state,
|
||||
});
|
||||
|
||||
const burning = rule('burning', RuletypesAlertStateDTO.firing);
|
||||
const quiet = rule('quiet', RuletypesAlertStateDTO.inactive);
|
||||
const waiting = rule('waiting', RuletypesAlertStateDTO.pending);
|
||||
|
||||
const serveRules = (
|
||||
rules: RuletypesRuleDTO[],
|
||||
rulesForLatest = rules,
|
||||
): string[] => {
|
||||
const requestedStates: string[] = [];
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v3/rules', (req, res, ctx) => {
|
||||
const states = req.url.searchParams.getAll('states').join();
|
||||
requestedStates.push(states);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
listRulesV3Response(
|
||||
states === 'firing' ? rules : rulesForLatest,
|
||||
req.url.searchParams,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
return requestedStates;
|
||||
};
|
||||
|
||||
const renderAlertRules = (): void => {
|
||||
render(
|
||||
<AlertRules
|
||||
onUpdateChecklistDoneItem={jest.fn()}
|
||||
loadingUserPreferences={false}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
const renderedRuleIds = (): (string | undefined)[] =>
|
||||
screen.getAllByTestId(/^home-alert-rule-/).map((row) => row.dataset.testid);
|
||||
|
||||
describe('Home alert rules', () => {
|
||||
it('lists firing rules first, then the latest others, and opens the clicked rule', async () => {
|
||||
const requestedStates = serveRules([quiet, burning, waiting]);
|
||||
renderAlertRules();
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('home-alert-rule-waiting'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(renderedRuleIds()).toStrictEqual([
|
||||
'home-alert-rule-burning',
|
||||
'home-alert-rule-quiet',
|
||||
'home-alert-rule-waiting',
|
||||
]);
|
||||
expect(requestedStates).toStrictEqual(['firing', '']);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByTestId('home-alert-rule-burning'));
|
||||
expect(navigate).toHaveBeenCalledWith(
|
||||
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=burning`,
|
||||
);
|
||||
});
|
||||
|
||||
it('skips the latest rules request when 5 rules are firing', async () => {
|
||||
const firing = Array.from({ length: 5 }, (_, index) =>
|
||||
rule(`firing-${index}`, RuletypesAlertStateDTO.firing),
|
||||
);
|
||||
const requestedStates = serveRules([quiet, ...firing]);
|
||||
renderAlertRules();
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('home-alert-rule-firing-4'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(renderedRuleIds()).toStrictEqual(
|
||||
firing.map(({ id }) => `home-alert-rule-${id}`),
|
||||
);
|
||||
expect(requestedStates).toStrictEqual(['firing']);
|
||||
});
|
||||
|
||||
it('shows a rule once when it stops firing between the two requests', async () => {
|
||||
const settled = { ...burning, state: RuletypesAlertStateDTO.inactive };
|
||||
serveRules([burning], [settled, quiet]);
|
||||
renderAlertRules();
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('home-alert-rule-quiet'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(renderedRuleIds()).toStrictEqual([
|
||||
'home-alert-rule-burning',
|
||||
'home-alert-rule-quiet',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
@@ -186,7 +187,9 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const isFilterApplied = useMemo(() => {
|
||||
// if any of the non-disabled queries has filters applied, return true
|
||||
const result = stagedQuery?.builder?.queryData?.filter(
|
||||
const result = (
|
||||
stagedQuery ? getActiveBuilder(stagedQuery) : undefined
|
||||
)?.queryData?.filter(
|
||||
(item) => !isEmpty(item.filters?.items) && !item.disabled,
|
||||
);
|
||||
return !!result?.length;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
@@ -35,7 +36,7 @@ function TimeSeriesViewContainer({
|
||||
const isValidToConvertToMs = useMemo(() => {
|
||||
const isValid: boolean[] = [];
|
||||
|
||||
currentQuery.builder.queryData.forEach(
|
||||
getActiveBuilder(currentQuery).queryData.forEach(
|
||||
({ aggregateAttribute, aggregateOperator }) => {
|
||||
const isExistDurationNanoAttribute =
|
||||
aggregateAttribute?.key === 'durationNano' ||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
FilterDeps,
|
||||
replaceFirstQueryData,
|
||||
} from 'container/AIAssistant/pageActions/builderQueryHelpers';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import {
|
||||
ActionResult,
|
||||
PageAction,
|
||||
@@ -93,7 +94,7 @@ export function tracesRunQueryAction(
|
||||
},
|
||||
autoApply: true,
|
||||
execute: async ({ filters }): Promise<ActionResult> => {
|
||||
const baseQuery = deps.currentQuery.builder.queryData[0];
|
||||
const baseQuery = getActiveBuilder(deps.currentQuery).queryData[0];
|
||||
if (!baseQuery) {
|
||||
throw new Error('No active query found in Traces Explorer.');
|
||||
}
|
||||
@@ -117,7 +118,7 @@ export function tracesRunQueryAction(
|
||||
},
|
||||
getContext: (): Record<string, unknown> => ({
|
||||
filters:
|
||||
deps.currentQuery.builder.queryData[0]?.filters?.items?.map(
|
||||
getActiveBuilder(deps.currentQuery).queryData[0]?.filters?.items?.map(
|
||||
(f: TagFilterItem) => ({
|
||||
key: f.key?.key,
|
||||
op: f.op,
|
||||
@@ -158,7 +159,7 @@ export function tracesAddFilterAction(
|
||||
},
|
||||
autoApply: true,
|
||||
execute: async ({ key, op, value }): Promise<ActionResult> => {
|
||||
const baseQuery = deps.currentQuery.builder.queryData[0];
|
||||
const baseQuery = getActiveBuilder(deps.currentQuery).queryData[0];
|
||||
if (!baseQuery) {
|
||||
throw new Error('No active query found in Traces Explorer.');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { parseListOrderBy } from 'utils/explorerUtils';
|
||||
@@ -15,8 +16,9 @@ export const getListViewQuery = (
|
||||
? [parseListOrderBy(orderBy)]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
const queryData = query.builder.queryData[i];
|
||||
const { queryData: builderQueries } = getActiveBuilder(query);
|
||||
for (let i = 0; i < builderQueries.length; i++) {
|
||||
const queryData = builderQueries[i];
|
||||
queryData.groupBy = [];
|
||||
queryData.having = {
|
||||
expression: '',
|
||||
|
||||
@@ -16,11 +16,11 @@ import {
|
||||
useDeleteDowntimeScheduleByID,
|
||||
useListDowntimeSchedules,
|
||||
} from 'api/generated/services/downtimeschedules';
|
||||
import { useListRules } from 'api/generated/services/rules';
|
||||
import type { AlertmanagertypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { ColumnSelector } from 'components/TanStackTableView';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import dayjs from 'dayjs';
|
||||
import { useListAllRules } from 'hooks/alerts/useListAllRules';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -55,7 +55,9 @@ export function PlannedDowntime(): JSX.Element {
|
||||
isError,
|
||||
isLoading,
|
||||
isFetching: isFetchingAlerts,
|
||||
} = useListAllRules({ cacheTime: 0 });
|
||||
} = useListRules({
|
||||
query: { cacheTime: 0 },
|
||||
});
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const { user } = useAppContext();
|
||||
@@ -71,7 +73,7 @@ export function PlannedDowntime(): JSX.Element {
|
||||
const downtimeSchedules = useListDowntimeSchedules();
|
||||
const alertOptions = React.useMemo(
|
||||
() =>
|
||||
alertsData?.map((i) => ({
|
||||
alertsData?.data?.map((i) => ({
|
||||
label: i.alert,
|
||||
value: i.id,
|
||||
})),
|
||||
@@ -179,7 +181,7 @@ export function PlannedDowntime(): JSX.Element {
|
||||
};
|
||||
|
||||
const refetchAllSchedules = (): void => {
|
||||
void downtimeSchedules.refetch();
|
||||
downtimeSchedules.refetch();
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { fireEvent, screen } from '@testing-library/react';
|
||||
import type {
|
||||
ListDowntimeSchedules200,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesListableRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
AlertmanagertypesRepeatOnDTO,
|
||||
@@ -127,19 +126,19 @@ jest.mock('api/generated/services/downtimeschedules', () => ({
|
||||
}));
|
||||
|
||||
let mockRulesState: {
|
||||
data?: Pick<RuletypesListableRuleDTO, 'id' | 'alert'>[];
|
||||
data?: { data: Array<{ id: number | string; alert: string }> };
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
} = {
|
||||
data: [],
|
||||
data: { data: [] },
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
};
|
||||
|
||||
jest.mock('hooks/alerts/useListAllRules', () => ({
|
||||
useListAllRules: (): typeof mockRulesState => mockRulesState,
|
||||
jest.mock('api/generated/services/rules', () => ({
|
||||
useListRules: (): typeof mockRulesState => mockRulesState,
|
||||
}));
|
||||
|
||||
describe('PlannedDowntime Component', () => {
|
||||
@@ -152,7 +151,7 @@ describe('PlannedDowntime Component', () => {
|
||||
});
|
||||
mockDowntimeQueryResult.data = MOCK_DOWNTIME_RESPONSE;
|
||||
mockRulesState = {
|
||||
data: [],
|
||||
data: { data: [] },
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
@@ -363,7 +362,7 @@ describe('PlannedDowntime Component', () => {
|
||||
|
||||
it('shows "Unknown rule" when scoped rule IDs cannot be resolved (Bug 56)', async () => {
|
||||
mockRulesState = {
|
||||
data: [],
|
||||
data: { data: [] },
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
@@ -388,7 +387,14 @@ describe('PlannedDowntime Component', () => {
|
||||
|
||||
it('shows resolved alert rule name when scoped rule ID exists', async () => {
|
||||
mockRulesState = {
|
||||
data: [{ id: 'rule-42', alert: 'Disk Space High' }],
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: 'rule-42',
|
||||
alert: 'Disk Space High',
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getMetricNameForQuery } from '../utils';
|
||||
import OptionRenderer from '../OptionRenderer/OptionRenderer';
|
||||
import { GroupByFilterProps } from './GroupByFilter.interfaces';
|
||||
import { toAttributeKey } from './utils';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
|
||||
export const GroupByFilter = memo(function GroupByFilter({
|
||||
query,
|
||||
@@ -45,6 +46,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
|
||||
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
|
||||
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
|
||||
@@ -52,7 +54,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
metricName: getMetricNameForQuery(query),
|
||||
searchText: debouncedValue,
|
||||
},
|
||||
query.builderQueryType,
|
||||
builderQueryType,
|
||||
{ enabled: !disabled && isFocused, keepPreviousData: true },
|
||||
);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { selectStyle } from '../QueryBuilderSearchV2/config';
|
||||
import { getMetricNameForQuery } from '../utils';
|
||||
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
||||
import { useOrderByFilter } from './useOrderByFilter';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
|
||||
export function OrderByFilter({
|
||||
query,
|
||||
@@ -34,6 +35,7 @@ export function OrderByFilter({
|
||||
handleSearchKeys,
|
||||
} = useOrderByFilter({ query, onChange, entityVersion });
|
||||
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
|
||||
@@ -41,7 +43,7 @@ export function OrderByFilter({
|
||||
metricName: getMetricNameForQuery(query),
|
||||
searchText: debouncedSearchText,
|
||||
},
|
||||
query.builderQueryType,
|
||||
builderQueryType,
|
||||
{
|
||||
enabled: !!query.aggregateAttribute?.key || isRawQuery,
|
||||
keepPreviousData: true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { type RefSelectProps, Select, Spin } from 'antd';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { Pill } from '@signozhq/ui/pill';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
@@ -856,14 +857,10 @@ function QueryBuilderSearchV2(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query]);
|
||||
|
||||
const isLastQuery = useMemo(
|
||||
() =>
|
||||
isEqual(
|
||||
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
|
||||
query,
|
||||
),
|
||||
[currentQuery, query],
|
||||
);
|
||||
const isLastQuery = useMemo(() => {
|
||||
const { queryData } = getActiveBuilder(currentQuery);
|
||||
return isEqual(queryData[queryData.length - 1], query);
|
||||
}, [currentQuery, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLastQuery) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { ContextLinksData } from 'types/api/widgets/widget';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -41,7 +42,7 @@ export function useGraphContextMenu({
|
||||
} {
|
||||
const drilldownQuery = useGetCompositeQueryParam() || query;
|
||||
|
||||
const isQueryTypeBuilder = drilldownQuery?.queryType === 'builder';
|
||||
const isQueryTypeBuilder = isBuilderQueryType(drilldownQuery?.queryType);
|
||||
|
||||
const { aggregateDrilldownConfig } = useAggregateDrilldown({
|
||||
query: drilldownQuery,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAppParams } from 'lib/router/useAppParams';
|
||||
import cx from 'classnames';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import Download from 'container/Download/Download';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import {
|
||||
createTableColumnsFromQuery,
|
||||
RowData,
|
||||
@@ -33,7 +34,7 @@ export function QueryTable({
|
||||
...props
|
||||
}: QueryTableProps): JSX.Element {
|
||||
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
|
||||
const isQueryTypeBuilder = query.queryType === 'builder';
|
||||
const isQueryTypeBuilder = isBuilderQueryType(query.queryType);
|
||||
|
||||
const { servicename: encodedServiceName } = useAppParams<'servicename'>();
|
||||
const servicename = decodeURIComponent(encodedServiceName || '');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import WidgetHeader from 'container/WidgetCard/Header';
|
||||
import { mockAppLocation } from 'tests/router-mocks';
|
||||
import { fireEvent, render } from 'tests/test-utils';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { QueryTable } from '../QueryTable';
|
||||
import { QueryTableProps, WidgetHeaderProps } from './mocks';
|
||||
@@ -29,6 +30,21 @@ describe('QueryTable -', () => {
|
||||
const tableRows = container.querySelectorAll('tr.ant-table-row');
|
||||
expect(tableRows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should enable drilldown cells for AI query builder queries', () => {
|
||||
const { container } = render(
|
||||
<QueryTable
|
||||
{...QueryTableProps}
|
||||
query={{
|
||||
...QueryTableProps.query,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: QueryTableProps.query.builder,
|
||||
}}
|
||||
enableDrillDown
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.clickable-cell')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const setSearchTerm = jest.fn();
|
||||
|
||||
@@ -14,12 +14,12 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import {
|
||||
getCustomTimeRangeWindowSweepInMS,
|
||||
getStartAndEndTimesInMilliseconds,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { useGraphClickToShowButton } from 'container/WidgetCard/hooks/useGraphClickToShowButton';
|
||||
@@ -132,13 +132,13 @@ function WidgetGraphComponent({
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
|
||||
const widgetQueryData = widget?.query
|
||||
? getActiveBuilder(widget.query)?.queryData
|
||||
: undefined;
|
||||
|
||||
const graphClick = useGraphClickToShowButton({
|
||||
graphRef: currentGraphRef?.current ? currentGraphRef : graphRef,
|
||||
isButtonEnabled: (widget?.query?.builder?.queryData &&
|
||||
Array.isArray(widget.query.builder.queryData)
|
||||
? widget.query.builder.queryData
|
||||
: []
|
||||
).some(
|
||||
isButtonEnabled: (Array.isArray(widgetQueryData) ? widgetQueryData : []).some(
|
||||
(q) =>
|
||||
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
|
||||
),
|
||||
@@ -219,7 +219,7 @@ function WidgetGraphComponent({
|
||||
customOnDragSelect={customOnDragSelect}
|
||||
setCurrentGraphRef={setCurrentGraphRef}
|
||||
enableDrillDown={
|
||||
enableDrillDown && widget?.query?.queryType === EQueryType.QUERY_BUILDER
|
||||
enableDrillDown && isBuilderQueryType(widget?.query?.queryType)
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
createColumnsAndDataSource,
|
||||
@@ -47,6 +48,18 @@ describe('Table Panel utils', () => {
|
||||
expect(getQueryLegend(query, 'B')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getQueryLegend reads the AI builder slot', () => {
|
||||
const builderQuery = widgetQueryWithLegend as Query;
|
||||
const query: Query = {
|
||||
...builderQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
|
||||
builder_ai_query: builderQuery.builder,
|
||||
};
|
||||
|
||||
expect(getQueryLegend(query, 'A')).toBe('p99');
|
||||
});
|
||||
|
||||
it('sorter function for table sorting', () => {
|
||||
let rowA: {
|
||||
A: string | number;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
|
||||
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEmpty, isNaN } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -119,14 +120,15 @@ export function getQueryLegend(
|
||||
let legend: string | undefined;
|
||||
switch (currentQuery.queryType) {
|
||||
case EQueryType.QUERY_BUILDER:
|
||||
case EQueryType.AI_QUERY_BUILDER:
|
||||
// check if the value is present in the queries
|
||||
legend = currentQuery?.builder?.queryData?.find(
|
||||
legend = getActiveBuilder(currentQuery)?.queryData?.find(
|
||||
(query) => query.queryName === queryName,
|
||||
)?.legend;
|
||||
|
||||
if (!legend) {
|
||||
// check if the value is present in the formula
|
||||
legend = currentQuery?.builder?.queryFormulas?.find(
|
||||
legend = getActiveBuilder(currentQuery)?.queryFormulas?.find(
|
||||
(query) => query.queryName === queryName,
|
||||
)?.legend;
|
||||
}
|
||||
@@ -199,7 +201,7 @@ export function createColumnsAndDataSource(
|
||||
: undefined;
|
||||
|
||||
const isNewAggregation =
|
||||
currentQuery?.builder?.queryData?.find(
|
||||
getActiveBuilder(currentQuery).queryData?.find(
|
||||
(query) => query.queryName === item.queryName,
|
||||
)?.aggregations?.length || 0;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import ValueGraph from 'components/ValueGraph';
|
||||
import { generateGridTitle } from 'utils/generateGridTitle';
|
||||
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
|
||||
import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
@@ -72,8 +73,7 @@ function GridValueComponent({
|
||||
);
|
||||
}
|
||||
|
||||
const isQueryTypeBuilder =
|
||||
widget?.query?.queryType === EQueryType.QUERY_BUILDER;
|
||||
const isQueryTypeBuilder = isBuilderQueryType(widget?.query?.queryType);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -58,6 +58,27 @@ describe('getLegend', () => {
|
||||
expect(legendsData).toBe('alias_sum');
|
||||
});
|
||||
|
||||
it('should read the AI builder slot for AI query builder queries', () => {
|
||||
const payloadQuery = getMockQuery({
|
||||
...mockQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: {
|
||||
...mockQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...mockQuery.builder.queryData[0],
|
||||
queryName: mockQueryData.queryName,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregations: [{ expression: "sum(bytes) as 'alias_sum'" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const legendsData = getLegend(mockQueryData, payloadQuery, MOCK_LABEL_NAME);
|
||||
expect(legendsData).toBe('alias_sum');
|
||||
});
|
||||
|
||||
it('should return legend when single builder query with no alias but legend set (builder)', () => {
|
||||
const payloadQuery = getMockQuery({
|
||||
...mockQuery,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
alertRulesFixture,
|
||||
listRulesV3Response,
|
||||
} from 'mocks-server/__mockdata__/alert_rules';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { AllTheProviders, renderHook, waitFor } from 'tests/test-utils';
|
||||
|
||||
import { useListAllRules } from '../useListAllRules';
|
||||
|
||||
const TOTAL = 250;
|
||||
|
||||
const rules = Array.from({ length: TOTAL }, (_, index) => ({
|
||||
...alertRulesFixture[0],
|
||||
id: `rule-${index}`,
|
||||
alert: `Rule ${index}`,
|
||||
}));
|
||||
|
||||
describe('useListAllRules', () => {
|
||||
it('fetches every page past the 200-rule cap', async () => {
|
||||
const offsets: string[] = [];
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v3/rules', (req, res, ctx) => {
|
||||
offsets.push(req.url.searchParams.get('offset') ?? '');
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(listRulesV3Response(rules, req.url.searchParams)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useListAllRules(), {
|
||||
wrapper: AllTheProviders,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data).toHaveLength(TOTAL);
|
||||
expect(result.current.data?.[TOTAL - 1]?.id).toBe(`rule-${TOTAL - 1}`);
|
||||
expect(offsets.sort()).toStrictEqual(['0', '200']);
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import {
|
||||
getListRulesV3QueryKey,
|
||||
listRulesV3,
|
||||
} from 'api/generated/services/rules';
|
||||
import type { RuletypesListableRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { range } from 'lodash-es';
|
||||
|
||||
// The list API clamps any larger limit to 200.
|
||||
const PAGE_SIZE = 200;
|
||||
|
||||
export const LIST_ALL_RULES_QUERY_KEY = [...getListRulesV3QueryKey(), 'all'];
|
||||
|
||||
function fetchPage(offset: number): ReturnType<typeof listRulesV3> {
|
||||
return listRulesV3({ limit: PAGE_SIZE, offset });
|
||||
}
|
||||
|
||||
// TODO(AshwinBhatkal): temporary. Replace with server-side search plus a lookup of saved rules by ID once
|
||||
// /api/v3/rules can filter by IDs: https://github.com/SigNoz/pulse-pod/issues/715
|
||||
async function fetchAllRules(): Promise<RuletypesListableRuleDTO[]> {
|
||||
const first = await fetchPage(0);
|
||||
const rest = await Promise.all(
|
||||
range(PAGE_SIZE, first.data.total, PAGE_SIZE).map(fetchPage),
|
||||
);
|
||||
|
||||
return [...first.data.rules, ...rest.flatMap((page) => page.data.rules)];
|
||||
}
|
||||
|
||||
export function useListAllRules(
|
||||
options?: Omit<
|
||||
UseQueryOptions<RuletypesListableRuleDTO[], Error>,
|
||||
'queryKey' | 'queryFn'
|
||||
>,
|
||||
): UseQueryResult<RuletypesListableRuleDTO[], Error> {
|
||||
return useQuery<RuletypesListableRuleDTO[], Error>({
|
||||
queryKey: LIST_ALL_RULES_QUERY_KEY,
|
||||
queryFn: fetchAllRules,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { useBuilderQueryType } from '../useBuilderQueryType';
|
||||
import { useQueryBuilder } from '../useQueryBuilder';
|
||||
|
||||
jest.mock('../useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
|
||||
const withQueryType = (queryType: EQueryType): void => {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: { queryType },
|
||||
});
|
||||
};
|
||||
|
||||
describe('useBuilderQueryType', () => {
|
||||
it('is AI for every query while AI is the query type', () => {
|
||||
withQueryType(EQueryType.AI_QUERY_BUILDER);
|
||||
|
||||
expect(renderHook(() => useBuilderQueryType({})).result.current).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
|
||||
it("follows the query's own tag otherwise", () => {
|
||||
withQueryType(EQueryType.QUERY_BUILDER);
|
||||
|
||||
expect(
|
||||
renderHook(() =>
|
||||
useBuilderQueryType({ builderQueryType: 'builder_ai_query' }),
|
||||
).result.current,
|
||||
).toBe('builder_ai_query');
|
||||
expect(
|
||||
renderHook(() => useBuilderQueryType({})).result.current,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { initialQueriesMap, initialQueryState } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { AllTheProviders, renderHook } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -105,18 +106,25 @@ describe('useGetCompositeQueryParam', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { QUERY_BUILDER, AI_QUERY_BUILDER } = EQueryType;
|
||||
[
|
||||
[ROUTES.LOGS_EXPLORER, DataSource.LOGS, ''],
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER, DataSource.TRACES, ''],
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER, DataSource.METRICS, ''],
|
||||
[ROUTES.METER_EXPLORER, DataSource.METRICS, TelemetrytypesSourceDTO.meter],
|
||||
].forEach(([route, dataSource, source]) => {
|
||||
[ROUTES.LOGS_EXPLORER, QUERY_BUILDER, DataSource.LOGS, ''],
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER, AI_QUERY_BUILDER, DataSource.TRACES, ''],
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER, QUERY_BUILDER, DataSource.METRICS, ''],
|
||||
[
|
||||
ROUTES.METER_EXPLORER,
|
||||
QUERY_BUILDER,
|
||||
DataSource.METRICS,
|
||||
TelemetrytypesSourceDTO.meter,
|
||||
],
|
||||
].forEach(([route, queryType, dataSource, source]) => {
|
||||
const query = renderOn(clickhouseWithOperator, route);
|
||||
const builder = query && getActiveBuilder(query);
|
||||
|
||||
expect(query?.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(query?.builder.queryData[0].dataSource).toBe(dataSource);
|
||||
expect(query?.builder.queryData[0].source ?? '').toBe(source);
|
||||
expect(query?.builder.queryTraceOperator).toStrictEqual([]);
|
||||
expect(query?.queryType).toBe(queryType);
|
||||
expect(builder?.queryData[0].dataSource).toBe(dataSource);
|
||||
expect(builder?.queryData[0].source ?? '').toBe(source);
|
||||
expect(builder?.queryTraceOperator).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import {
|
||||
Querybuildertypesv5QueryTypeDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { BuilderQuerySource, DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type ExplorerQueryRules = {
|
||||
dataSource: DataSource;
|
||||
/** `meter` on the meter explorer, which shares the metrics signal; '' elsewhere. */
|
||||
source: BuilderQuerySource;
|
||||
builderQueryType: BuilderQueryType;
|
||||
/** AI_QUERY_BUILDER on the AI explorer, whose queries live in the AI slot. */
|
||||
queryType: EQueryType.QUERY_BUILDER | EQueryType.AI_QUERY_BUILDER;
|
||||
allowsTraceOperator: boolean;
|
||||
};
|
||||
|
||||
@@ -18,31 +16,31 @@ const EXPLORER_QUERY_RULES: Partial<Record<string, ExplorerQueryRules>> = {
|
||||
[ROUTES.LOGS_EXPLORER]: {
|
||||
dataSource: DataSource.LOGS,
|
||||
source: '',
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.TRACES_EXPLORER]: {
|
||||
dataSource: DataSource.TRACES,
|
||||
source: '',
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
allowsTraceOperator: true,
|
||||
},
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER]: {
|
||||
dataSource: DataSource.TRACES,
|
||||
source: '',
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_ai_query,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER]: {
|
||||
dataSource: DataSource.METRICS,
|
||||
source: '',
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.METER_EXPLORER]: {
|
||||
dataSource: DataSource.METRICS,
|
||||
source: TelemetrytypesSourceDTO.meter,
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,11 +8,8 @@ import {
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
QueryMode,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -44,8 +41,9 @@ function firstBuilderQuery(
|
||||
queryData: unknown[],
|
||||
pathname: string,
|
||||
): IBuilderQuery {
|
||||
return normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname)
|
||||
.query.builder.queryData[0];
|
||||
return getActiveBuilder(
|
||||
normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname).query,
|
||||
).queryData[0];
|
||||
}
|
||||
|
||||
function without(builderQuery: object, key: keyof IBuilderQuery): object {
|
||||
@@ -62,9 +60,11 @@ describe('normalizeCompositeQuery', () => {
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
extra: 'kept',
|
||||
};
|
||||
const aiQuery = withBuilderQueries(initialQueriesMap.traces, [
|
||||
{ ...tracesQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER },
|
||||
]);
|
||||
const aiQuery: Query = {
|
||||
...initialQueriesMap.traces,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: initialQueriesMap.traces.builder,
|
||||
};
|
||||
|
||||
[NOT_AN_EXPLORER, ROUTES.LOGS_EXPLORER].forEach((pathname) =>
|
||||
expect(
|
||||
@@ -86,6 +86,33 @@ describe('normalizeCompositeQuery', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('moves the AI queries of an older link into the AI slot', () => {
|
||||
const aiQueryData = [
|
||||
{ ...tracesQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER },
|
||||
];
|
||||
const { query, changed } = normalizeCompositeQuery(
|
||||
withBuilderQueries(initialQueriesMap.traces, aiQueryData),
|
||||
ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
);
|
||||
|
||||
expect(changed).toBe(true);
|
||||
expect(query.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(query.builder_ai_query?.queryData).toStrictEqual([tracesQuery]);
|
||||
expect(query.builder).toStrictEqual(initialQueryState.builder);
|
||||
});
|
||||
|
||||
it('drops the AI slot outside the AI explorer', () => {
|
||||
const { query } = normalizeCompositeQuery(
|
||||
{
|
||||
...initialQueriesMap.traces,
|
||||
builder_ai_query: initialQueriesMap.traces.builder,
|
||||
},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
|
||||
expect(query.builder_ai_query).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds missing blocks and a stable id without changing its inputs', () => {
|
||||
const { builder: _, ...builderless } = {
|
||||
...initialQueriesMap.logs,
|
||||
@@ -209,7 +236,7 @@ describe('normalizeCompositeQuery', () => {
|
||||
expect(
|
||||
firstBuilderQuery(
|
||||
logs,
|
||||
[{ ...logsQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER }],
|
||||
[{ ...logsQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER }],
|
||||
ROUTES.LOGS_EXPLORER,
|
||||
).builderQueryType,
|
||||
).toBeUndefined();
|
||||
@@ -220,7 +247,7 @@ describe('normalizeCompositeQuery', () => {
|
||||
{
|
||||
...tracesQuery,
|
||||
queryName: 'D',
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
builderQueryType: EQueryType.AI_QUERY_BUILDER,
|
||||
...llmFilter,
|
||||
},
|
||||
],
|
||||
@@ -243,10 +270,7 @@ describe('normalizeCompositeQuery', () => {
|
||||
).toBe(TelemetrytypesSourceDTO.meter);
|
||||
expect(
|
||||
firstBuilderQuery(traces, [{ ...tracesQuery, ...llmFilter }], ai),
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
...llmFilter,
|
||||
});
|
||||
).toStrictEqual({ ...tracesQuery, ...llmFilter });
|
||||
expect(
|
||||
firstBuilderQuery(
|
||||
traces,
|
||||
@@ -260,17 +284,13 @@ describe('normalizeCompositeQuery', () => {
|
||||
],
|
||||
ai,
|
||||
),
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
queryName: 'B',
|
||||
filter: { expression: '' },
|
||||
});
|
||||
).toStrictEqual({ ...tracesQuery, queryName: 'B', ...llmFilter });
|
||||
expect(
|
||||
firstBuilderQuery(traces, [{ ...logsQuery, queryName: 'C' }], ai),
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'C',
|
||||
filter: { expression: '' },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
Querybuildertypesv5QueryTypeDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import type { QueryBuilderData } from 'types/common/queryBuilder';
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
|
||||
import {
|
||||
@@ -78,6 +80,14 @@ function addMissingBlocks(query: Query, urlQuery: object): void {
|
||||
if (!query.builder.queryTraceOperator) {
|
||||
query.builder.queryTraceOperator = [];
|
||||
}
|
||||
|
||||
if (isPlainObject(query.builder_ai_query)) {
|
||||
const aiBuilder = query.builder_ai_query as QueryBuilderData;
|
||||
aiBuilder.queryFormulas = aiBuilder.queryFormulas ?? [];
|
||||
aiBuilder.queryTraceOperator = aiBuilder.queryTraceOperator ?? [];
|
||||
} else {
|
||||
delete query.builder_ai_query;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +103,21 @@ function isListOfObjects(value: unknown): boolean {
|
||||
}
|
||||
|
||||
function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
|
||||
// Explorers only have the query builder: no PromQL or ClickHouse editor.
|
||||
query.queryType = EQueryType.QUERY_BUILDER;
|
||||
// Explorers only have a query builder: no PromQL or ClickHouse editor.
|
||||
query.queryType = rules.queryType;
|
||||
if (rules.queryType === EQueryType.AI_QUERY_BUILDER) {
|
||||
// Older links carry the AI queries inside `builder`.
|
||||
if (!query.builder_ai_query) {
|
||||
query.builder_ai_query = query.builder;
|
||||
query.builder = cloneDeep(initialQueryState.builder);
|
||||
}
|
||||
} else {
|
||||
delete query.builder_ai_query;
|
||||
}
|
||||
const builder = getActiveBuilder(query);
|
||||
if (!isListOfObjects(builder.queryData) || builder.queryData.length === 0) {
|
||||
builder.queryData = cloneDeep(initialQueryState.builder.queryData);
|
||||
}
|
||||
|
||||
if (query.promql.some((promql) => promql.query !== '')) {
|
||||
query.promql = cloneDeep(initialQueryState.promql);
|
||||
@@ -105,10 +128,10 @@ function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
|
||||
}
|
||||
|
||||
if (!rules.allowsTraceOperator) {
|
||||
query.builder.queryTraceOperator = [];
|
||||
builder.queryTraceOperator = [];
|
||||
}
|
||||
|
||||
query.builder.queryData = query.builder.queryData.map((builderQuery) =>
|
||||
builder.queryData = builder.queryData.map((builderQuery) =>
|
||||
fitBuilderQueryToExplorer(builderQuery, rules),
|
||||
);
|
||||
}
|
||||
@@ -148,12 +171,9 @@ function fillMissingSignal(
|
||||
builderQuery.source = rules.source;
|
||||
}
|
||||
|
||||
// A missing `builderQueryType` already means builder_query, so only AI needs it written.
|
||||
if (
|
||||
builderQuery.builderQueryType === undefined &&
|
||||
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
) {
|
||||
builderQuery.builderQueryType = rules.builderQueryType;
|
||||
// The AI slot marks its queries as AI; a tag from an older link is redundant.
|
||||
if (rules.queryType === EQueryType.AI_QUERY_BUILDER) {
|
||||
delete builderQuery.builderQueryType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,15 +189,11 @@ function belongsToExplorer(
|
||||
return false;
|
||||
}
|
||||
|
||||
const builderQueryType =
|
||||
builderQuery.builderQueryType ??
|
||||
Querybuildertypesv5QueryTypeDTO.builder_query;
|
||||
|
||||
if (builderQueryType !== rules.builderQueryType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
// Only the AI explorer runs AI queries.
|
||||
return (
|
||||
builderQuery.builderQueryType !==
|
||||
Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
|
||||
@@ -186,11 +202,5 @@ function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
|
||||
);
|
||||
builderQuery.source = rules.source;
|
||||
|
||||
if (
|
||||
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
) {
|
||||
builderQuery.builderQueryType = rules.builderQueryType;
|
||||
}
|
||||
|
||||
return builderQuery;
|
||||
}
|
||||
|
||||
18
frontend/src/hooks/queryBuilder/useBuilderQueryType.ts
Normal file
18
frontend/src/hooks/queryBuilder/useBuilderQueryType.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
/**
|
||||
* The query's builder type, for picking the field suggestion endpoints. Alert editors
|
||||
* keep AI queries in `builder`, so a query's own tag still counts.
|
||||
*/
|
||||
export function useBuilderQueryType(
|
||||
query: Pick<IBuilderQuery, 'builderQueryType'>,
|
||||
): BuilderQueryType | undefined {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
return currentQuery?.queryType === EQueryType.AI_QUERY_BUILDER
|
||||
? 'builder_ai_query'
|
||||
: query.builderQueryType;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
saveAsPreviousQuery,
|
||||
} from 'components/QueryBuilderV2/QueryV2/previousQuery.utils';
|
||||
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { LEGEND } from 'constants/global';
|
||||
import {
|
||||
ATTRIBUTE_TYPES,
|
||||
@@ -488,13 +489,15 @@ export const useQueryOperations: UseQueryOperations = ({
|
||||
],
|
||||
);
|
||||
|
||||
const activeQueryCount = getActiveBuilder(currentQuery).queryData.length;
|
||||
|
||||
const handleDeleteQuery = useCallback(() => {
|
||||
if (currentQuery.builder.queryData.length > 1) {
|
||||
if (activeQueryCount > 1) {
|
||||
removeQueryBuilderEntityByIndex('queryData', index);
|
||||
}
|
||||
setLastUsedQuery(0);
|
||||
}, [
|
||||
currentQuery.builder.queryData.length,
|
||||
activeQueryCount,
|
||||
setLastUsedQuery,
|
||||
removeQueryBuilderEntityByIndex,
|
||||
index,
|
||||
|
||||
@@ -67,6 +67,23 @@ describe('useGetQueryLabels', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI_QUERY_BUILDER type', () => {
|
||||
it('returns formula labels from builder_ai_query', () => {
|
||||
const query = buildQuery({
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: {
|
||||
queryData: [],
|
||||
queryFormulas: [{ queryName: 'F1' } as unknown as IBuilderFormula],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGetQueryLabels(query));
|
||||
|
||||
expect(result.current).toStrictEqual([{ label: 'F1', value: 'F1' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLICKHOUSE type', () => {
|
||||
it('returns empty array when clickhouse_sql is undefined', () => {
|
||||
const query = buildQuery({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getQueryLabelWithAggregation } from 'components/QueryBuilderV2/utils';
|
||||
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
@@ -7,11 +8,12 @@ export const useGetQueryLabels = (
|
||||
currentQuery: Query,
|
||||
): { label: string; value: string }[] =>
|
||||
useMemo(() => {
|
||||
if (currentQuery?.queryType === EQueryType.QUERY_BUILDER) {
|
||||
if (currentQuery && isBuilderQueryType(currentQuery.queryType)) {
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
const queryLabels = getQueryLabelWithAggregation(
|
||||
currentQuery?.builder?.queryData || [],
|
||||
activeBuilder?.queryData || [],
|
||||
);
|
||||
const formulaLabels = (currentQuery?.builder?.queryFormulas ?? []).map(
|
||||
const formulaLabels = (activeBuilder?.queryFormulas ?? []).map(
|
||||
(formula) => ({
|
||||
label: formula.queryName,
|
||||
value: formula.queryName,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
QueryRangeResponseV5,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { prepareQueryRangePayload } from './prepareQueryRangePayload';
|
||||
@@ -36,11 +36,11 @@ import { prepareQueryRangePayload } from './prepareQueryRangePayload';
|
||||
* Validates if metric name is available for METRICS data source
|
||||
*/
|
||||
function validateMetricNameForMetricsDataSource(query: Query): boolean {
|
||||
if (query.queryType !== 'builder') {
|
||||
if (!isBuilderQueryType(query.queryType)) {
|
||||
return true; // Non-builder queries don't need this validation
|
||||
}
|
||||
|
||||
const { queryData } = query.builder;
|
||||
const { queryData } = getActiveBuilder(query);
|
||||
|
||||
// Check if any METRICS data source queries exist
|
||||
const metricsQueries = queryData.filter(
|
||||
@@ -124,14 +124,15 @@ export const getLegend = (
|
||||
labelName: string,
|
||||
): string => {
|
||||
// For non-query builder queries, return the label name directly
|
||||
if (payloadQuery.queryType !== EQueryType.QUERY_BUILDER) {
|
||||
if (!isBuilderQueryType(payloadQuery.queryType)) {
|
||||
return labelName;
|
||||
}
|
||||
|
||||
const activeBuilder = getActiveBuilder(payloadQuery);
|
||||
// Combine queryData and queryTraceOperator
|
||||
const allQueries = [
|
||||
...(payloadQuery?.builder?.queryData || []),
|
||||
...(payloadQuery?.builder?.queryTraceOperator || []),
|
||||
...(activeBuilder?.queryData || []),
|
||||
...(activeBuilder?.queryTraceOperator || []),
|
||||
];
|
||||
|
||||
const aggregationPerQuery = allQueries.reduce((acc, query) => {
|
||||
|
||||
@@ -161,6 +161,35 @@ describe('extractQueryTextStrings', () => {
|
||||
expect(texts).toStrictEqual(['env = $deployment_environment']);
|
||||
});
|
||||
|
||||
it('extracts from the AI slot, not builder, for an AI query', () => {
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filter: { expression: 'env = $ignored' },
|
||||
} as unknown as IBuilderQuery,
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
builder_ai_query: {
|
||||
queryData: [
|
||||
{
|
||||
filter: { expression: 'service.name = $service' },
|
||||
} as unknown as IBuilderQuery,
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
};
|
||||
|
||||
expect(extractQueryTextStrings(query)).toStrictEqual([
|
||||
'service.name = $service',
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips non-string filter values', () => {
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { escapeRegExp, isArray } from 'lodash-es';
|
||||
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import type { QueryBuilderData } from 'types/common/queryBuilder';
|
||||
|
||||
/**
|
||||
* Builds a RegExp that matches any recognized variable reference syntax:
|
||||
@@ -50,9 +52,11 @@ export function containsAnyVariableReference(text: string): boolean {
|
||||
return !!text && ANY_VARIABLE_REFERENCE.test(text);
|
||||
}
|
||||
|
||||
function extractQueryBuilderTexts(query: Query): string[] {
|
||||
function extractQueryBuilderTexts(
|
||||
builder: QueryBuilderData | undefined,
|
||||
): string[] {
|
||||
const texts: string[] = [];
|
||||
const queryDataList = query.builder?.queryData;
|
||||
const queryDataList = builder?.queryData;
|
||||
if (isArray(queryDataList)) {
|
||||
queryDataList.forEach((queryData) => {
|
||||
queryData.filters?.items?.forEach((filter: TagFilterItem) => {
|
||||
@@ -102,13 +106,13 @@ function extractClickhouseSQLTexts(query: Query): string[] {
|
||||
/**
|
||||
* Extracts all text strings from a widget Query that could contain variable
|
||||
* references. Covers:
|
||||
* - QUERY_BUILDER: filter items' string values + filter.expression
|
||||
* - QUERY_BUILDER / AI_QUERY_BUILDER: filter items' string values + filter.expression
|
||||
* - PROM: each promql[].query
|
||||
* - CLICKHOUSE: each clickhouse_sql[].query
|
||||
*/
|
||||
export function extractQueryTextStrings(query: Query): string[] {
|
||||
if (query.queryType === EQueryType.QUERY_BUILDER) {
|
||||
return extractQueryBuilderTexts(query);
|
||||
if (isBuilderQueryType(query.queryType)) {
|
||||
return extractQueryBuilderTexts(getActiveBuilder(query));
|
||||
}
|
||||
|
||||
if (query.queryType === EQueryType.PROM) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { mapCompositeQueryFromQuery } from '../mapCompositeQueryFromQuery';
|
||||
|
||||
const emptyBuilder = {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
|
||||
describe('mapCompositeQueryFromQuery', () => {
|
||||
it('sends an AI query as builder_ai_query envelopes under the builder query type', () => {
|
||||
const query: Query = {
|
||||
id: 'q',
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
...emptyBuilder,
|
||||
queryData: [
|
||||
{ ...initialQueryBuilderFormValuesMap.metrics, queryName: 'M' },
|
||||
],
|
||||
},
|
||||
builder_ai_query: {
|
||||
...emptyBuilder,
|
||||
queryData: [{ ...initialQueryBuilderFormValuesMap.traces, queryName: 'A' }],
|
||||
},
|
||||
};
|
||||
|
||||
const composite = mapCompositeQueryFromQuery(query, PANEL_TYPES.TIME_SERIES);
|
||||
|
||||
expect(composite.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(
|
||||
composite.queries?.map((q) => [q.type, (q.spec as { name: string }).name]),
|
||||
).toStrictEqual([['builder_ai_query', 'A']]);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { toWireQuery } from 'lib/query/builderSlot';
|
||||
import {
|
||||
BuilderClickHouseResource,
|
||||
BuilderPromQLResource,
|
||||
@@ -91,8 +92,13 @@ const buildPromQuery = (
|
||||
return compositeQuery;
|
||||
};
|
||||
|
||||
const queryTypeMethodMapping = {
|
||||
const queryTypeMethodMapping: Record<
|
||||
EQueryType,
|
||||
(query: Query, panelType: PANEL_TYPES | null) => ICompositeMetricQuery
|
||||
> = {
|
||||
[EQueryType.QUERY_BUILDER]: buildBuilderQuery,
|
||||
[EQueryType.AI_QUERY_BUILDER]: (query, panelType) =>
|
||||
buildBuilderQuery(toWireQuery(query), panelType),
|
||||
[EQueryType.CLICKHOUSE]: buildClickHouseQuery,
|
||||
[EQueryType.PROM]: buildPromQuery,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
PromQuery,
|
||||
QueryBuilderFormula,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
convertBuilderQueryToIBuilderQuery,
|
||||
convertQueryBuilderFormulaToIBuilderFormula,
|
||||
@@ -37,6 +38,7 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
> = {};
|
||||
const promQueries: IPromQLQuery[] = [];
|
||||
const clickhouseQueries: IClickHouseQuery[] = [];
|
||||
const isAIQuery = compositeQuery.queryType === EQueryType.AI_QUERY_BUILDER;
|
||||
|
||||
compositeQuery.queries?.forEach((q) => {
|
||||
const spec = q.spec as BuilderQuery | PromQuery | ClickHouseQuery;
|
||||
@@ -49,10 +51,13 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
}
|
||||
} else if (q.type === 'builder_ai_query') {
|
||||
if (spec.name) {
|
||||
builderQueries[spec.name] = {
|
||||
...convertBuilderQueryToIBuilderQuery(spec as BuilderQuery),
|
||||
builderQueryType: 'builder_ai_query',
|
||||
};
|
||||
const builderQuery = convertBuilderQueryToIBuilderQuery(
|
||||
spec as BuilderQuery,
|
||||
);
|
||||
// Only AI queries kept in `builder` (alerts) need the tag; the AI slot marks its own.
|
||||
builderQueries[spec.name] = isAIQuery
|
||||
? builderQuery
|
||||
: { ...builderQuery, builderQueryType: 'builder_ai_query' };
|
||||
builderQueryTypes[spec.name] = 'builder_ai_query';
|
||||
}
|
||||
} else if (q.type === 'builder_formula') {
|
||||
@@ -87,8 +92,14 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const builder = transformQueryBuilderDataModel(
|
||||
builderQueries,
|
||||
builderQueryTypes,
|
||||
);
|
||||
return {
|
||||
builder: transformQueryBuilderDataModel(builderQueries, builderQueryTypes),
|
||||
builder: isAIQuery ? transformQueryBuilderDataModel({}) : builder,
|
||||
...(isAIQuery && { builder_ai_query: builder }),
|
||||
promql: promQueries,
|
||||
clickhouse_sql: clickhouseQueries,
|
||||
queryType: compositeQuery.queryType,
|
||||
|
||||
93
frontend/src/lib/query/__tests__/builderSlot.test.ts
Normal file
93
frontend/src/lib/query/__tests__/builderSlot.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
} from 'constants/queryBuilder';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderSlotKey,
|
||||
isBuilderQueryType,
|
||||
toWireQuery,
|
||||
} from 'lib/query/builderSlot';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
const baseQuery = initialQueriesMap[DataSource.METRICS];
|
||||
const aiBuilder = getActiveBuilder(initialQueryAIWithType);
|
||||
|
||||
describe('isBuilderQueryType', () => {
|
||||
it.each([
|
||||
[EQueryType.QUERY_BUILDER, true],
|
||||
[EQueryType.AI_QUERY_BUILDER, true],
|
||||
[EQueryType.CLICKHOUSE, false],
|
||||
[EQueryType.PROM, false],
|
||||
])('%s → %s', (queryType, expected) => {
|
||||
expect(isBuilderQueryType(queryType)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBuilderSlotKey', () => {
|
||||
it.each([
|
||||
[EQueryType.QUERY_BUILDER, 'builder'],
|
||||
[EQueryType.CLICKHOUSE, 'builder'],
|
||||
[EQueryType.PROM, 'builder'],
|
||||
[EQueryType.AI_QUERY_BUILDER, 'builder_ai_query'],
|
||||
])('maps %s to %s', (queryType, slot) => {
|
||||
expect(getBuilderSlotKey(queryType)).toBe(slot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveBuilder', () => {
|
||||
it('reads builder for the Query Builder type', () => {
|
||||
const query: Query = { ...baseQuery, builder_ai_query: aiBuilder };
|
||||
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
|
||||
});
|
||||
|
||||
it('reads the AI slot for the AI type', () => {
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: aiBuilder,
|
||||
};
|
||||
expect(getActiveBuilder(query)).toBe(aiBuilder);
|
||||
});
|
||||
|
||||
it('reads an unused AI slot as holding no queries', () => {
|
||||
const query: Query = { ...baseQuery, queryType: EQueryType.AI_QUERY_BUILDER };
|
||||
expect(getActiveBuilder(query)).toStrictEqual({
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps builder for ClickHouse and PromQL', () => {
|
||||
const query: Query = { ...baseQuery, queryType: EQueryType.PROM };
|
||||
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toWireQuery', () => {
|
||||
it('passes non-AI queries through untouched', () => {
|
||||
expect(toWireQuery(baseQuery)).toBe(baseQuery);
|
||||
});
|
||||
|
||||
it('sends AI as a builder query holding the AI slot, every query tagged', () => {
|
||||
const untagged = {
|
||||
...aiBuilder.queryData[0],
|
||||
builderQueryType: undefined,
|
||||
};
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: { ...aiBuilder, queryData: [untagged] },
|
||||
};
|
||||
|
||||
const wire = toWireQuery(query);
|
||||
|
||||
expect(wire.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(wire.builder.queryData).toStrictEqual([
|
||||
{ ...untagged, builderQueryType: 'builder_ai_query' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
73
frontend/src/lib/query/builderSlot.ts
Normal file
73
frontend/src/lib/query/builderSlot.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type {
|
||||
Query,
|
||||
QueryState,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import type { QueryBuilderData } from 'types/common/queryBuilder';
|
||||
|
||||
export type BuilderSlotKey = 'builder' | 'builder_ai_query';
|
||||
|
||||
const EMPTY_BUILDER: QueryBuilderData = {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
|
||||
/** Query Builder and AI Query Builder; both author builder queries, each in its own slot. */
|
||||
export function isBuilderQueryType(queryType: EQueryType | undefined): boolean {
|
||||
return (
|
||||
queryType === EQueryType.QUERY_BUILDER ||
|
||||
queryType === EQueryType.AI_QUERY_BUILDER
|
||||
);
|
||||
}
|
||||
|
||||
/** The `Query` slot builder edits land in; non-builder types keep `builder`, as they always have. */
|
||||
export function getBuilderSlotKey(queryType: EQueryType): BuilderSlotKey {
|
||||
return queryType === EQueryType.AI_QUERY_BUILDER
|
||||
? 'builder_ai_query'
|
||||
: 'builder';
|
||||
}
|
||||
|
||||
/**
|
||||
* The query as the backend takes it. AI goes out as a `builder` query whose queries carry
|
||||
* `builderQueryType: builder_ai_query`, the tag set from the slot so a stale one can't leak.
|
||||
*/
|
||||
export function toWireQuery(query: Query): Query {
|
||||
if (query.queryType !== EQueryType.AI_QUERY_BUILDER) {
|
||||
return query;
|
||||
}
|
||||
const aiBuilder = getActiveBuilder(query);
|
||||
return {
|
||||
...query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: {
|
||||
...aiBuilder,
|
||||
queryData: aiBuilder.queryData.map((queryData) => ({
|
||||
...queryData,
|
||||
builderQueryType: 'builder_ai_query',
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** The builder at `slot`; an unused AI slot holds none. */
|
||||
export function getBuilderAt(
|
||||
state: QueryState,
|
||||
slot: BuilderSlotKey,
|
||||
): QueryBuilderData {
|
||||
return state[slot] ?? EMPTY_BUILDER;
|
||||
}
|
||||
|
||||
/** `state` with the builder at `slot` replaced. */
|
||||
export function withBuilderAt<T extends QueryState>(
|
||||
state: T,
|
||||
slot: BuilderSlotKey,
|
||||
builder: QueryBuilderData,
|
||||
): T {
|
||||
return { ...state, [slot]: builder };
|
||||
}
|
||||
|
||||
/** The builder queries `queryType` authors; an unused AI slot holds none. */
|
||||
export function getActiveBuilder(query: Query): QueryBuilderData {
|
||||
return getBuilderAt(query, getBuilderSlotKey(query.queryType));
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { FORMULA_REGEXP } from 'constants/regExp';
|
||||
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
|
||||
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
|
||||
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { get, isEqual, isNaN, isObject } from 'lodash-es';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
@@ -93,8 +94,8 @@ const getQueryByName = <T extends keyof QueryBuilderData>(
|
||||
queryArray.find((q) => q.name === currentQueryName) || defaultQueryValue
|
||||
);
|
||||
}
|
||||
if (query.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const queryArray = (query.builder[type] || []) as (
|
||||
if (isBuilderQueryType(query.queryType)) {
|
||||
const queryArray = (getActiveBuilder(query)[type] || []) as (
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
)[];
|
||||
@@ -173,7 +174,7 @@ const addOperatorFormulaColumns = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (queryType === EQueryType.QUERY_BUILDER) {
|
||||
if (isBuilderQueryType(queryType)) {
|
||||
const currentQueryData = query as IBuilderQuery;
|
||||
let operatorLabel = `${currentQueryData.aggregateOperator}`;
|
||||
if (currentQueryData.aggregateAttribute?.key) {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
BuilderVariant,
|
||||
type Query,
|
||||
QueryMode,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
/** Which variant the `builder` slot holds, whatever query type is active. */
|
||||
export function getBuilderVariant(builder: Query['builder']): BuilderVariant {
|
||||
return builder.queryData[0]?.builderQueryType === QueryMode.AI_QUERY_BUILDER
|
||||
? BuilderVariant.AI
|
||||
: BuilderVariant.MAIN;
|
||||
}
|
||||
|
||||
/** The variant a tab authors; `undefined` for tabs that own their own slot. */
|
||||
export function toBuilderVariant(mode: QueryMode): BuilderVariant | undefined {
|
||||
if (mode === QueryMode.AI_QUERY_BUILDER) {
|
||||
return BuilderVariant.AI;
|
||||
}
|
||||
return mode === QueryMode.QUERY_BUILDER ? BuilderVariant.MAIN : undefined;
|
||||
}
|
||||
|
||||
/** The tab a query belongs to: its query type, or AI when a builder query carries the tag. */
|
||||
export function getQueryMode(query: Query): QueryMode {
|
||||
if (query.queryType !== QueryMode.QUERY_BUILDER) {
|
||||
return query.queryType;
|
||||
}
|
||||
return getBuilderVariant(query.builder) === BuilderVariant.AI
|
||||
? QueryMode.AI_QUERY_BUILDER
|
||||
: QueryMode.QUERY_BUILDER;
|
||||
}
|
||||
|
||||
/** Query Builder and AI both author `Query.builder`; the other modes own their own slot. */
|
||||
export function isBuilderMode(mode: QueryMode): boolean {
|
||||
return toBuilderVariant(mode) !== undefined;
|
||||
}
|
||||
|
||||
/** Drops the AI tag, leaving the same query readable as a plain builder query. */
|
||||
export function withoutAIQueryTag(query: Query): Query {
|
||||
if (getBuilderVariant(query.builder) !== BuilderVariant.AI) {
|
||||
return query;
|
||||
}
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: query.builder.queryData.map(
|
||||
({ builderQueryType: _tag, ...queryData }) => queryData,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getGetRuleByIDQueryKey,
|
||||
getGetRuleHistoryTimelineQueryOptions,
|
||||
invalidateGetRuleByID,
|
||||
invalidateListRulesV3,
|
||||
invalidateListRules,
|
||||
updateRuleByID,
|
||||
useGetRuleByID,
|
||||
useGetRuleHistoryOverallStatus,
|
||||
@@ -443,7 +443,7 @@ export const useAlertRuleClone = ({
|
||||
message: `Success`,
|
||||
});
|
||||
|
||||
await invalidateListRulesV3(queryClient);
|
||||
await invalidateListRules(queryClient);
|
||||
|
||||
params.set(QueryParams.ruleId, response.data.id);
|
||||
navigate(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
@@ -494,7 +494,7 @@ export const useAlertRuleUpdate = ({
|
||||
data: { ...existingRule.data, alert: intermediateName },
|
||||
});
|
||||
}
|
||||
void invalidateListRulesV3(queryClient);
|
||||
void invalidateListRules(queryClient);
|
||||
notifications.success({ message: 'Alert renamed successfully' });
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
FIRST_DOWNTIME_NAME,
|
||||
} from './__story_mockdata__/plannedDowntime';
|
||||
|
||||
import { alertRulesPage } from '../../AlertRules/stories/__story_mockdata__/ruleViews';
|
||||
import { RULE_MAX } from '../../stories/__story_mockdata__/alerts';
|
||||
import {
|
||||
alertRulesResponse,
|
||||
RULE_MAX,
|
||||
} from '../../stories/__story_mockdata__/alerts';
|
||||
import { AlertListSubTabs, AlertListTabs } from '../../types';
|
||||
|
||||
const LIST = 'Planned downtime · list';
|
||||
@@ -84,20 +86,18 @@ export const plannedDowntimeMocks = defineStoryMocks({
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: null })),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v3/rules', (req, res, ctx) => {
|
||||
if (values.rulesState === 'error') {
|
||||
return res(ctx.status(500), ctx.json({ status: 'error' }));
|
||||
}
|
||||
return res(
|
||||
ctx.json(
|
||||
alertRulesPage(
|
||||
values.silencedRules,
|
||||
{ severity: 'mixed', state: 'mixed' },
|
||||
req.url.searchParams,
|
||||
rest.get('http://localhost/api/v2/rules', (_req, res, ctx) =>
|
||||
values.rulesState === 'error'
|
||||
? res(ctx.status(500), ctx.json({ status: 'error' }))
|
||||
: res(
|
||||
ctx.json(
|
||||
alertRulesResponse(values.silencedRules, {
|
||||
severity: 'mixed',
|
||||
state: 'mixed',
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
config: () => ({
|
||||
route: `/alerts?tab=${AlertListTabs.CONFIGURATION}&subTab=${AlertListSubTabs.PLANNED_DOWNTIME}`,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { Flame } from '@signozhq/icons';
|
||||
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getQueryMode } from 'lib/query/queryMode';
|
||||
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
|
||||
import { useCreateAlertFromPanel } from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/hooks/useCreateAlertFromPanel';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import ConfigActionRow from './ConfigActionRow';
|
||||
import styles from './ConfigActions.module.scss';
|
||||
@@ -33,7 +32,7 @@ function ConfigActions({
|
||||
//TODO: AI query check should be removed after we support alerts on ai query @tewarig
|
||||
if (
|
||||
!actions.createAlert ||
|
||||
getQueryMode(currentQuery) === QueryMode.AI_QUERY_BUILDER
|
||||
currentQuery.queryType === EQueryType.AI_QUERY_BUILDER
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('PanelTypeSwitcher', () => {
|
||||
return {
|
||||
...OPTIONS.find((option) => option.kind === kind),
|
||||
mode: kind === 'signoz/TextPanel' ? 'static' : 'query',
|
||||
supportedQueryModesKindMap: Object.fromEntries(
|
||||
supportedQueryTypesKindMap: Object.fromEntries(
|
||||
queryTypes.map((queryType) => [
|
||||
queryType,
|
||||
queryType === EQueryType.QUERY_BUILDER
|
||||
|
||||
@@ -25,6 +25,25 @@ describe('getPanelTypeDisabledReason', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('offers the AI kinds to an AI query and explains the rest', () => {
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/TimeSeriesPanel',
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
signal: TelemetrytypesSignalDTO.traces,
|
||||
label: 'Time Series',
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
kind: 'signoz/HeatmapPanel',
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
signal: TelemetrytypesSignalDTO.traces,
|
||||
label: 'Heatmap',
|
||||
}),
|
||||
).toBe("Heatmap isn't available for AI Query Builder queries");
|
||||
});
|
||||
|
||||
it('explains an unsupported query type', () => {
|
||||
expect(
|
||||
getPanelTypeDisabledReason({
|
||||
|
||||
@@ -12,6 +12,7 @@ const QUERY_TYPE_LABEL: Record<EQueryType, string> = {
|
||||
[EQueryType.QUERY_BUILDER]: 'Query Builder',
|
||||
[EQueryType.CLICKHOUSE]: 'ClickHouse',
|
||||
[EQueryType.PROM]: 'PromQL',
|
||||
[EQueryType.AI_QUERY_BUILDER]: 'AI Query Builder',
|
||||
};
|
||||
|
||||
const SIGNAL_LABEL: Record<TelemetrytypesSignalDTO, string> = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { useDashboardVariableNames } from 'pages/DashboardPage/DashboardContainer/hooks/useDashboardVariableNames';
|
||||
|
||||
import type { VariableItem } from './types';
|
||||
@@ -20,13 +21,16 @@ const GLOBAL_TIMESTAMP_VARIABLES: VariableItem[] = [
|
||||
*/
|
||||
export function useContextLinkVariables(): VariableItem[] {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
|
||||
const dashboardVariableNames = useDashboardVariableNames();
|
||||
|
||||
// `_`-prefixed to match V1 and avoid colliding with dashboard-variable names.
|
||||
const fieldVariableNames = useMemo(() => {
|
||||
const names = new Set<string>();
|
||||
(currentQuery?.builder?.queryData ?? []).forEach((query) => {
|
||||
(activeQueryData ?? []).forEach((query) => {
|
||||
(query.groupBy ?? []).forEach((field) => {
|
||||
if (field.key) {
|
||||
names.add(`_${field.key}`);
|
||||
@@ -34,7 +38,7 @@ export function useContextLinkVariables(): VariableItem[] {
|
||||
});
|
||||
});
|
||||
return Array.from(names);
|
||||
}, [currentQuery?.builder?.queryData]);
|
||||
}, [activeQueryData]);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
|
||||
@@ -14,7 +14,7 @@ jest.mock('pages/DashboardPage/DashboardContainer/Panels/registry', () => ({
|
||||
displayName: 'Time Series',
|
||||
icon: (): null => null,
|
||||
mode: 'query',
|
||||
supportedQueryModesKindMap: {
|
||||
supportedQueryTypesKindMap: {
|
||||
builder: { kind: 'signal', signals: ['metrics', 'logs', 'traces'] },
|
||||
clickhouse_sql: { kind: 'signal-less' },
|
||||
promql: { kind: 'signal-less' },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
type ReactNode,
|
||||
@@ -23,22 +24,20 @@ import { useSyncQueryBuilderFields } from 'components/QueryBuilderV2/useSyncQuer
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
getQueryModeSignals,
|
||||
getQueryTypeSignals,
|
||||
isRawRequest,
|
||||
} from '../../Panels/types/panelCapabilities';
|
||||
import type { RenderableQueryPanelDefinition } from '../../Panels/types/panelDefinition';
|
||||
import { toPanelType } from '../../Panels/types/panelKind';
|
||||
import { getQueryMode, isBuilderMode } from 'lib/query/queryMode';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
|
||||
import { useQueryTypeChange } from './useQueryTypeChange';
|
||||
import styles from './PanelEditorQueryBuilder.module.scss';
|
||||
|
||||
interface PanelEditorQueryBuilderProps {
|
||||
/** The edited kind's definition — drives supported query types, the signals the
|
||||
* builder may be pointed at, and how it narrows the builder's fields. */
|
||||
panelDefinition: RenderableQueryPanelDefinition;
|
||||
/** Switch authoring tab; owned by the host, which remembers the Query Builder query. */
|
||||
onChangeQueryMode: (key: string) => void;
|
||||
/** Preview fetch in flight — drives the Stage & Run button's loading/cancel state. */
|
||||
isLoadingQueries: boolean;
|
||||
/** Run the current query (Stage & Run button / ⌘↵). Always re-runs. */
|
||||
@@ -58,7 +57,6 @@ interface PanelEditorQueryBuilderProps {
|
||||
*/
|
||||
function PanelEditorQueryBuilder({
|
||||
panelDefinition,
|
||||
onChangeQueryMode,
|
||||
isLoadingQueries,
|
||||
onStageRunQuery,
|
||||
onCancelQuery,
|
||||
@@ -68,6 +66,10 @@ function PanelEditorQueryBuilder({
|
||||
// The shared QueryBuilderV2 provider still speaks the legacy PANEL_TYPES; what the
|
||||
// builder offers for this kind comes from the kind's own declaration.
|
||||
const panelType = toPanelType(panelDefinition.kind);
|
||||
const onChangeQueryType = useQueryTypeChange({
|
||||
panelType,
|
||||
supportedQueryTypesKindMap: panelDefinition.supportedQueryTypesKindMap,
|
||||
});
|
||||
// Raw rows: the builder drops its aggregation controls, and with them the trace
|
||||
// operator that combines aggregated trace queries (V1 parity).
|
||||
const isRawQuery = isRawRequest(panelDefinition.queryCapabilities);
|
||||
@@ -99,10 +101,10 @@ function PanelEditorQueryBuilder({
|
||||
|
||||
const items = useMemo(() => {
|
||||
const queryTypeComponents: Record<
|
||||
QueryMode,
|
||||
EQueryType,
|
||||
{ icon: ReactNode; label: string; component: ReactNode }
|
||||
> = {
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
icon: <Atom size={14} />,
|
||||
label: 'Query Builder',
|
||||
component: (
|
||||
@@ -110,9 +112,9 @@ function PanelEditorQueryBuilder({
|
||||
<QueryBuilderV2
|
||||
panelType={panelType}
|
||||
fieldsConfig={panelDefinition.queryBuilderFields}
|
||||
allowedDataSources={getQueryModeSignals(
|
||||
panelDefinition.supportedQueryModesKindMap,
|
||||
QueryMode.QUERY_BUILDER,
|
||||
allowedDataSources={getQueryTypeSignals(
|
||||
panelDefinition.supportedQueryTypesKindMap,
|
||||
EQueryType.QUERY_BUILDER,
|
||||
)}
|
||||
showTraceOperator={!isRawQuery}
|
||||
version="v3"
|
||||
@@ -123,12 +125,12 @@ function PanelEditorQueryBuilder({
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[QueryMode.CLICKHOUSE]: {
|
||||
[EQueryType.CLICKHOUSE]: {
|
||||
icon: <Terminal size={14} />,
|
||||
label: 'ClickHouse Query',
|
||||
component: <ClickHouseQueryContainer />,
|
||||
},
|
||||
[QueryMode.PROM]: {
|
||||
[EQueryType.PROM]: {
|
||||
icon: (
|
||||
<PromQLIcon
|
||||
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||
@@ -137,7 +139,7 @@ function PanelEditorQueryBuilder({
|
||||
label: 'PromQL',
|
||||
component: <PromQLQueryContainer />,
|
||||
},
|
||||
[QueryMode.AI_QUERY_BUILDER]: {
|
||||
[EQueryType.AI_QUERY_BUILDER]: {
|
||||
icon: <Brain size={14} />,
|
||||
label: 'AI Query Builder',
|
||||
component: (
|
||||
@@ -160,8 +162,8 @@ function PanelEditorQueryBuilder({
|
||||
};
|
||||
|
||||
const modes = Object.keys(
|
||||
panelDefinition.supportedQueryModesKindMap,
|
||||
) as QueryMode[];
|
||||
panelDefinition.supportedQueryTypesKindMap,
|
||||
) as EQueryType[];
|
||||
|
||||
return modes.map((queryType) => ({
|
||||
key: queryType,
|
||||
@@ -176,7 +178,7 @@ function PanelEditorQueryBuilder({
|
||||
),
|
||||
children: queryTypeComponents[queryType].component,
|
||||
// QB wants a null data source and AI pins traces: both mounted, they overwrite each other's provider config in a loop.
|
||||
destroyInactiveTabPane: isBuilderMode(queryType),
|
||||
destroyInactiveTabPane: isBuilderQueryType(queryType),
|
||||
}));
|
||||
}, [panelDefinition, panelType, isDarkMode, isRawQuery]);
|
||||
|
||||
@@ -193,8 +195,8 @@ function PanelEditorQueryBuilder({
|
||||
className={cx(styles.tabsContainer, {
|
||||
[styles.stickyNav]: stickyHeader,
|
||||
})}
|
||||
activeKey={getQueryMode(currentQuery)}
|
||||
onChange={onChangeQueryMode}
|
||||
activeKey={currentQuery.queryType}
|
||||
onChange={onChangeQueryType}
|
||||
tabBarExtraContent={
|
||||
<span className={styles.runQueryBtnContainer}>
|
||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||
|
||||
@@ -8,7 +8,6 @@ import PanelEditorQueryBuilder from './PanelEditorQueryBuilder';
|
||||
function QueryBuilderEditorPane({
|
||||
panelDefinition,
|
||||
isLoadingQueries,
|
||||
onChangeQueryMode,
|
||||
onStageRunQuery,
|
||||
onCancelQuery,
|
||||
stickyHeader,
|
||||
@@ -17,7 +16,6 @@ function QueryBuilderEditorPane({
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
onStageRunQuery={onStageRunQuery}
|
||||
onCancelQuery={onCancelQuery}
|
||||
stickyHeader={stickyHeader}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { useRef } from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
import {
|
||||
toPanelType,
|
||||
type PanelKind,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import type { PanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
|
||||
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder';
|
||||
import { useQueryModeChange } from '../../hooks/useQueryModeChange';
|
||||
|
||||
// Capture the props the (real-guard-fed) QueryBuilderV2 receives without rendering it.
|
||||
const mockQueryBuilderV2 = jest.fn();
|
||||
@@ -57,25 +51,19 @@ const BUILDER_QUERY = {
|
||||
builder: { queryData: [{ dataSource: 'traces' }] },
|
||||
};
|
||||
const AI_QUERY = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder: { queryData: [] },
|
||||
builder_ai_query: {
|
||||
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
|
||||
},
|
||||
};
|
||||
|
||||
/** Stands in for the editor shell: owns the tab switch and its parked Query Builder query. */
|
||||
/** Stands in for the editor shell. */
|
||||
function BuilderHost({ panelKind }: { panelKind: PanelKind }): JSX.Element {
|
||||
const panelDefinition = requireQueryPanelDefinition(panelKind);
|
||||
const parkedBuilders = useRef<BuildersByVariant>({});
|
||||
const onChangeQueryMode = useQueryModeChange({
|
||||
panelType: toPanelType(panelKind),
|
||||
supportedQueryModesKindMap: panelDefinition.supportedQueryModesKindMap,
|
||||
parkedBuilders,
|
||||
});
|
||||
return (
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
isLoadingQueries={false}
|
||||
onStageRunQuery={jest.fn()}
|
||||
onCancelQuery={jest.fn()}
|
||||
@@ -172,11 +160,12 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds an AI traces query when switching to the AI tab', () => {
|
||||
it('seeds an AI traces query when switching to the AI tab', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
mockBuilder(BUILDER_QUERY);
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
fireEvent.click(screen.getByText('AI Query Builder'));
|
||||
await user.click(screen.getByText('AI Query Builder'));
|
||||
|
||||
expect(updateAllQueriesOperators).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
@@ -184,11 +173,16 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
'traces',
|
||||
);
|
||||
const [seeded] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(seeded.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(seeded.builder.queryData[0].builderQueryType).toBe('builder_ai_query');
|
||||
expect(seeded.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(seeded.builder_ai_query.queryData[0]).toMatchObject({
|
||||
dataSource: 'traces',
|
||||
});
|
||||
expect(seeded.builder_ai_query.queryData[0].builderQueryType).toBeUndefined();
|
||||
expect(seeded.builder).toBe(BUILDER_QUERY.builder);
|
||||
});
|
||||
|
||||
it('keeps the builder query when returning from ClickHouse', () => {
|
||||
it('keeps the builder query when returning from ClickHouse', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logsQuery = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: { queryData: [{ dataSource: 'logs', legend: 'kept' }] },
|
||||
@@ -196,18 +190,19 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
mockBuilder(logsQuery);
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
fireEvent.click(screen.getByText('ClickHouse Query'));
|
||||
await user.click(screen.getByText('ClickHouse Query'));
|
||||
const [onClickHouse] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(onClickHouse.queryType).toBe(EQueryType.CLICKHOUSE);
|
||||
// The builder slot already holds the Query Builder query — nothing to swap.
|
||||
expect(onClickHouse.builder).toBe(logsQuery.builder);
|
||||
});
|
||||
|
||||
it('shows a fresh default query when leaving AI', () => {
|
||||
it('seeds a default query for the kind when leaving AI for an unused Query Builder', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
mockBuilder(AI_QUERY);
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
fireEvent.click(screen.getByText('Query Builder'));
|
||||
await user.click(screen.getByText('Query Builder'));
|
||||
|
||||
const [next] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(next.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
@@ -215,7 +210,22 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
expect(next.builder.queryData[0].dataSource).toBe('metrics');
|
||||
});
|
||||
|
||||
it("keeps each tab's own query across Query Builder ↔ AI round trips", () => {
|
||||
it("reseeds a Query Builder query this kind can't author when leaving AI", async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
mockBuilder({
|
||||
...AI_QUERY,
|
||||
builder: { queryData: [{ dataSource: 'metrics' }] },
|
||||
});
|
||||
renderBuilder('signoz/ListPanel');
|
||||
|
||||
await user.click(screen.getByText('Query Builder'));
|
||||
|
||||
const [next] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(next.builder.queryData[0].dataSource).toBe('logs');
|
||||
});
|
||||
|
||||
it("keeps each tab's own query across Query Builder ↔ AI round trips", async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logsQuery = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: { queryData: [{ dataSource: 'logs' }] },
|
||||
@@ -226,23 +236,26 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
mockBuilder(logsQuery);
|
||||
const { rerender } = render(host());
|
||||
|
||||
fireEvent.click(screen.getByText('AI Query Builder'));
|
||||
await user.click(screen.getByText('AI Query Builder'));
|
||||
const [aiQuery] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
// The user writes an AI query, then goes back to Query Builder.
|
||||
const editedAIBuilder = {
|
||||
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
|
||||
};
|
||||
mockBuilder({ ...aiQuery, builder: editedAIBuilder });
|
||||
mockBuilder({ ...aiQuery, builder_ai_query: editedAIBuilder });
|
||||
rerender(host());
|
||||
fireEvent.click(screen.getByText('Query Builder'));
|
||||
await user.click(screen.getByText('Query Builder'));
|
||||
const [backToQB] = redirectWithQueryBuilderData.mock.calls[1];
|
||||
expect(backToQB.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(backToQB.builder).toBe(logsQuery.builder);
|
||||
expect(backToQB.builder_ai_query).toBe(editedAIBuilder);
|
||||
|
||||
mockBuilder(backToQB);
|
||||
rerender(host());
|
||||
fireEvent.click(screen.getByText('AI Query Builder'));
|
||||
await user.click(screen.getByText('AI Query Builder'));
|
||||
const [backToAI] = redirectWithQueryBuilderData.mock.calls[2];
|
||||
expect(backToAI.builder).toBe(editedAIBuilder);
|
||||
expect(backToAI.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(backToAI.builder_ai_query).toBe(editedAIBuilder);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
getQueryTypeSignals,
|
||||
type SupportedQueryTypesKindMap,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
|
||||
import { seedBuilderForMode } from 'pages/DashboardPage/DashboardContainer/Panels/utils/seedBuilderForMode';
|
||||
|
||||
interface UseQueryTypeChangeArgs {
|
||||
panelType: PANEL_TYPES;
|
||||
supportedQueryTypesKindMap: SupportedQueryTypesKindMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab switching: each query type keeps its own slot, so a switch only changes `queryType`.
|
||||
* A builder slot is seeded when it can't be used as-is: an AI slot never opened, or
|
||||
* Query Builder queries on a signal this kind can't author (an AI panel's default).
|
||||
*/
|
||||
export function useQueryTypeChange({
|
||||
panelType,
|
||||
supportedQueryTypesKindMap,
|
||||
}: UseQueryTypeChangeArgs): (key: string) => void {
|
||||
const {
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
updateAllQueriesOperators,
|
||||
} = useQueryBuilder();
|
||||
|
||||
return useCallback(
|
||||
(key: string): void => {
|
||||
const queryType = key as EQueryType;
|
||||
const builderSignals = getQueryTypeSignals(
|
||||
supportedQueryTypesKindMap,
|
||||
EQueryType.QUERY_BUILDER,
|
||||
);
|
||||
const seed = (): Query['builder'] =>
|
||||
seedBuilderForMode({
|
||||
mode: queryType,
|
||||
defaultSignal: builderSignals[0],
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
});
|
||||
|
||||
const next: Query = { ...currentQuery, queryType };
|
||||
if (queryType === EQueryType.AI_QUERY_BUILDER && !next.builder_ai_query) {
|
||||
next.builder_ai_query = seed();
|
||||
}
|
||||
const authorableSignals = new Set<string>(builderSignals);
|
||||
const builderFitsKind =
|
||||
next.builder.queryData.length > 0 &&
|
||||
next.builder.queryData.every((query) =>
|
||||
authorableSignals.has(query.dataSource),
|
||||
);
|
||||
if (queryType === EQueryType.QUERY_BUILDER && !builderFitsKind) {
|
||||
next.builder = seed();
|
||||
}
|
||||
|
||||
redirectWithQueryBuilderData(next);
|
||||
},
|
||||
[
|
||||
currentQuery,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
supportedQueryTypesKindMap,
|
||||
updateAllQueriesOperators,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -72,8 +72,6 @@ interface QueryEditorBodyProps {
|
||||
/** Kind switch, owned by the shell (its cache must survive the fork swap). */
|
||||
onChangePanelKind: (kind: PanelKind) => void;
|
||||
originalPanelKind?: PanelKind;
|
||||
/** Tab switch, owned by the shell alongside the kind switch it shares memory with. */
|
||||
onChangeQueryMode: (key: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +93,6 @@ function QueryEditorBody({
|
||||
panelDefinition,
|
||||
onChangePanelKind,
|
||||
originalPanelKind,
|
||||
onChangeQueryMode,
|
||||
}: QueryEditorBodyProps): JSX.Element {
|
||||
// Read here rather than taken as props: this renders inside a loaded dashboard
|
||||
// subtree, so it resolves the same context every other consumer does.
|
||||
@@ -312,7 +309,6 @@ function QueryEditorBody({
|
||||
panelDefinition={panelDefinition}
|
||||
signal={listSignal}
|
||||
isLoadingQueries={isFetching}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
onStageRunQuery={runQuery}
|
||||
onCancelQuery={cancelQuery}
|
||||
spec={spec}
|
||||
|
||||
@@ -60,9 +60,8 @@ jest.mock('../hooks/usePanelEditorQuerySync', () => ({
|
||||
}));
|
||||
|
||||
const mockUseTypeSwitch = jest.fn();
|
||||
jest.mock('../hooks/usePanelKindAndQueryModeSwitch', () => ({
|
||||
usePanelKindAndQueryModeSwitch: (args: unknown): unknown =>
|
||||
mockUseTypeSwitch(args),
|
||||
jest.mock('../hooks/usePanelKindChange', () => ({
|
||||
usePanelKindChange: (args: unknown): unknown => mockUseTypeSwitch(args),
|
||||
}));
|
||||
|
||||
jest.mock('../hooks/usePanelEditorSave', () => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
@@ -22,8 +22,6 @@ import { usePanelEditorQuerySync } from '../hooks/usePanelEditorQuerySync';
|
||||
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
|
||||
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder/PanelEditorQueryBuilder';
|
||||
import { useQueryModeChange } from '../hooks/useQueryModeChange';
|
||||
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
|
||||
jest.mock('hooks/useSafeNavigate', () => {
|
||||
@@ -81,17 +79,10 @@ function EditorRoute({ panelId }: { panelId: string }): JSX.Element {
|
||||
savedQueries: panel.spec.queries,
|
||||
});
|
||||
const panelDefinition = requireQueryPanelDefinition('signoz/TimeSeriesPanel');
|
||||
const parkedBuilders = useRef<BuildersByVariant>({});
|
||||
const onChangeQueryMode = useQueryModeChange({
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
supportedQueryModesKindMap: panelDefinition.supportedQueryModesKindMap,
|
||||
parkedBuilders,
|
||||
});
|
||||
|
||||
return (
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
isLoadingQueries={false}
|
||||
onStageRunQuery={noop}
|
||||
onCancelQuery={noop}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { handleQueryChange } from 'lib/query/panelQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { resolveQueryMode } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
import { resolveQueryType } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
import { getBuilderQueries } from '../../../Panels/utils/getBuilderQueries';
|
||||
import { toPerses } from '../../../queryV5/persesQueryAdapters';
|
||||
import { getSwitchedPluginSpec } from '../../getSwitchedPluginSpec';
|
||||
import { usePanelKindAndQueryModeSwitch } from '../usePanelKindAndQueryModeSwitch';
|
||||
import { usePanelKindChange } from '../usePanelKindChange';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
@@ -18,10 +18,8 @@ jest.mock('lib/query/panelQuery', () => ({
|
||||
handleQueryChange: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../Panels/capabilities', () => ({
|
||||
resolveQueryMode: jest.fn(),
|
||||
resolveQueryType: jest.fn(),
|
||||
getSupportedSignals: jest.fn(() => ['metrics']),
|
||||
getQueryPanelDefinition: jest.requireActual('../../../Panels/capabilities')
|
||||
.getQueryPanelDefinition,
|
||||
// Real predicate: these specs use real (query) kinds and the static path is
|
||||
// exercised through its own cases below.
|
||||
isStaticPanelKind: jest.requireActual('../../../Panels/capabilities')
|
||||
@@ -39,7 +37,7 @@ jest.mock('../../../Panels/utils/getBuilderQueries', () => ({
|
||||
|
||||
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
|
||||
const mockHandleQueryChange = handleQueryChange as unknown as jest.Mock;
|
||||
const mockResolveQueryMode = resolveQueryMode as unknown as jest.Mock;
|
||||
const mockResolveQueryType = resolveQueryType as unknown as jest.Mock;
|
||||
const mockToPerses = toPerses as unknown as jest.Mock;
|
||||
const mockGetSwitchedPluginSpec = getSwitchedPluginSpec as unknown as jest.Mock;
|
||||
const mockGetBuilderQueries = getBuilderQueries as unknown as jest.Mock;
|
||||
@@ -102,7 +100,7 @@ function builderState(currentQuery: Query): {
|
||||
};
|
||||
}
|
||||
|
||||
describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
describe('usePanelKindChange', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockHandleQueryChange.mockReturnValue(TRANSFORMED);
|
||||
@@ -111,7 +109,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
mockGetBuilderQueries.mockReturnValue([{ signal: 'logs' }]);
|
||||
// The guard owns coercion (tested in capabilities.test.ts); here it always
|
||||
// resolves to Query Builder so the coerced type flows into handleQueryChange.
|
||||
mockResolveQueryMode.mockReturnValue('builder');
|
||||
mockResolveQueryType.mockReturnValue('builder');
|
||||
});
|
||||
|
||||
it('does nothing when switching to the current kind', () => {
|
||||
@@ -120,7 +118,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
usePanelKindChange({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
@@ -139,7 +137,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
usePanelKindChange({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
@@ -171,7 +169,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
usePanelKindChange({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
@@ -202,7 +200,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
usePanelKindChange({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
@@ -238,7 +236,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
usePanelKindChange({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
@@ -279,7 +277,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
usePanelKindChange({
|
||||
spec: makeSpec('signoz/HeatmapPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.HEATMAP,
|
||||
setSpec,
|
||||
@@ -320,7 +318,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
usePanelKindChange({
|
||||
spec: makeSpec('signoz/HeatmapPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.HEATMAP,
|
||||
setSpec,
|
||||
@@ -338,7 +336,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
mockUseQueryBuilder.mockReturnValue(builderState(promQuery));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
usePanelKindChange({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
@@ -347,7 +345,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
|
||||
// The hook asks the guard to resolve the active query type against the new kind…
|
||||
expect(mockResolveQueryMode).toHaveBeenCalledWith(
|
||||
expect(mockResolveQueryType).toHaveBeenCalledWith(
|
||||
'signoz/ListPanel',
|
||||
'promql',
|
||||
);
|
||||
@@ -356,211 +354,128 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
expect((queryArg as Query).queryType).toBe('builder');
|
||||
});
|
||||
|
||||
it('keeps the AI tag when the new kind supports AI', () => {
|
||||
const setSpec = jest.fn();
|
||||
const aiQuery = fakeQuery('ai', 'builder', [
|
||||
{ builderQueryType: 'builder_ai_query' },
|
||||
]);
|
||||
mockUseQueryBuilder.mockReturnValue(builderState(aiQuery));
|
||||
mockResolveQueryMode.mockReturnValue('builder_ai_query');
|
||||
describe('AI query slot', () => {
|
||||
const QB_BUILDER = { queryData: [{ dataSource: 'logs' }] };
|
||||
const AI_BUILDER = {
|
||||
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
|
||||
};
|
||||
const withBothSlots = (id: string, queryType: string): Query =>
|
||||
({
|
||||
id,
|
||||
queryType,
|
||||
builder: QB_BUILDER,
|
||||
builder_ai_query: AI_BUILDER,
|
||||
}) as unknown as Query;
|
||||
const lastRedirect = (state: ReturnType<typeof builderState>): Query =>
|
||||
state.redirectWithQueryBuilderData.mock.calls[
|
||||
state.redirectWithQueryBuilderData.mock.calls.length - 1
|
||||
][0] as Query;
|
||||
const lastSpec = (setSpec: jest.Mock): DashboardtypesPanelSpecDTO =>
|
||||
setSpec.mock.calls[setSpec.mock.calls.length - 1][0];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.onChangePanelKind('signoz/TimeSeriesPanel'));
|
||||
|
||||
const [, queryArg] = mockHandleQueryChange.mock.calls[0];
|
||||
expect((queryArg as Query).builder).toBe(aiQuery.builder);
|
||||
expect((queryArg as Query).builder.queryData[0].builderQueryType).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
|
||||
it('drops the AI tag when the new kind does not support AI', () => {
|
||||
const setSpec = jest.fn();
|
||||
const aiQuery = fakeQuery('ai', 'builder', [
|
||||
{ dataSource: 'traces', builderQueryType: 'builder_ai_query' },
|
||||
]);
|
||||
const state = builderState(aiQuery);
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
mockHandleQueryChange.mockImplementation((_type, query) => query);
|
||||
// The guard coerces to Query Builder; the tag must not survive it.
|
||||
mockResolveQueryMode.mockReturnValue('builder');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.onChangePanelKind('signoz/TimeSeriesPanel'));
|
||||
|
||||
const redirected = state.redirectWithQueryBuilderData.mock
|
||||
.calls[0][0] as Query;
|
||||
expect(redirected.builder.queryData[0].builderQueryType).toBeUndefined();
|
||||
expect(redirected.builder.queryData[0].dataSource).toBe('traces');
|
||||
});
|
||||
|
||||
it('stays on the AI tab when returning to a kind last left on Query Builder', () => {
|
||||
const setSpec = jest.fn();
|
||||
const qbQuery = fakeQuery('qb', 'builder', [{ dataSource: 'logs' }]);
|
||||
const aiQuery = fakeQuery('ai', 'builder', [
|
||||
{ dataSource: 'traces', builderQueryType: 'builder_ai_query' },
|
||||
]);
|
||||
let state = builderState(qbQuery);
|
||||
mockUseQueryBuilder.mockImplementation(() => state);
|
||||
mockHandleQueryChange.mockImplementation((_type, query) => query);
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
|
||||
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
|
||||
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
|
||||
);
|
||||
|
||||
// Leave Table on Query Builder — its stash holds the QB query.
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
|
||||
// On List the user moves to the AI tab, then returns to Table.
|
||||
state = builderState(aiQuery);
|
||||
mockResolveQueryMode.mockReturnValue('builder_ai_query');
|
||||
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
|
||||
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
|
||||
|
||||
const redirected = state.redirectWithQueryBuilderData.mock
|
||||
.calls[0][0] as Query;
|
||||
expect(redirected.builder.queryData[0].builderQueryType).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
const restored = setSpec.mock.calls[
|
||||
setSpec.mock.calls.length - 1
|
||||
][0] as DashboardtypesPanelSpecDTO;
|
||||
// The kind's own display spec still comes back; only the query follows the tab.
|
||||
expect(restored.plugin.spec).toBe(TABLE_PLUGIN_SPEC);
|
||||
expect(restored.queries).toBe(CONVERTED);
|
||||
|
||||
// Table's Query Builder query waited behind the AI tab — the tab switch gives it back.
|
||||
act(() => result.current.onChangeQueryMode('builder'));
|
||||
const backToQB = state.redirectWithQueryBuilderData.mock.calls[1][0] as Query;
|
||||
expect(backToQB.builder).toBe(qbQuery.builder);
|
||||
});
|
||||
|
||||
describe('both builder tabs, per kind', () => {
|
||||
const qbQuery = fakeQuery('qb', 'builder', [{ dataSource: 'logs' }]);
|
||||
const aiQuery = fakeQuery('ai', 'builder', [
|
||||
{ dataSource: 'traces', builderQueryType: 'builder_ai_query' },
|
||||
]);
|
||||
|
||||
// Table gets a Query Builder query and an AI one; the user leaves it on AI for List.
|
||||
function leaveTableOnAI(): {
|
||||
result: { current: ReturnType<typeof usePanelKindAndQueryModeSwitch> };
|
||||
rerender: (props: {
|
||||
spec: DashboardtypesPanelSpecDTO;
|
||||
panelType: PANEL_TYPES;
|
||||
}) => void;
|
||||
setSpec: jest.Mock;
|
||||
setState: (query: Query) => ReturnType<typeof builderState>;
|
||||
} {
|
||||
const setSpec = jest.fn();
|
||||
let state = builderState(qbQuery);
|
||||
mockUseQueryBuilder.mockImplementation(() => state);
|
||||
beforeEach(() => {
|
||||
mockHandleQueryChange.mockImplementation((_type, query) => query);
|
||||
});
|
||||
|
||||
it('rebuilds both builder slots for the new kind on a first visit', () => {
|
||||
const setSpec = jest.fn();
|
||||
const state = builderState(withBothSlots('q', 'builder_ai_query'));
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
mockResolveQueryType.mockReturnValue('builder_ai_query');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
|
||||
const rebuilt = mockHandleQueryChange.mock.calls.map(
|
||||
([, query]) => (query as Query).builder,
|
||||
);
|
||||
expect(rebuilt).toStrictEqual([QB_BUILDER, AI_BUILDER]);
|
||||
const redirected = lastRedirect(state);
|
||||
expect(redirected.queryType).toBe('builder_ai_query');
|
||||
expect(redirected.builder.queryData[0].dataSource).toBe('logs');
|
||||
expect(redirected.builder_ai_query?.queryData[0].dataSource).toBe('traces');
|
||||
});
|
||||
|
||||
it('falls back to Query Builder, keeping the AI query, when the new kind has no AI', () => {
|
||||
const setSpec = jest.fn();
|
||||
const state = builderState(withBothSlots('q', 'builder_ai_query'));
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
mockResolveQueryType.mockReturnValue('builder');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.onChangePanelKind('signoz/TimeSeriesPanel'));
|
||||
|
||||
const redirected = lastRedirect(state);
|
||||
expect(redirected.queryType).toBe('builder');
|
||||
expect(redirected.builder.queryData[0].dataSource).toBe('logs');
|
||||
expect(redirected.builder_ai_query).toBeDefined();
|
||||
});
|
||||
|
||||
it('restores a revisited kind on the tab in use, regenerating its preview queries', () => {
|
||||
const setSpec = jest.fn();
|
||||
let state = builderState(withBothSlots('table', 'builder_ai_query'));
|
||||
mockUseQueryBuilder.mockImplementation(() => state);
|
||||
mockResolveQueryType.mockReturnValue('builder_ai_query');
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
|
||||
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
|
||||
usePanelKindChange({ ...props, setSpec }),
|
||||
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
|
||||
);
|
||||
const setState = (query: Query): ReturnType<typeof builderState> => {
|
||||
state = builderState(query);
|
||||
return state;
|
||||
};
|
||||
|
||||
act(() => result.current.onChangeQueryMode('builder_ai_query'));
|
||||
setState(aiQuery);
|
||||
rerender({ spec: tableSpec, panelType: PANEL_TYPES.TABLE });
|
||||
mockResolveQueryMode.mockReturnValue('builder_ai_query');
|
||||
// Leave Table on AI; on List the user moves to Query Builder, then returns.
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
return { result, rerender, setSpec, setState };
|
||||
}
|
||||
|
||||
it('rebuilds the parked query for the new kind on a first visit', () => {
|
||||
const { result } = leaveTableOnAI();
|
||||
|
||||
// Table's Query Builder query was rebuilt for List, and is one tab away there.
|
||||
expect(mockHandleQueryChange).toHaveBeenCalledWith(
|
||||
'list',
|
||||
expect.objectContaining({ builder: qbQuery.builder }),
|
||||
PANEL_TYPES.TABLE,
|
||||
);
|
||||
act(() => result.current.onChangeQueryMode('builder'));
|
||||
const { redirectWithQueryBuilderData } =
|
||||
mockUseQueryBuilder.mock.results[
|
||||
mockUseQueryBuilder.mock.results.length - 1
|
||||
].value;
|
||||
const backToQB = redirectWithQueryBuilderData.mock.calls[
|
||||
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||
][0] as Query;
|
||||
expect(backToQB.builder.queryData[0].dataSource).toBe('logs');
|
||||
expect(backToQB.builder.queryData[0].builderQueryType).toBeUndefined();
|
||||
});
|
||||
|
||||
it("brings back both tabs' queries when revisiting a kind", () => {
|
||||
const { result, rerender, setSpec, setState } = leaveTableOnAI();
|
||||
|
||||
// Back to Table from List, still on the AI tab.
|
||||
const state = setState(
|
||||
fakeQuery('list-ai', 'builder', [
|
||||
{ dataSource: 'traces', builderQueryType: 'builder_ai_query' },
|
||||
]),
|
||||
);
|
||||
state = builderState(withBothSlots('list', 'builder'));
|
||||
mockResolveQueryType.mockReturnValue('builder');
|
||||
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
|
||||
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
|
||||
|
||||
const restored = state.redirectWithQueryBuilderData.mock
|
||||
.calls[0][0] as Query;
|
||||
expect(restored.builder).toBe(aiQuery.builder);
|
||||
const restoredSpec = setSpec.mock.calls[
|
||||
setSpec.mock.calls.length - 1
|
||||
][0] as DashboardtypesPanelSpecDTO;
|
||||
expect(restoredSpec.plugin.spec).toBe(TABLE_PLUGIN_SPEC);
|
||||
expect(restoredSpec.queries).toBe(TABLE_QUERIES);
|
||||
|
||||
// Table's Query Builder query is one tab away.
|
||||
act(() => result.current.onChangeQueryMode('builder'));
|
||||
const backToQB = state.redirectWithQueryBuilderData.mock
|
||||
.calls[1][0] as Query;
|
||||
expect(backToQB.builder).toBe(qbQuery.builder);
|
||||
const restored = lastRedirect(state);
|
||||
expect(restored.id).toBe('table');
|
||||
expect(restored.queryType).toBe('builder');
|
||||
expect(restored.builder).toBe(QB_BUILDER);
|
||||
expect(restored.builder_ai_query).toBe(AI_BUILDER);
|
||||
expect(lastSpec(setSpec).plugin.spec).toBe(TABLE_PLUGIN_SPEC);
|
||||
expect(lastSpec(setSpec).queries).toBe(CONVERTED);
|
||||
});
|
||||
|
||||
it('swaps in the Query Builder query when revisiting a kind left on AI', () => {
|
||||
const { result, rerender, setSpec, setState } = leaveTableOnAI();
|
||||
|
||||
// On List the user is back on Query Builder, then returns to Table.
|
||||
const state = setState(
|
||||
fakeQuery('list-qb', 'builder', [{ dataSource: 'logs' }]),
|
||||
it('carries the AI query into a revisited kind that never had one', () => {
|
||||
const setSpec = jest.fn();
|
||||
let state = builderState(
|
||||
fakeQuery('table', 'builder', [{ dataSource: 'logs' }]),
|
||||
);
|
||||
mockResolveQueryMode.mockReturnValue('builder');
|
||||
mockUseQueryBuilder.mockImplementation(() => state);
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
|
||||
usePanelKindChange({ ...props, setSpec }),
|
||||
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
|
||||
);
|
||||
// Leave Table on Query Builder; on List the user writes an AI query, then returns.
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
state = builderState(withBothSlots('list', 'builder_ai_query'));
|
||||
mockResolveQueryType.mockReturnValue('builder_ai_query');
|
||||
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
|
||||
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
|
||||
|
||||
const restored = state.redirectWithQueryBuilderData.mock
|
||||
.calls[0][0] as Query;
|
||||
expect(restored.builder).toBe(qbQuery.builder);
|
||||
// The stashed preview queries were the AI query's; they're rebuilt for the swap.
|
||||
const restoredSpec = setSpec.mock.calls[
|
||||
setSpec.mock.calls.length - 1
|
||||
][0] as DashboardtypesPanelSpecDTO;
|
||||
expect(restoredSpec.queries).toBe(CONVERTED);
|
||||
|
||||
// Table's AI query is one tab away.
|
||||
act(() => result.current.onChangeQueryMode('builder_ai_query'));
|
||||
const backToAI = state.redirectWithQueryBuilderData.mock
|
||||
.calls[1][0] as Query;
|
||||
expect(backToAI.builder).toBe(aiQuery.builder);
|
||||
const restored = lastRedirect(state);
|
||||
expect(restored.id).toBe('table');
|
||||
expect(restored.queryType).toBe('builder_ai_query');
|
||||
expect(restored.builder_ai_query?.queryData[0].builderQueryType).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
expect(lastSpec(setSpec).queries).toBe(CONVERTED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -573,7 +488,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
|
||||
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
|
||||
usePanelKindChange({ ...props, setSpec }),
|
||||
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getIsQueryModified } from 'lib/query/panelQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -111,9 +112,10 @@ export function usePanelEditorQuerySync({
|
||||
|
||||
// Re-commit on a query-type/datasource switch so the preview refetches. Skip
|
||||
// mount: the initial query is synced into the draft by the staged-query effect below.
|
||||
const activeQueryData = getActiveBuilder(currentQuery).queryData;
|
||||
const dataSources = useMemo(
|
||||
() => (currentQuery.builder?.queryData ?? []).map((q) => q.dataSource),
|
||||
[currentQuery.builder],
|
||||
() => (activeQueryData ?? []).map((q) => q.dataSource),
|
||||
[activeQueryData],
|
||||
);
|
||||
const dataSourceSignature = dataSources.join(',');
|
||||
const prevDataSourcesRef = useRef(dataSources);
|
||||
|
||||
@@ -11,27 +11,16 @@ import { handleQueryChange } from 'lib/query/panelQuery';
|
||||
import type { PartialPanelTypes } from 'lib/query/panelTypeDataSourceFormValuesMap';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
|
||||
import {
|
||||
type BuilderVariant,
|
||||
type BuildersByVariant,
|
||||
type OrderByPayload,
|
||||
type Query,
|
||||
QueryMode,
|
||||
import type {
|
||||
OrderByPayload,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
getQueryPanelDefinition,
|
||||
isStaticPanelKind,
|
||||
resolveQueryMode,
|
||||
resolveQueryType,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
import { getQueryModeSignals } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
|
||||
import {
|
||||
getBuilderVariant,
|
||||
getQueryMode,
|
||||
toBuilderVariant,
|
||||
withoutAIQueryTag,
|
||||
} from 'lib/query/queryMode';
|
||||
import { seedBuilderForMode } from 'pages/DashboardPage/DashboardContainer/Panels/utils/seedBuilderForMode';
|
||||
import { toPanelType, type PanelKind } from '../../Panels/types/panelKind';
|
||||
import { getBuilderQueries } from '../../Panels/utils/getBuilderQueries';
|
||||
import { toPerses } from '../../queryV5/persesQueryAdapters';
|
||||
@@ -43,7 +32,6 @@ import {
|
||||
withPercentileHistogramAggregation,
|
||||
withSingleHeatmapQuery,
|
||||
} from '../utils/heatmapQuery';
|
||||
import { useQueryModeChange } from './useQueryModeChange';
|
||||
|
||||
// V1's handleQueryChange clears orderBy for lists; re-seed the fresh-list default (timestamp desc).
|
||||
const DEFAULT_LIST_ORDER_BY: OrderByPayload[] = [
|
||||
@@ -69,21 +57,17 @@ interface KindState {
|
||||
pluginSpec: DashboardtypesPanelPluginDTO['spec'];
|
||||
queries: DashboardtypesQueryDTO[];
|
||||
query: Query;
|
||||
/** Both builder variants' queries as the kind was left. */
|
||||
builders: BuildersByVariant;
|
||||
}
|
||||
|
||||
interface UsePanelKindAndQueryModeSwitchArgs {
|
||||
interface UsePanelKindChangeArgs {
|
||||
spec: DashboardtypesPanelSpecDTO;
|
||||
panelType: PANEL_TYPES;
|
||||
setSpec: (next: DashboardtypesPanelSpecDTO) => void;
|
||||
}
|
||||
|
||||
interface UsePanelKindAndQueryModeSwitchApi {
|
||||
interface UsePanelKindChangeApi {
|
||||
/** Switch the panel to `newKind`, transforming/restoring its query + spec. */
|
||||
onChangePanelKind: (newKind: PanelKind) => void;
|
||||
/** Switch authoring tab; shares the off-screen builder stash with the kind switch. */
|
||||
onChangeQueryMode: (key: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,22 +76,15 @@ interface UsePanelKindAndQueryModeSwitchApi {
|
||||
* the two things that don't: a per-kind session cache that makes switching reversible
|
||||
* (`Table → List → Table` restores the original query + spec), and, on first visit to a
|
||||
* kind, a query rebuild (`handleQueryChange`) + spec reset (`getSwitchedPluginSpec`).
|
||||
* It also owns the tab switch, stashing the off-screen builder variant per kind.
|
||||
*/
|
||||
export function usePanelKindAndQueryModeSwitch({
|
||||
export function usePanelKindChange({
|
||||
spec,
|
||||
panelType,
|
||||
setSpec,
|
||||
}: UsePanelKindAndQueryModeSwitchArgs): UsePanelKindAndQueryModeSwitchApi {
|
||||
const {
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
updateAllQueriesOperators,
|
||||
} = useQueryBuilder();
|
||||
}: UsePanelKindChangeArgs): UsePanelKindChangeApi {
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const cacheRef = useRef<Map<PanelKind, KindState>>(new Map());
|
||||
// Builder queries for the tabs not on screen; the live one is `currentQuery.builder`.
|
||||
const parkedBuilders = useRef<BuildersByVariant>({});
|
||||
|
||||
// Latest spec/query/type, read inside the stable callback without re-subscribing.
|
||||
const specRef = useRef(spec);
|
||||
@@ -117,14 +94,6 @@ export function usePanelKindAndQueryModeSwitch({
|
||||
const panelTypeRef = useRef(panelType);
|
||||
panelTypeRef.current = panelType;
|
||||
|
||||
const onChangeQueryMode = useQueryModeChange({
|
||||
panelType,
|
||||
supportedQueryModesKindMap:
|
||||
getQueryPanelDefinition(spec.plugin.kind as PanelKind)
|
||||
?.supportedQueryModesKindMap ?? {},
|
||||
parkedBuilders,
|
||||
});
|
||||
|
||||
const onChangePanelKind = useCallback(
|
||||
(newKind: PanelKind): void => {
|
||||
const currentSpec = specRef.current;
|
||||
@@ -142,20 +111,9 @@ export function usePanelKindAndQueryModeSwitch({
|
||||
pluginSpec: currentSpec.plugin.spec,
|
||||
queries: currentSpec.queries,
|
||||
query,
|
||||
builders: {
|
||||
...parkedBuilders.current,
|
||||
[getBuilderVariant(query.builder)]: query.builder,
|
||||
},
|
||||
});
|
||||
|
||||
const newPanelType = toPanelType(newKind);
|
||||
const targetMode = resolveQueryMode(newKind, getQueryMode(query));
|
||||
const targetVariant = toBuilderVariant(targetMode);
|
||||
// An AI query is a builder query carrying the tag, so it rides on `builder`.
|
||||
const targetQueryType =
|
||||
targetMode === QueryMode.AI_QUERY_BUILDER
|
||||
? QueryMode.QUERY_BUILDER
|
||||
: targetMode;
|
||||
|
||||
// Only `plugin` needs a cast: it's a discriminated union over `kind`, and a
|
||||
// dynamically-chosen kind can't be correlated with its spec statically (as in
|
||||
@@ -172,10 +130,12 @@ export function usePanelKindAndQueryModeSwitch({
|
||||
} as DashboardtypesPanelPluginDTO,
|
||||
queries,
|
||||
});
|
||||
const rebuildForNewKind = (source: Query): Query => {
|
||||
|
||||
const queryType = resolveQueryType(newKind, query.queryType);
|
||||
const rebuildForNewKind = (builder: Query['builder']): Query['builder'] => {
|
||||
const transformed = handleQueryChange(
|
||||
newPanelType as keyof PartialPanelTypes,
|
||||
{ ...source, queryType: targetQueryType },
|
||||
{ ...query, queryType: EQueryType.QUERY_BUILDER, builder },
|
||||
panelTypeRef.current,
|
||||
);
|
||||
// Match a fresh list panel's default order so the builder's Order By isn't empty.
|
||||
@@ -183,62 +143,41 @@ export function usePanelKindAndQueryModeSwitch({
|
||||
newKind === 'signoz/ListPanel'
|
||||
? withDefaultListOrder(transformed)
|
||||
: transformed;
|
||||
return newKind === 'signoz/HeatmapPanel'
|
||||
? withSingleHeatmapQuery(ordered)
|
||||
: withPercentileHistogramAggregation(ordered);
|
||||
};
|
||||
// Tab unused on this kind: AI gets a fresh seed, Query Builder reuses `source` untagged.
|
||||
const seedTargetTab = (source: Query): Query =>
|
||||
targetMode === QueryMode.AI_QUERY_BUILDER
|
||||
? {
|
||||
...source,
|
||||
builder: seedBuilderForMode({
|
||||
mode: targetMode,
|
||||
defaultSignal: getQueryModeSignals(
|
||||
getQueryPanelDefinition(newKind)?.supportedQueryModesKindMap,
|
||||
QueryMode.QUERY_BUILDER,
|
||||
)[0],
|
||||
panelType: newPanelType,
|
||||
updateAllQueriesOperators,
|
||||
}),
|
||||
}
|
||||
: withoutAIQueryTag(source);
|
||||
// Target tab's query comes from the stash; the rest stay parked.
|
||||
const unparkTargetTab = (
|
||||
source: Query,
|
||||
builders: BuildersByVariant,
|
||||
): { query: Query; parked: BuildersByVariant } => {
|
||||
const stored = targetVariant ? builders[targetVariant] : source.builder;
|
||||
const next = stored
|
||||
? { ...source, builder: stored }
|
||||
: seedTargetTab(source);
|
||||
const parked: BuildersByVariant = { ...builders };
|
||||
delete parked[getBuilderVariant(next.builder)];
|
||||
return { query: next, parked };
|
||||
return (
|
||||
newKind === 'signoz/HeatmapPanel'
|
||||
? withSingleHeatmapQuery(ordered)
|
||||
: withPercentileHistogramAggregation(ordered)
|
||||
).builder;
|
||||
};
|
||||
|
||||
// Revisit → restore the stash in the active mode; a static kind has no builder to re-seed.
|
||||
// Revisit → restore the stash (the reversibility path) on the tab in use, an AI
|
||||
// query the kind never had riding along. A static kind's stash carries
|
||||
// `queries: []` and its builder query is untouched — there is no builder to
|
||||
// re-seed for it.
|
||||
const cached = cacheRef.current.get(newKind);
|
||||
if (cached) {
|
||||
if (isStaticPanelKind(newKind)) {
|
||||
setSpec(buildSpec(cached.pluginSpec, cached.queries));
|
||||
return;
|
||||
}
|
||||
const restored = unparkTargetTab(cached.query, cached.builders);
|
||||
parkedBuilders.current = restored.parked;
|
||||
// The stashed preview queries belong to the stashed query, not a swapped-in one.
|
||||
const restored: Query = {
|
||||
...cached.query,
|
||||
queryType,
|
||||
...(!cached.query.builder_ai_query &&
|
||||
query.builder_ai_query && {
|
||||
builder_ai_query: rebuildForNewKind(query.builder_ai_query),
|
||||
}),
|
||||
};
|
||||
// The stashed preview queries belong to the stashed query's type.
|
||||
setSpec(
|
||||
buildSpec(
|
||||
cached.pluginSpec,
|
||||
restored.query.builder === cached.query.builder
|
||||
queryType === cached.query.queryType
|
||||
? cached.queries
|
||||
: toPerses(restored.query, newPanelType),
|
||||
: toPerses(restored, newPanelType),
|
||||
),
|
||||
);
|
||||
redirectWithQueryBuilderData({
|
||||
...restored.query,
|
||||
queryType: targetQueryType,
|
||||
});
|
||||
redirectWithQueryBuilderData(restored);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -252,25 +191,15 @@ export function usePanelKindAndQueryModeSwitch({
|
||||
return;
|
||||
}
|
||||
|
||||
// First visit → rebuild both builder tabs' queries for the new panel type.
|
||||
const transformed = rebuildForNewKind(query);
|
||||
const rebuiltBuilders: BuildersByVariant = {};
|
||||
(Object.keys(parkedBuilders.current) as BuilderVariant[]).forEach(
|
||||
(variant) => {
|
||||
const parked = parkedBuilders.current[variant];
|
||||
if (parked) {
|
||||
rebuiltBuilders[variant] = rebuildForNewKind({
|
||||
...query,
|
||||
builder: parked,
|
||||
}).builder;
|
||||
}
|
||||
},
|
||||
);
|
||||
rebuiltBuilders[getBuilderVariant(transformed.builder)] =
|
||||
transformed.builder;
|
||||
const picked = unparkTargetTab(transformed, rebuiltBuilders);
|
||||
parkedBuilders.current = picked.parked;
|
||||
const nextQuery = picked.query;
|
||||
// First visit → rebuild every builder slot for the new kind.
|
||||
const nextQuery: Query = {
|
||||
...query,
|
||||
queryType,
|
||||
builder: rebuildForNewKind(query.builder),
|
||||
...(query.builder_ai_query && {
|
||||
builder_ai_query: rebuildForNewKind(query.builder_ai_query),
|
||||
}),
|
||||
};
|
||||
const signal = getBuilderQueries(currentSpec.queries)[0]
|
||||
?.signal as TelemetrytypesSignalDTO;
|
||||
|
||||
@@ -282,8 +211,8 @@ export function usePanelKindAndQueryModeSwitch({
|
||||
);
|
||||
redirectWithQueryBuilderData(nextQuery);
|
||||
},
|
||||
[setSpec, redirectWithQueryBuilderData, updateAllQueriesOperators],
|
||||
[setSpec, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
return { onChangePanelKind, onChangeQueryMode };
|
||||
return { onChangePanelKind };
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { type MutableRefObject, useCallback } from 'react';
|
||||
import type { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import {
|
||||
getQueryModeSignals,
|
||||
type SupportedQueryModesKindMap,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
|
||||
import {
|
||||
type BuildersByVariant,
|
||||
type Query,
|
||||
QueryMode,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { getBuilderVariant, toBuilderVariant } from 'lib/query/queryMode';
|
||||
import { seedBuilderForMode } from 'pages/DashboardPage/DashboardContainer/Panels/utils/seedBuilderForMode';
|
||||
|
||||
interface UseQueryModeChangeArgs {
|
||||
panelType: PANEL_TYPES;
|
||||
supportedQueryModesKindMap: SupportedQueryModesKindMap;
|
||||
/** Builder queries for the variant not on screen. */
|
||||
parkedBuilders: MutableRefObject<BuildersByVariant>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab switching. Query Builder and AI share `currentQuery.builder`, so crossing between
|
||||
* them swaps in the parked query, seeding a first visit; ClickHouse and PromQL pass through.
|
||||
*/
|
||||
export function useQueryModeChange({
|
||||
panelType,
|
||||
supportedQueryModesKindMap,
|
||||
parkedBuilders,
|
||||
}: UseQueryModeChangeArgs): (key: string) => void {
|
||||
const {
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
updateAllQueriesOperators,
|
||||
} = useQueryBuilder();
|
||||
|
||||
return useCallback(
|
||||
(key: string): void => {
|
||||
const targetMode = key as QueryMode;
|
||||
const targetVariant = toBuilderVariant(targetMode);
|
||||
const heldVariant = getBuilderVariant(currentQuery.builder);
|
||||
|
||||
const seedFor = (mode: QueryMode): Query['builder'] =>
|
||||
seedBuilderForMode({
|
||||
mode,
|
||||
defaultSignal: getQueryModeSignals(
|
||||
supportedQueryModesKindMap,
|
||||
QueryMode.QUERY_BUILDER,
|
||||
)[0],
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
});
|
||||
|
||||
// Keyed on what `builder` holds, not the active tab: ClickHouse → QB must not reseed.
|
||||
let { builder } = currentQuery;
|
||||
if (targetVariant && targetVariant !== heldVariant) {
|
||||
parkedBuilders.current[heldVariant] = currentQuery.builder;
|
||||
builder = parkedBuilders.current[targetVariant] ?? seedFor(targetMode);
|
||||
delete parkedBuilders.current[targetVariant];
|
||||
}
|
||||
|
||||
redirectWithQueryBuilderData({
|
||||
...currentQuery,
|
||||
// An AI query is a builder query carrying the AI tag; its query type stays `builder`.
|
||||
queryType:
|
||||
targetMode === QueryMode.AI_QUERY_BUILDER
|
||||
? QueryMode.QUERY_BUILDER
|
||||
: targetMode,
|
||||
builder,
|
||||
});
|
||||
},
|
||||
[
|
||||
currentQuery,
|
||||
panelType,
|
||||
parkedBuilders,
|
||||
redirectWithQueryBuilderData,
|
||||
supportedQueryModesKindMap,
|
||||
updateAllQueriesOperators,
|
||||
],
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user