Compare commits

..

9 Commits

Author SHA1 Message Date
nityanandagohain
a849eac876 fix: lint 2026-09-09 14:22:07 +05:30
nityanandagohain
5258f5e8e3 fix: more cleanup 2026-09-09 14:13:27 +05:30
nityanandagohain
8292988751 Merge remote-tracking branch 'origin/main' into issue_5329 2026-09-09 13:28:46 +05:30
nityanandagohain
6e96a5ce37 feat: support for default attribute mapping 2026-09-09 13:27:25 +05:30
Srikanth Chekuri
d5af6f6d6b feat(telemetrymetadata): fields keys and values for the quick filters migration (#12794)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- `fields/values` returns `boolValues` for bool fields: static span
fields such as `has_error`, stored bool attributes (decided by a
row-existence check, so they take no limit slots), and keys the caller
declares bool. `isRoot` and `isEntryPoint` return `true` only, matching
the condition builder.
- `fields/values` honors `startUnixMilli` for logs and traces against
the tag tables. The predicate is `toDate(unix_milli / 1000) >=
toDate(start)`, the partition expression, because the tag tables
deduplicate a value per day and keep whichever hour was inserted last.
For traces this means "span started on or after the start's day", for
logs "ingested on or after the start's day". `endUnixMilli` is ignored.
- `fields/keys` honors `fieldContext` and `fieldDataType` for the static
intrinsic and calculated keys, matches them case-insensitively, types
`isRoot`/`isEntryPoint` as bool, gives meter keys a data type, filters
the traces keys table by tag type for the scope context, and no longer
returns early for the log and scope contexts (logs and audit).
- The span context keeps returning attribute keys, since queries spell
span attributes as `span.<name>`.
- Default quick filters name the span and log fields the way the fields
API serves them (`has_error` as a span bool, `name`, `duration_nano`,
`response_status_code`, `http_host`, `http_url`, `trace_id` as span
fields, `severity_text` as a log field). Migration 126 rewrites the
static fields of every stored quick filter the same way, whatever
context the legacy seeds gave them. Deprecated names such as `hasError`
are not served.
- The API-backed checkbox hook merges `boolValues` into its options; the
`has_error` title is kept.
- The fields/values handler no longer drops `boolValues` from the
response.


🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-09-08 22:11:27 +00:00
Vikrant Gupta
e7c48e964f fix(billing): keep spacing and link styling consistent when permissions are denied (#12801)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- The denied usage callout on the billing page keeps the same 8px gap to
the blocks below it that the usage table has, so the layout reads the
same with or without `subscription:read`.
- "pay the bill" in the failed-payment banner is an inline link again,
with a visible disabled state when the user lacks permission.

#### Additional Information

Part of SigNoz/platform-pod#3091.
2026-09-08 18:34:39 +00:00
Pandey
fe8c9d8cf4 chore: remove query progress tracking (#12784)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- The frontend websocket client for query progress
(`api/common/getQueryStats.ts`) was deleted in #9183, so
`/api/v3/query_progress` and `/ws/query_progress` have had no consumers
since.
- Removes the `query_progress` tracker package, the
`GetQueryProgressUpdates` handler, the `RegisterWebSocketPaths` route
group, and the `websocket.Upgrader` on `APIHandler`.
- Removes the clickhouse reader hooks that reported progress
(`ReportQueryStartForProgressTracking`, `SubscribeToQueryProgress`, and
the `clickhouse.WithProgress` callback in `GetTimeSeriesResultV3`),
along with the untyped `"queryId"` context value they relied on.
- Removes the `X-SIGNOZ-QUERY-ID` header the logs explorer still sent,
plus the now-dead `Sec-WebSocket-Protocol` entries in the CORS allowlist
and the `identn` tokenizer defaults. Livetail streams over SSE, so no
websocket endpoint remains.

#### Additional Information

- `gorilla/websocket` moves from a direct to an indirect dependency
(still pulled in by opamp-go).
- Dropping `Sec-WebSocket-Protocol` from `identn.tokenizer.headers`
changes a default in `conf/example.yaml`. It is only a token-extraction
source for websocket handshakes, of which there are none left, but
calling it out since it is a config-surface change.
2026-09-08 10:56:58 +00:00
dependabot[bot]
2ad3057fa6 chore(deps): bump google.golang.org/grpc from 1.82.1 to 1.83.1 in /scripts/promqltestcorpus (#12758)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from
1.82.1 to 1.83.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's
releases</a>.</em></p>
<blockquote>
<h2>Release 1.83.1</h2>
<h1>Security</h1>
<ul>
<li>xds/rbac: Fix a bug where nested <code>Principal</code> or
<code>Permission</code> rules with <code>:scheme</code> or
<code>grpc-</code> prefixed header matchers were not rejected, which
could cause DENY rules to fail open. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9258">#9258</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/nvxbug"><code>@​nvxbug</code></a></li>
</ul>
</li>
<li>xds/rbac: Fix a bug where the <code>host</code> header matcher was
not being replaced with <code>:authority</code> in nested
<code>Principal</code> or <code>Permission</code> rules. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9258">#9258</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/nvxbug"><code>@​nvxbug</code></a></li>
</ul>
</li>
<li>xds/rbac: Fix a bug where a header matcher whose name was not
lowercase, such as <code>X-Role</code>, matched no header, which could
cause DENY rules to fail open. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9332">#9332</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/alimony"><code>@​alimony</code></a></li>
</ul>
</li>
<li>xds/rbac: Fix a bug where a <code>:scheme</code> or
<code>grpc-</code> prefixed header matcher was accepted when its name
was not lowercase. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9332">#9332</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/alimony"><code>@​alimony</code></a></li>
</ul>
</li>
<li>xds/rbac: Fix a bug where a <code>Host</code> header matcher was not
replaced with <code>:authority</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9332">#9332</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/alimony"><code>@​alimony</code></a></li>
</ul>
</li>
</ul>
<h1>Performance</h1>
<ul>
<li>transport: Restrict memory overhead of buffering small data frames.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/9331">#9331</a>)</li>
</ul>
<h2>Release 1.83.0</h2>
<h1>Security</h1>
<ul>
<li>server: Stop reading from connections when flooded by HTTP/2 frames
to mitigate resource exhaustion. The default value for this limit is 100
frames, excluding DATA and HEADERS, and may be changed by setting
environment variable
<code>GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT</code>.</li>
<li>xds/rbac: Support <code>Metadata</code> and
<code>RequestedServerName</code> permissions matcher fields. If present
in a DENY rule, previously these would be ignored and fail-open.</li>
<li>xds/rbac: Fix panic when parsing unsupported fields in
<code>NotRule</code>/<code>NotId</code> permissions.</li>
<li>xds/rbac: Support the deprecated <code>source_ip</code> principal
identifier by treating it as equivalent to
<code>direct_remote_ip</code>.</li>
<li>xds: Fix panic when parsing route header matchers configured with
empty <code>exact_match</code>, <code>prefix_match</code>, or
<code>suffix_match</code> strings. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9223">#9223</a>)</li>
</ul>
<h1>New Features</h1>
<ul>
<li>xds/googlec2p: Enable DirectPath over Interconnect support for
on-premises clients via the <code>force-xds</code> target URI query
parameter. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9133">#9133</a>)</li>
<li>xds: Enable xDS configuration to control which fields get propagated
from ORCA backend metric reports to LRS load reports. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9145">#9145</a>)</li>
<li>authz: Add <code>OnPolicyUpdate</code> callback to
<code>FileWatcherOptions</code> to notify when an authz policy is loaded
or updated. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9142">#9142</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/hnefatl"><code>@​hnefatl</code></a></li>
</ul>
</li>
<li>xds: Add support for the GCP Authentication HTTP Filter, which
automatically fetches and attaches GCP Service Account Identity JWT
tokens to outgoing RPCs.
<ul>
<li>This feature can be enabled by setting environment variable
<code>GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER=true</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9119">#9119</a>)</li>
</ul>
</li>
<li>xds: Add support for xDS-based HTTP CONNECT proxies.
<ul>
<li>This feature can be enabled by setting environment variable
<code>GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT=true</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9151">#9151</a>)</li>
</ul>
</li>
<li>xds: Add support for <code>contains_match</code> in route header
matchers. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9223">#9223</a>)</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>credentials/alts: Fix panic when processing malformed frames by
validating that the message frame length exceeds the message type field
size. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9197">#9197</a>)</li>
<li>grpc: Fix compilation on Plan 9 targets (<code>GOOS=plan9</code>),
broken since v1.81.0. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9255">#9255</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/Yusufihsangorgel"><code>@​Yusufihsangorgel</code></a></li>
</ul>
</li>
</ul>
<h2>Release 1.82.2</h2>
<h1>Security</h1>
<ul>
<li>server: Reject requests missing both <code>:authority</code> and
<code>Host</code> headers with HTTP 400 and status
<code>Internal</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/pull/9365">grpc/grpc-go#9365</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1550d9e0cd"><code>1550d9e</code></a>
Change version to 1.83.1 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9336">#9336</a>)</li>
<li><a
href="ebba6f3f1b"><code>ebba6f3</code></a>
Cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/9258">#9258</a>
and <a
href="https://redirect.github.com/grpc/grpc-go/issues/9332">#9332</a>
into v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9335">#9335</a>)</li>
<li><a
href="8cfeca0e1e"><code>8cfeca0</code></a>
Cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/9331">#9331</a> to
v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9333">#9333</a>)</li>
<li><a
href="dec6951305"><code>dec6951</code></a>
Change version to 1.83.1-dev (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9229">#9229</a>)</li>
<li><a
href="4c226daff8"><code>4c226da</code></a>
Change version to 1.83.0 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9228">#9228</a>)</li>
<li><a
href="c198988aa9"><code>c198988</code></a>
Cherrypick 9223 into v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9279">#9279</a>)</li>
<li><a
href="8ce3ebf24a"><code>8ce3ebf</code></a>
Cherrypick PR 9255 into v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9263">#9263</a>)</li>
<li><a
href="e39384978c"><code>e393849</code></a>
Cherry-pick recent changes from master (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9240">#9240</a>)</li>
<li><a
href="2a112a82f5"><code>2a112a8</code></a>
authz: add onPolicyUpdate callback to authz file watcher (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9142">#9142</a>)</li>
<li><a
href="1a80fca960"><code>1a80fca</code></a>
vet: adds a check to disallow usage of regex.Compile in xDS code (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9216">#9216</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/grpc/grpc-go/compare/v1.82.1...v1.83.1">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 09:33:33 +00:00
Aditya Singh
9fc73fc0b1 feat(quick-filters): migrate keys + save/load to new field APIs (#12717)
#### Description

- quick filter keys now come from fields/keys for all pages (logs,
traces, exceptions, api monitoring, meter)..replaces the old v3
filter_suggestions and aggregate attribute_keys calls
- save/load moved to the new v2 /me/filters api that stores
TelemetryFieldKey (name, fieldContext, fieldDataType). removed the old
hand written v1 clients and types
- filters are now identified by name + fieldContext + fieldDataType so
same name fields with different context/datatype don't clash. kept this
contained to the quick filter settings
- values still use the old v3 path for now..that migration (checkboxv2 /
fields/values) is the next PR

#### Screen recording


https://github.com/user-attachments/assets/130c8054-9f7d-4d8d-b224-62da7ccaf41e



#### Issues closed by this PR

Part of https://github.com/SigNoz/engineering-pod/issues/5979
2026-09-08 08:41:06 +00:00
145 changed files with 4789 additions and 2316 deletions

View File

@@ -397,7 +397,6 @@ identn:
# headers to use for tokenizer identN resolver
headers:
- Authorization
- Sec-WebSocket-Protocol
apikey:
# toggle apikey identN
enabled: true

View File

@@ -9427,6 +9427,8 @@ components:
type: string
name:
type: string
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
updatedAt:
format: date-time
type: string
@@ -9439,6 +9441,7 @@ components:
- fieldContext
- config
- enabled
- origin
type: object
SpantypesSpanMapperConfig:
properties:
@@ -9467,48 +9470,75 @@ components:
type: string
orgId:
type: string
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
version:
type: integer
required:
- id
- orgId
- name
- condition
- enabled
- origin
- version
type: object
SpantypesSpanMapperGroupCondition:
nullable: true
properties:
attributes:
items:
type: string
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
nullable: true
type: array
resource:
items:
type: string
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
nullable: true
type: array
required:
- attributes
- resource
type: object
SpantypesSpanMapperGroupConditionKey:
properties:
enabled:
type: boolean
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
value:
type: string
required:
- value
- enabled
type: object
SpantypesSpanMapperOperation:
enum:
- move
- copy
type: string
SpantypesSpanMapperOrigin:
enum:
- user
- system
type: string
SpantypesSpanMapperSource:
properties:
context:
$ref: '#/components/schemas/SpantypesFieldContext'
enabled:
type: boolean
key:
type: string
operation:
$ref: '#/components/schemas/SpantypesSpanMapperOperation'
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
priority:
type: integer
required:
@@ -9516,6 +9546,7 @@ components:
- context
- operation
- priority
- enabled
type: object
SpantypesSpanMapperTestSpan:
properties:

View File

@@ -184,7 +184,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
@@ -197,7 +196,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
})
handler := c.Handler(r)

View File

@@ -10518,15 +10518,31 @@ export interface SpantypesGettableFlamegraphTraceDTO {
startTimestampMillis: number;
}
export enum SpantypesSpanMapperOriginDTO {
user = 'user',
system = 'system',
}
export interface SpantypesSpanMapperGroupConditionKeyDTO {
/**
* @type boolean
*/
enabled: boolean;
origin?: SpantypesSpanMapperOriginDTO;
/**
* @type string
*/
value: string;
}
export type SpantypesSpanMapperGroupConditionDTOAnyOf = {
/**
* @type array,null
*/
attributes: string[] | null;
attributes: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
/**
* @type array,null
*/
resource: string[] | null;
resource: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
};
/**
@@ -10562,6 +10578,7 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
*/
orgId: string;
origin: SpantypesSpanMapperOriginDTO;
/**
* @type string
* @format date-time
@@ -10571,6 +10588,10 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
*/
updatedBy?: string;
/**
* @type integer
*/
version: number;
}
export interface SpantypesGettableSpanMapperGroupsDTO {
@@ -10628,11 +10649,16 @@ export enum SpantypesSpanMapperOperationDTO {
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type boolean
*/
enabled: boolean;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
origin?: SpantypesSpanMapperOriginDTO;
/**
* @type integer
*/
@@ -10674,6 +10700,7 @@ export interface SpantypesSpanMapperDTO {
* @type string
*/
name: string;
origin: SpantypesSpanMapperOriginDTO;
/**
* @type string
* @format date-time

View File

@@ -1,25 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/quickFilters/getCustomFilters';
const getCustomFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const { signal } = props;
try {
const response = await axios.get(`/orgs/me/filters/${signal}`);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getCustomFilters;

View File

@@ -1,13 +0,0 @@
import axios from 'api';
import { AxiosError } from 'axios';
import { SuccessResponse } from 'types/api';
import { UpdateCustomFiltersProps } from 'types/api/quickFilters/updateCustomFilters';
const updateCustomFiltersAPI = async (
props: UpdateCustomFiltersProps,
): Promise<SuccessResponse<void> | AxiosError> =>
axios.put(`/orgs/me/filters`, {
...props.data,
});
export default updateCustomFiltersAPI;

View File

@@ -17,7 +17,6 @@ function InputWithLabel({
onChange,
className,
closeIcon,
disabled,
}: {
label: string;
initialValue?: string | number | null;
@@ -28,7 +27,6 @@ function InputWithLabel({
onChange: (value: string) => void;
className?: string;
closeIcon?: React.ReactNode;
disabled?: boolean;
}): JSX.Element {
const [inputValue, setInputValue] = useState<string>(
initialValue ? initialValue.toString() : '',
@@ -55,7 +53,6 @@ function InputWithLabel({
type={type}
value={inputValue}
onChange={handleChange}
disabled={disabled}
name={label.toLowerCase()}
data-testid={`input-${label}`}
/>

View File

@@ -1,18 +1,11 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderField } from './queryBuilderFields.types';
import {
mergeQueryBuilderFieldsConfig,
RAW_QUERY_FIELDS,
resolveQueryBuilderField,
} from './queryBuilderFields.utils';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import { clearPreviousQuery } from './QueryV2/previousQuery.utils';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
@@ -21,18 +14,12 @@ import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
import './QueryBuilderV2.styles.scss';
// Raw rows come from logs or spans; metrics only exist aggregated.
const RAW_QUERY_SIGNALS = [
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
];
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
panelType: newPanelType,
fieldsConfig,
allowedDataSources,
isRawQuery = false,
filterConfigs = {},
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
@@ -84,48 +71,55 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
};
}, []);
const resolvedConfig = useMemo(
() =>
mergeQueryBuilderFieldsConfig(
isRawQuery ? RAW_QUERY_FIELDS : undefined,
fieldsConfig,
),
[isRawQuery, fieldsConfig],
);
const additionalQueries = useMemo(
() =>
resolveQueryBuilderField(
QueryBuilderField.AdditionalQueries,
resolvedConfig,
),
[resolvedConfig],
);
const formula = useMemo(
() => resolveQueryBuilderField(QueryBuilderField.Formula, resolvedConfig),
[resolvedConfig],
);
const isMultiQueryAllowed = useMemo(
() => !additionalQueries.hidden && (!isRawQuery || showTraceOperator),
[additionalQueries.hidden, showTraceOperator, isRawQuery],
() => !isListViewPanel || showTraceOperator,
[showTraceOperator, isListViewPanel],
);
const queryDataSources = useMemo(
() => allowedDataSources ?? (isRawQuery ? RAW_QUERY_SIGNALS : undefined),
[allowedDataSources, isRawQuery],
);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] =
useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
};
// What the editor renders. A single-query builder edits the first query alone, so
// the query list beside it must not advertise ones there is no way to reach.
const renderedQueries = useMemo(
() =>
isMultiQueryAllowed
? currentQuery.builder.queryData
: currentQuery.builder.queryData.slice(0, 1),
[isMultiQueryAllowed, currentQuery.builder.queryData],
);
return config;
}, []);
const listViewTracesFilterConfigs: QueryBuilderProps['filterConfigs'] =
useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
limit: { isHidden: true, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
};
return config;
}, []);
const queryFilterConfigs = useMemo(() => {
if (isListViewPanel) {
return currentQuery.builder.queryData[0].dataSource === DataSource.TRACES
? listViewTracesFilterConfigs
: listViewLogFilterConfigs;
}
return filterConfigs;
}, [
isListViewPanel,
filterConfigs,
currentQuery.builder.queryData,
listViewLogFilterConfigs,
listViewTracesFilterConfigs,
]);
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
if (
@@ -151,46 +145,31 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
const shouldShowFooter = useMemo(
() =>
(!showOnlyWhereClause && !isListViewPanel) ||
(currentDataSource === DataSource.TRACES && showTraceOperator),
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
);
const showQueryList = useMemo(
() => (!showOnlyWhereClause && !isRawQuery) || showTraceOperator,
[isRawQuery, showOnlyWhereClause, showTraceOperator],
() => (!showOnlyWhereClause && !isListViewPanel) || showTraceOperator,
[isListViewPanel, showOnlyWhereClause, showTraceOperator],
);
const showFormula = useMemo(() => {
if (formula.hidden) {
return false;
}
if (currentDataSource === DataSource.TRACES) {
return !isRawQuery;
return !isListViewPanel;
}
return true;
}, [formula.hidden, isRawQuery, currentDataSource]);
}, [isListViewPanel, currentDataSource]);
const showAddTraceOperator = useMemo(
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
// Nothing left to add means no footer at all, rather than an empty bar under the
// last query.
const shouldShowFooter = useMemo(
() =>
(!additionalQueries.hidden || showFormula || showAddTraceOperator) &&
((!showOnlyWhereClause && !isRawQuery) ||
(currentDataSource === DataSource.TRACES && showTraceOperator)),
[
additionalQueries.hidden,
showFormula,
showAddTraceOperator,
isRawQuery,
showTraceOperator,
showOnlyWhereClause,
currentDataSource,
],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>): void => {
const target = e.target as HTMLElement | null;
@@ -220,8 +199,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
key={currentQuery.builder.queryData[0].queryName}
index={0}
query={currentQuery.builder.queryData[0]}
fieldsConfig={fieldsConfig}
allowedDataSources={queryDataSources}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
isMultiQueryAllowed={isMultiQueryAllowed}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
@@ -229,7 +208,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isRawQuery={isRawQuery}
isListViewPanel={isListViewPanel}
signalSource={currentQuery.builder.queryData[0].source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
@@ -237,14 +216,14 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
savePreviousQuery={savePreviousQuery}
/>
) : (
renderedQueries.map((query, index) => (
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
key={query.queryName}
index={index}
query={query}
fieldsConfig={fieldsConfig}
allowedDataSources={queryDataSources}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isMultiQueryAllowed={isMultiQueryAllowed}
isAvailableToDisable={false}
@@ -252,7 +231,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
hasTraceOperator={hasTraceOperator}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isRawQuery={isRawQuery}
isListViewPanel={isListViewPanel}
signalSource={query.source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
@@ -272,7 +251,14 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
return (
<div key={formula.queryName} className="qb-formula">
<Formula query={query} formula={formula} index={index} isQBV2 />
<Formula
filterConfigs={filterConfigs}
query={query}
formula={formula}
index={index}
isAdditionalFilterEnable={false}
isQBV2
/>
</div>
);
})}
@@ -281,13 +267,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
{shouldShowFooter && (
<QueryFooter
showAddQuery={!additionalQueries.hidden}
showAddFormula={showFormula}
isAddFormulaDisabled={formula.disabled}
addFormulaDisabledReason={formula.reason}
addNewBuilderQuery={addNewBuilderQuery}
isAddQueryDisabled={additionalQueries.disabled}
addQueryDisabledReason={additionalQueries.reason}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showAddTraceOperator}
@@ -296,8 +277,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
{hasTraceOperator && (
<TraceOperator
isRawQuery={isRawQuery}
fieldsConfig={resolvedConfig}
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
@@ -305,7 +285,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
{showQueryList && (
<div className="query-names-section">
{renderedQueries.map((query) => (
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">
{query.queryName}
</div>

View File

@@ -23,11 +23,6 @@
align-items: center;
justify-content: center;
gap: var(--margin-2);
&--disabled {
opacity: 0.45;
cursor: not-allowed;
}
}
}

View File

@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import cx from 'classnames';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -15,16 +14,6 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import {
QueryBuilderField,
QueryBuilderFieldsConfig,
} from '../../queryBuilderFields.types';
import {
mergeQueryBuilderFieldsConfig,
RAW_QUERY_FIELDS,
resolveQueryBuilderFields,
} from '../../queryBuilderFields.utils';
import HavingFilter from './HavingFilter/HavingFilter';
import { buildDefaultLegendFromGroupBy } from './utils';
@@ -33,37 +22,34 @@ import './QueryAddOns.styles.scss';
interface AddOn {
icon: React.ReactNode;
label: string;
key: QueryBuilderField;
key: string;
description?: string;
docLink?: string;
}
/** Fields the add-on bar does not own: each has its own control elsewhere in the query. */
type NonAddOnField =
| QueryBuilderField.Aggregation
| QueryBuilderField.StepInterval
| QueryBuilderField.Functions
| QueryBuilderField.Formula
| QueryBuilderField.AdditionalQueries;
// Omit rather than Partial, so a field added to the enum has to be placed on one side.
const ADD_ONS_KEYS_TO_QUERY_PATH: Omit<
Record<QueryBuilderField, string>,
NonAddOnField
> = {
[QueryBuilderField.GroupBy]: 'groupBy',
[QueryBuilderField.Having]: 'having.expression',
[QueryBuilderField.OrderBy]: 'orderBy',
[QueryBuilderField.Limit]: 'limit',
[QueryBuilderField.Legend]: 'legend',
[QueryBuilderField.ReduceTo]: 'reduceTo',
const ADD_ONS_KEYS = {
GROUP_BY: 'group_by',
HAVING: 'having',
ORDER_BY: 'order_by',
LIMIT: 'limit',
LEGEND_FORMAT: 'legend_format',
REDUCE_TO: 'reduce_to',
};
const ADD_ONS: AddOn[] = [
const ADD_ONS_KEYS_TO_QUERY_PATH = {
[ADD_ONS_KEYS.GROUP_BY]: 'groupBy',
[ADD_ONS_KEYS.HAVING]: 'having.expression',
[ADD_ONS_KEYS.ORDER_BY]: 'orderBy',
[ADD_ONS_KEYS.LIMIT]: 'limit',
[ADD_ONS_KEYS.LEGEND_FORMAT]: 'legend',
[ADD_ONS_KEYS.REDUCE_TO]: 'reduceTo',
};
const ADD_ONS = [
{
icon: <BarChart size={14} />,
label: 'Group By',
key: QueryBuilderField.GroupBy,
key: ADD_ONS_KEYS.GROUP_BY,
description:
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
docLink: 'https://signoz.io/docs/querying/aggregation-grouping/#grouping',
@@ -71,7 +57,7 @@ const ADD_ONS: AddOn[] = [
{
icon: <ScrollText size={14} />,
label: 'Having',
key: QueryBuilderField.Having,
key: ADD_ONS_KEYS.HAVING,
description:
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
docLink:
@@ -80,7 +66,7 @@ const ADD_ONS: AddOn[] = [
{
icon: <ScrollText size={14} />,
label: 'Order By',
key: QueryBuilderField.OrderBy,
key: ADD_ONS_KEYS.ORDER_BY,
description:
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
docLink:
@@ -89,7 +75,7 @@ const ADD_ONS: AddOn[] = [
{
icon: <ScrollText size={14} />,
label: 'Limit',
key: QueryBuilderField.Limit,
key: ADD_ONS_KEYS.LIMIT,
description:
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
docLink:
@@ -98,7 +84,7 @@ const ADD_ONS: AddOn[] = [
{
icon: <ScrollText size={14} />,
label: 'Legend format',
key: QueryBuilderField.Legend,
key: ADD_ONS_KEYS.LEGEND_FORMAT,
description:
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
docLink:
@@ -106,10 +92,10 @@ const ADD_ONS: AddOn[] = [
},
];
const REDUCE_TO: AddOn = {
const REDUCE_TO = {
icon: <ScrollText size={14} />,
label: 'Reduce to',
key: QueryBuilderField.ReduceTo,
key: ADD_ONS_KEYS.REDUCE_TO,
description:
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
docLink:
@@ -168,26 +154,26 @@ function TooltipContent({
function QueryAddOns({
query,
version,
isRawQuery,
isListViewPanel,
showReduceTo,
panelType,
index,
fieldsConfig,
isForTraceOperator = false,
}: {
query: IBuilderQuery;
version: string;
isRawQuery: boolean;
isListViewPanel: boolean;
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
fieldsConfig?: QueryBuilderFieldsConfig;
isForTraceOperator?: boolean;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
const initializedRef = useRef(false);
const prevAvailableKeysRef = useRef<Set<QueryBuilderField> | null>(null);
const prevAvailableKeysRef = useRef<Set<string> | null>(null);
const { handleChangeQueryData } = useQueryOperations({
index,
@@ -198,62 +184,40 @@ function QueryAddOns({
const { handleSetQueryData } = useQueryBuilder();
const supportedAddOns = useMemo((): AddOn[] => {
let addOns: AddOn[];
useEffect(() => {
if (isListViewPanel) {
setAddOns([]);
if (panelType === PANEL_TYPES.VALUE) {
addOns = ADD_ONS.filter((addOn) => addOn.key === QueryBuilderField.Legend);
} else if (query.dataSource === DataSource.METRICS) {
// Group by for metrics is offered by MetricsAggregateSection instead.
addOns = ADD_ONS.filter((addOn) => addOn.key !== QueryBuilderField.GroupBy);
} else {
addOns = [...ADD_ONS];
setSelectedViews([
ADD_ONS.find((addOn) => addOn.key === ADD_ONS_KEYS.ORDER_BY) as AddOn,
]);
return;
}
return showReduceTo ? [...addOns, REDUCE_TO] : addOns;
}, [panelType, query.dataSource, showReduceTo]);
let filteredAddOns: AddOn[];
if (panelType === PANEL_TYPES.VALUE) {
// Filter out all add-ons except legend format
filteredAddOns = ADD_ONS.filter(
(addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT,
);
} else {
filteredAddOns = Object.values(ADD_ONS);
const resolvedFields = useMemo(
() =>
resolveQueryBuilderFields(
supportedAddOns.map((addOn) => addOn.key),
mergeQueryBuilderFieldsConfig(
isRawQuery ? RAW_QUERY_FIELDS : undefined,
fieldsConfig,
),
),
[supportedAddOns, fieldsConfig, isRawQuery],
);
if (query.dataSource === DataSource.METRICS) {
// Filter out group_by for metrics data source (handled in MetricsAggregateSection)
filteredAddOns = filteredAddOns.filter(
(addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY,
);
}
}
const offeredAddOns = useMemo(
() =>
supportedAddOns.filter((addOn) => !resolvedFields.get(addOn.key)?.hidden),
[supportedAddOns, resolvedFields],
);
if (showReduceTo) {
filteredAddOns = [...filteredAddOns, REDUCE_TO];
}
setAddOns(filteredAddOns);
const pinnedAddOns = useMemo(
() => offeredAddOns.filter((addOn) => resolvedFields.get(addOn.key)?.pinned),
[offeredAddOns, resolvedFields],
);
const togglableAddOns = useMemo(
() => offeredAddOns.filter((addOn) => !resolvedFields.get(addOn.key)?.pinned),
[offeredAddOns, resolvedFields],
);
const isPinned = useCallback(
(key: QueryBuilderField): boolean => Boolean(resolvedFields.get(key)?.pinned),
[resolvedFields],
);
const isDisabled = useCallback(
(key: QueryBuilderField): boolean =>
Boolean(resolvedFields.get(key)?.disabled),
[resolvedFields],
);
useEffect(() => {
const availableAddOnKeys = new Set(offeredAddOns.map((a) => a.key));
const availableAddOnKeys = new Set(filteredAddOns.map((a) => a.key));
const previousKeys = prevAvailableKeysRef.current;
const hasAvailabilityItemsChanged =
previousKeys !== null &&
@@ -267,39 +231,27 @@ function QueryAddOns({
const activeAddOnKeys = new Set(
Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
.filter(([, path]) => hasValue(get(query, path)))
.map(([key]) => key as QueryBuilderField),
.map(([key]) => key),
);
// Initial seeding from query values on mount. A disabled field never opens.
// Initial seeding from query values on mount
setSelectedViews(
offeredAddOns.filter((addOn) => {
const resolved = resolvedFields.get(addOn.key);
return (
resolved?.pinned ||
(activeAddOnKeys.has(addOn.key) && !resolved?.disabled)
);
}),
filteredAddOns.filter(
(addOn) =>
activeAddOnKeys.has(addOn.key) && availableAddOnKeys.has(addOn.key),
),
);
return;
}
setSelectedViews((prev) => {
const kept = prev.filter((view) => availableAddOnKeys.has(view.key));
const reopenedPinned = pinnedAddOns.filter(
(addOn) => !kept.some((view) => view.key === addOn.key),
);
return [...kept, ...reopenedPinned];
});
}, [offeredAddOns, pinnedAddOns, query]);
setSelectedViews((prev) =>
prev.filter((view) =>
filteredAddOns.some((addOn) => addOn.key === view.key),
),
);
}, [panelType, isListViewPanel, query, showReduceTo]);
const handleOptionClick = (clickedAddOn: AddOn): void => {
if (isDisabled(clickedAddOn.key)) {
return;
}
const isAlreadySelected = selectedViews.some(
(view) => view.key === clickedAddOn.key,
);
@@ -313,7 +265,7 @@ function QueryAddOns({
// and existing group-by keys, prefill the legend using all group-by keys.
// This keeps existing custom legends intact and only helps seed a sensible default.
if (
clickedAddOn.key === QueryBuilderField.Legend &&
clickedAddOn.key === ADD_ONS_KEYS.LEGEND_FORMAT &&
isEmpty(query?.legend) &&
Array.isArray(query.groupBy) &&
query.groupBy.length > 0
@@ -358,16 +310,9 @@ function QueryAddOns({
[handleSetQueryData, index, query],
);
const handleRemoveView = useCallback(
(key: QueryBuilderField): void => {
if (isPinned(key)) {
return;
}
setSelectedViews((prev) => prev.filter((view) => view.key !== key));
},
[isPinned],
);
const handleRemoveView = useCallback((key: string): void => {
setSelectedViews((prev) => prev.filter((view) => view.key !== key));
}, []);
const handleChangeQueryLegend = useCallback(
(value: string) => {
@@ -396,7 +341,7 @@ function QueryAddOns({
<div className="query-add-ons" data-testid="query-add-ons">
{selectedViews.length > 0 && (
<div className="selected-add-ons-content">
{selectedViews.find((view) => view.key === QueryBuilderField.GroupBy) && (
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content" data-testid="group-by-content">
<div className="periscope-input-with-label">
<Tooltip
@@ -424,17 +369,15 @@ function QueryAddOns({
onChange={handleChangeGroupByKeys}
/>
</div>
{!isPinned(QueryBuilderField.GroupBy) && (
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={(): void => handleRemoveView(QueryBuilderField.GroupBy)}
/>
)}
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={(): void => handleRemoveView('group_by')}
/>
</div>
</div>
)}
{selectedViews.find((view) => view.key === QueryBuilderField.Having) && (
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content" data-testid="having-content">
<div className="periscope-input-with-label">
<Tooltip
@@ -454,7 +397,11 @@ function QueryAddOns({
</Tooltip>
<div className="input">
<HavingFilter
onClose={(): void => handleRemoveView(QueryBuilderField.Having)}
onClose={(): void => {
setSelectedViews((prev) =>
prev.filter((view) => view.key !== 'having'),
);
}}
onChange={handleChangeHaving}
queryData={query}
/>
@@ -462,7 +409,7 @@ function QueryAddOns({
</div>
</div>
)}
{selectedViews.find((view) => view.key === QueryBuilderField.Limit) && (
{selectedViews.find((view) => view.key === 'limit') && (
<div className="add-on-content" data-testid="limit-content">
<InputWithLabel
label="Limit"
@@ -470,12 +417,16 @@ function QueryAddOns({
onChange={handleChangeLimit}
initialValue={query?.limit ?? undefined}
placeholder="Enter limit"
onClose={(): void => handleRemoveView(QueryBuilderField.Limit)}
onClose={(): void => {
setSelectedViews((prev) =>
prev.filter((view) => view.key !== 'limit'),
);
}}
closeIcon={<ChevronUp size={16} />}
/>
</div>
)}
{selectedViews.find((view) => view.key === QueryBuilderField.OrderBy) && (
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content" data-testid="order-by-content">
<div className="periscope-input-with-label">
<Tooltip
@@ -498,22 +449,22 @@ function QueryAddOns({
entityVersion={version}
query={query}
onChange={handleChangeOrderByKeys}
isRawQuery={isRawQuery}
isListViewPanel={isListViewPanel}
isNewQueryV2
/>
</div>
{!isPinned(QueryBuilderField.OrderBy) && (
{!isListViewPanel && (
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={(): void => handleRemoveView(QueryBuilderField.OrderBy)}
onClick={(): void => handleRemoveView('order_by')}
/>
)}
</div>
</div>
)}
{selectedViews.find((view) => view.key === QueryBuilderField.ReduceTo) &&
{selectedViews.find((view) => view.key === 'reduce_to') &&
showReduceTo && (
<div className="add-on-content" data-testid="reduce-to-content">
<div className="periscope-input-with-label">
@@ -536,25 +487,27 @@ function QueryAddOns({
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
</div>
{!isPinned(QueryBuilderField.ReduceTo) && (
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={(): void => handleRemoveView(QueryBuilderField.ReduceTo)}
/>
)}
<Button
className="close-btn periscope-btn ghost"
icon={<ChevronUp size={16} />}
onClick={(): void => handleRemoveView('reduce_to')}
/>
</div>
</div>
)}
{selectedViews.find((view) => view.key === QueryBuilderField.Legend) && (
{selectedViews.find((view) => view.key === 'legend_format') && (
<div className="add-on-content" data-testid="legend-format-content">
<InputWithLabel
label="Legend format"
placeholder="Write legend format"
onChange={handleChangeQueryLegend}
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
onClose={(): void => handleRemoveView(QueryBuilderField.Legend)}
onClose={(): void => {
setSelectedViews((prev) =>
prev.filter((view) => view.key !== 'legend_format'),
);
}}
closeIcon={<ChevronUp size={16} />}
/>
</div>
@@ -567,49 +520,42 @@ function QueryAddOns({
className="add-ons-tabs"
value={selectedViews.map((view) => view.key)}
onChange={(newKeys: string[]): void => {
const oldKeys: string[] = selectedViews.map((view) => view.key);
const oldKeys = selectedViews.map((view) => view.key);
const toggledKey =
newKeys.find((key) => !oldKeys.includes(key)) ??
oldKeys.find((key) => !newKeys.includes(key));
newKeys.find((k) => !oldKeys.includes(k)) ??
oldKeys.find((k) => !newKeys.includes(k));
if (!toggledKey) {
return;
}
const clickedAddOn = togglableAddOns.find((a) => a.key === toggledKey);
const clickedAddOn = addOns.find((a) => a.key === toggledKey);
if (clickedAddOn) {
handleOptionClick(clickedAddOn);
}
}}
items={togglableAddOns.map((addOn) => {
const resolved = resolvedFields.get(addOn.key);
return {
value: addOn.key,
label: (
<Tooltip
title={
<TooltipContent
label={addOn.label}
description={resolved?.reason ?? addOn.description}
docLink={resolved?.disabled ? undefined : addOn.docLink}
/>
}
placement="top"
mouseEnterDelay={0.5}
items={addOns.map((addOn) => ({
value: addOn.key,
label: (
<Tooltip
title={
<TooltipContent
label={addOn.label}
description={addOn.description}
docLink={addOn.docLink}
/>
}
placement="top"
mouseEnterDelay={0.5}
>
<span
className="add-on-tab-title"
data-testid={`query-add-on-${addOn.key}`}
>
<span
className={cx('add-on-tab-title', {
'add-on-tab-title--disabled': resolved?.disabled,
})}
aria-disabled={resolved?.disabled}
data-testid={`query-add-on-${addOn.key}`}
>
{addOn.icon}
{addOn.label}
</span>
</Tooltip>
),
};
})}
{addOn.icon}
{addOn.label}
</span>
</Tooltip>
),
}))}
/>
</div>
);

View File

@@ -8,12 +8,6 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
QueryBuilderField,
QueryBuilderFieldsConfig,
} from '../../queryBuilderFields.types';
import { resolveQueryBuilderField } from '../../queryBuilderFields.utils';
import QueryAggregationSelect from './QueryAggregationSelect';
import './QueryAggregation.styles.scss';
@@ -24,32 +18,24 @@ function QueryAggregationOptions({
onAggregationIntervalChange,
onChange,
queryData,
fieldsConfig,
}: {
dataSource: DataSource;
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery | IBuilderTraceOperator;
fieldsConfig?: QueryBuilderFieldsConfig;
}): JSX.Element {
const stepInterval = useMemo(() => {
const showAggregationInterval = useMemo(() => {
if (panelType === PANEL_TYPES.VALUE) {
return { hidden: true, disabled: false, reason: undefined };
return false;
}
const isNonMetricSource =
dataSource === DataSource.TRACES || dataSource === DataSource.LOGS;
if (
isNonMetricSource &&
(panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.PIE)
) {
return { hidden: true, disabled: false, reason: undefined };
if (dataSource === DataSource.TRACES || dataSource === DataSource.LOGS) {
return !(panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.PIE);
}
return resolveQueryBuilderField(QueryBuilderField.StepInterval, fieldsConfig);
}, [dataSource, panelType, fieldsConfig]);
return true;
}, [dataSource, panelType]);
const handleAggregationIntervalChange = (value: string): void => {
onAggregationIntervalChange(Number(value));
@@ -71,24 +57,22 @@ function QueryAggregationOptions({
}
/>
{!stepInterval.hidden && (
{showAggregationInterval && (
<div className="query-aggregation-interval">
<Tooltip
title={
stepInterval.reason ?? (
<div>
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn about step intervals
</a>
</div>
)
<div>
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
>
Learn about step intervals
</a>
</div>
}
placement="top"
>
@@ -108,7 +92,6 @@ function QueryAggregationOptions({
placeholder="Auto"
type="number"
onChange={handleAggregationIntervalChange}
disabled={stepInterval.disabled}
labelAfter
/>
</div>
@@ -122,7 +105,6 @@ function QueryAggregationOptions({
QueryAggregationOptions.defaultProps = {
panelType: null,
onChange: undefined,
fieldsConfig: undefined,
};
export default QueryAggregationOptions;

View File

@@ -17,13 +17,13 @@ function TraceOperatorSection({
const { currentQuery, panelType } = useQueryBuilder();
const showTraceOperatorWarning = useMemo(() => {
const isRawQueryPanel =
const isListViewPanel =
panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE;
const hasMultipleQueries = currentQuery.builder.queryData.length > 1;
const hasTraceOperator =
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0;
return isRawQueryPanel && hasMultipleQueries && !hasTraceOperator;
return isListViewPanel && hasMultipleQueries && !hasTraceOperator;
}, [
currentQuery?.builder?.queryData,
currentQuery?.builder?.queryTraceOperator,
@@ -77,74 +77,50 @@ export default function QueryFooter({
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
showAddQuery = true,
showAddFormula = true,
showAddTraceOperator = false,
isAddQueryDisabled = false,
addQueryDisabledReason,
isAddFormulaDisabled = false,
addFormulaDisabledReason,
}: {
addNewBuilderQuery: () => void;
addNewFormula: () => void;
addTraceOperator?: () => void;
showAddTraceOperator: boolean;
showAddQuery?: boolean;
showAddFormula?: boolean;
isAddQueryDisabled?: boolean;
addQueryDisabledReason?: string;
isAddFormulaDisabled?: boolean;
addFormulaDisabledReason?: string;
}): JSX.Element {
return (
<div className="qb-footer">
<div className="qb-footer-container">
{showAddQuery && (
<div className="qb-add-new-query">
<Tooltip
title={
addQueryDisabledReason ?? (
<div style={{ textAlign: 'center' }}>Add New Query</div>
)
}
>
<Button
className="add-new-query-button periscope-btn "
data-testid="add-new-query-button"
icon={<Plus size={16} />}
onClick={addNewBuilderQuery}
disabled={isAddQueryDisabled}
/>
</Tooltip>
</div>
)}
<div className="qb-add-new-query">
<Tooltip title={<div style={{ textAlign: 'center' }}>Add New Query</div>}>
<Button
className="add-new-query-button periscope-btn "
icon={<Plus size={16} />}
onClick={addNewBuilderQuery}
/>
</Tooltip>
</div>
{showAddFormula && (
<div className="qb-add-formula">
<Tooltip
title={
addFormulaDisabledReason ?? (
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/querying/multi-query-analysis/#advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
)
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/querying/multi-query-analysis/#advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-formula-button periscope-btn "
data-testid="add-formula-button"
icon={<Sigma size={16} />}
onClick={addNewFormula}
disabled={isAddFormulaDisabled}
>
Add Formula
</Button>

View File

@@ -20,13 +20,6 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { HandleChangeQueryDataV5 } from 'types/common/operations.types';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderField } from '../queryBuilderFields.types';
import {
mergeQueryBuilderFieldsConfig,
RAW_QUERY_FIELDS,
resolveQueryBuilderField,
} from '../queryBuilderFields.utils';
import MetricsAggregateSection from './MerticsAggregateSection/MetricsAggregateSection';
import { MetricsSelect } from './MetricsSelect/MetricsSelect';
import QueryAddOns from './QueryAddOns/QueryAddOns';
@@ -38,7 +31,8 @@ export const QueryV2 = forwardRef(function QueryV2(
index,
queryVariant,
query,
isRawQuery = false,
filterConfigs,
isListViewPanel = false,
showTraceOperator = false,
hasTraceOperator = false,
version,
@@ -49,8 +43,6 @@ export const QueryV2 = forwardRef(function QueryV2(
signalSourceChangeEnabled = false,
queriesCount = 1,
savePreviousQuery = false,
fieldsConfig,
allowedDataSources,
}: QueryProps & {
onSignalSourceChange: (value: string) => void;
signalSourceChangeEnabled: boolean;
@@ -61,7 +53,7 @@ export const QueryV2 = forwardRef(function QueryV2(
): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
const hasQueryFunctions = query?.functions?.length > 0;
const showFunctions = query?.functions?.length > 0;
const { dataSource, builderQueryType } = query;
const [isCollapsed, setIsCollapsed] = useState(false);
@@ -74,7 +66,8 @@ export const QueryV2 = forwardRef(function QueryV2(
} = useQueryOperations({
index,
query,
isRawQuery,
filterConfigs,
isListViewPanel,
entityVersion: version,
savePreviousQuery,
});
@@ -106,31 +99,14 @@ export const QueryV2 = forwardRef(function QueryV2(
[dataSource, builderQueryType],
);
const resolvedConfig = useMemo(
() =>
mergeQueryBuilderFieldsConfig(
isRawQuery ? RAW_QUERY_FIELDS : undefined,
fieldsConfig,
),
[isRawQuery, fieldsConfig],
);
const aggregation = useMemo(
() => resolveQueryBuilderField(QueryBuilderField.Aggregation, resolvedConfig),
[resolvedConfig],
);
const functions = useMemo(
() => resolveQueryBuilderField(QueryBuilderField.Functions, resolvedConfig),
[resolvedConfig],
);
const showInlineQuerySearch = useMemo(() => {
if (!showTraceOperator) {
return false;
}
return dataSource === DataSource.TRACES && (hasTraceOperator || isRawQuery);
}, [hasTraceOperator, isRawQuery, showTraceOperator, dataSource]);
return (
dataSource === DataSource.TRACES && (hasTraceOperator || isListViewPanel)
);
}, [hasTraceOperator, isListViewPanel, showTraceOperator, dataSource]);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
@@ -173,15 +149,12 @@ export const QueryV2 = forwardRef(function QueryV2(
hasTraceOperator={hasTraceOperator}
isMetricsDataSource={dataSource === DataSource.METRICS}
showFunctions={
!functions.hidden &&
((version && version === ENTITY_VERSION_V4) ||
query.dataSource === DataSource.LOGS ||
query.dataSource === DataSource.METRICS ||
hasQueryFunctions ||
false)
(version && version === ENTITY_VERSION_V4) ||
query.dataSource === DataSource.LOGS ||
query.dataSource === DataSource.METRICS ||
showFunctions ||
false
}
areFunctionsDisabled={functions.disabled}
functionsDisabledReason={functions.reason}
isCollapsed={isCollapsed}
showTraceOperator={showTraceOperator}
entityType="query"
@@ -194,8 +167,7 @@ export const QueryV2 = forwardRef(function QueryV2(
onQueryFunctionsUpdates={handleQueryFunctionsUpdates}
showDeleteButton={false}
showCloneOption={false}
isRawQuery={isRawQuery}
allowedDataSources={allowedDataSources}
isListViewPanel={isListViewPanel}
index={index}
queryVariant={queryVariant}
onChangeDataSource={handleChangeDataSource}
@@ -295,7 +267,7 @@ export const QueryV2 = forwardRef(function QueryV2(
</div>
{!showOnlyWhereClause &&
!aggregation.hidden &&
!isListViewPanel &&
!(hasTraceOperator && dataSource === DataSource.TRACES) &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
@@ -305,7 +277,6 @@ export const QueryV2 = forwardRef(function QueryV2(
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={query}
fieldsConfig={fieldsConfig}
/>
)}
@@ -326,10 +297,9 @@ export const QueryV2 = forwardRef(function QueryV2(
index={index}
query={query}
version="v3"
isRawQuery={isRawQuery}
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
fieldsConfig={fieldsConfig}
/>
)}
</div>

View File

@@ -11,7 +11,6 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderFieldsConfig } from '../../queryBuilderFields.types';
import QueryAddOns from '../QueryAddOns/QueryAddOns';
import QueryAggregation from '../QueryAggregation/QueryAggregation';
import TraceOperatorEditor from './TraceOperatorEditor';
@@ -20,12 +19,10 @@ import './TraceOperator.styles.scss';
export default function TraceOperator({
traceOperator,
isRawQuery = false,
fieldsConfig,
isListViewPanel = false,
}: {
traceOperator: IBuilderTraceOperator;
isRawQuery?: boolean;
fieldsConfig?: QueryBuilderFieldsConfig;
isListViewPanel?: boolean;
}): JSX.Element {
const { panelType, removeTraceOperator } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
@@ -61,12 +58,12 @@ export default function TraceOperator({
);
return (
<div className={cx('qb-trace-operator', !isRawQuery && 'non-list-view')}>
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
<div className="qb-trace-operator-container">
<div
className={cx(
'qb-trace-operator-label-with-input',
!isRawQuery && 'qb-trace-operator-arrow',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<Typography.Text className="label">Trace Operator</Typography.Text>
@@ -79,9 +76,9 @@ export default function TraceOperator({
</div>
</div>
{!isRawQuery && (
{!isListViewPanel && (
<div className="qb-trace-operator-aggregation-container">
<div className={cx(!isRawQuery && 'qb-trace-operator-arrow')}>
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
<QueryAggregation
dataSource={DataSource.TRACES}
key={`query-search-${traceOperator.queryName}`}
@@ -89,13 +86,12 @@ export default function TraceOperator({
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={traceOperator}
fieldsConfig={fieldsConfig}
/>
</div>
<div
className={cx(
'qb-trace-operator-add-ons-container',
!isRawQuery && 'qb-trace-operator-arrow',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<QueryAddOns
@@ -103,10 +99,9 @@ export default function TraceOperator({
query={traceOperator}
version="v3"
isForTraceOperator
isRawQuery={false}
isListViewPanel={false}
showReduceTo={false}
panelType={panelType}
fieldsConfig={fieldsConfig}
/>
</div>
</div>

View File

@@ -142,6 +142,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
isMetricsDataSource: false,
operators: [],
spaceAggregationOptions: [],
listOfAdditionalFilters: [],
handleChangeOperator: jest.fn(),
handleSpaceAggregationChange: jest.fn(),
handleChangeAggregatorAttribute: jest.fn(),
@@ -151,6 +152,7 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
jest.fn() as unknown as ReturnType<UseQueryOperations>['handleChangeQueryData'],
handleChangeFormulaData: jest.fn(),
handleQueryFunctionsUpdates: handleQueryFunctionsUpdatesMock,
listOfAdditionalFormulaFilters: [],
});
});

View File

@@ -95,7 +95,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={baseQuery()}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo
panelType={PANEL_TYPES.VALUE}
index={0}
@@ -119,7 +119,7 @@ describe('QueryAddOns', () => {
groupBy: ['service.name'],
})}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
@@ -135,7 +135,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={baseQuery()}
version="v5"
isRawQuery
isListViewPanel
showReduceTo={false}
panelType={PANEL_TYPES.LIST}
index={0}
@@ -151,7 +151,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={baseQuery({ limit: 5 })}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
@@ -176,7 +176,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={query}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
@@ -195,7 +195,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={baseQuery()}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
@@ -211,7 +211,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={baseQuery({ reduceTo: ReduceOperators.SUM })}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
@@ -234,7 +234,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={query}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
@@ -286,7 +286,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={query}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
@@ -314,7 +314,7 @@ describe('QueryAddOns', () => {
<QueryAddOns
query={query}
version="v5"
isRawQuery={false}
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}

View File

@@ -1,46 +0,0 @@
import { render, screen } from 'tests/test-utils';
import QueryFooter from '../QueryV2/QueryFooter/QueryFooter';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: (): {
currentQuery: { builder: { queryData: unknown[] } };
panelType: string;
} => ({
currentQuery: { builder: { queryData: [] } },
panelType: 'time_series',
}),
}));
const noop = (): void => {};
describe('QueryFooter', () => {
it('offers both buttons by default', () => {
render(
<QueryFooter
addNewBuilderQuery={noop}
addNewFormula={noop}
showAddTraceOperator={false}
/>,
);
expect(screen.getByTestId('add-new-query-button')).toBeInTheDocument();
expect(screen.getByTestId('add-formula-button')).toBeInTheDocument();
});
// A kind whose request takes a single query (Heatmap) hides the button outright
// rather than disabling it — a query it adds is one the builder cannot render.
it('drops the Add New Query button when the caller withholds it', () => {
render(
<QueryFooter
addNewBuilderQuery={noop}
addNewFormula={noop}
showAddQuery={false}
showAddTraceOperator={false}
/>,
);
expect(screen.queryByTestId('add-new-query-button')).not.toBeInTheDocument();
expect(screen.getByTestId('add-formula-button')).toBeInTheDocument();
});
});

View File

@@ -1,140 +0,0 @@
import { QueryBuilderField } from '../queryBuilderFields.types';
import {
mergeQueryBuilderFieldsConfig,
RAW_QUERY_FIELDS,
resolveQueryBuilderField,
resolveQueryBuilderFields,
} from '../queryBuilderFields.utils';
const SUPPORTED = [
QueryBuilderField.GroupBy,
QueryBuilderField.Having,
QueryBuilderField.OrderBy,
QueryBuilderField.Limit,
QueryBuilderField.Legend,
];
describe('resolveQueryBuilderField', () => {
it('leaves an unconfigured field available', () => {
expect(resolveQueryBuilderField(QueryBuilderField.Having)).toStrictEqual({
hidden: false,
disabled: false,
pinned: false,
});
});
it('hides a field configured hidden', () => {
const resolved = resolveQueryBuilderField(QueryBuilderField.Having, {
[QueryBuilderField.Having]: { state: 'hidden' },
});
expect(resolved.hidden).toBe(true);
expect(resolved.disabled).toBe(false);
});
it('carries the reason through on a disabled field', () => {
const resolved = resolveQueryBuilderField(QueryBuilderField.Having, {
[QueryBuilderField.Having]: {
state: 'disabled',
reason: 'Having filters aggregated results.',
},
});
expect(resolved).toStrictEqual({
hidden: false,
disabled: true,
reason: 'Having filters aggregated results.',
pinned: false,
});
});
it('pins a field configured pinned', () => {
const resolved = resolveQueryBuilderField(QueryBuilderField.OrderBy, {
[QueryBuilderField.OrderBy]: { state: 'pinned' },
});
expect(resolved.pinned).toBe(true);
expect(resolved.hidden).toBe(false);
});
it('only ever resolves one state at a time', () => {
const resolved = resolveQueryBuilderField(QueryBuilderField.Limit, {
[QueryBuilderField.Limit]: { state: 'disabled', reason: 'why' },
});
expect([resolved.hidden, resolved.disabled, resolved.pinned]).toStrictEqual([
false,
true,
false,
]);
});
});
describe('resolveQueryBuilderFields', () => {
it('resolves every supported field and nothing else', () => {
const resolved = resolveQueryBuilderFields(SUPPORTED);
expect([...resolved.keys()]).toStrictEqual(SUPPORTED);
});
it('cannot widen beyond what the builder supports', () => {
const resolved = resolveQueryBuilderFields([QueryBuilderField.Legend], {
[QueryBuilderField.ReduceTo]: { state: 'pinned' },
});
expect(resolved.has(QueryBuilderField.ReduceTo)).toBe(false);
});
});
describe('mergeQueryBuilderFieldsConfig', () => {
it('returns the override when there is no baseline', () => {
const override = { [QueryBuilderField.Limit]: { state: 'hidden' } } as const;
expect(mergeQueryBuilderFieldsConfig(undefined, override)).toBe(override);
});
it('returns the baseline when there is no override', () => {
expect(mergeQueryBuilderFieldsConfig(RAW_QUERY_FIELDS, undefined)).toBe(
RAW_QUERY_FIELDS,
);
});
it('lets the override win per field, leaving the rest of the baseline intact', () => {
const merged = mergeQueryBuilderFieldsConfig(RAW_QUERY_FIELDS, {
[QueryBuilderField.Having]: { state: 'disabled', reason: 'no aggregation' },
});
expect(merged?.[QueryBuilderField.Having]).toStrictEqual({
state: 'disabled',
reason: 'no aggregation',
});
expect(merged?.[QueryBuilderField.GroupBy]).toStrictEqual({
state: 'hidden',
});
expect(merged?.[QueryBuilderField.OrderBy]).toStrictEqual({
state: 'pinned',
});
});
});
describe('RAW_QUERY_FIELDS', () => {
it('reduces an aggregate surface to a pinned order by', () => {
const resolved = resolveQueryBuilderFields(SUPPORTED, RAW_QUERY_FIELDS);
const visible = [...resolved.entries()]
.filter(([, field]) => !field.hidden)
.map(([key]) => key);
expect(visible).toStrictEqual([QueryBuilderField.OrderBy]);
expect(resolved.get(QueryBuilderField.OrderBy)?.pinned).toBe(true);
});
it('leaves additional queries alone, so trace matching still allows several', () => {
expect(
resolveQueryBuilderField(
QueryBuilderField.AdditionalQueries,
RAW_QUERY_FIELDS,
).hidden,
).toBe(false);
});
});

View File

@@ -1,36 +0,0 @@
/**
* Everything the query builder can surface.
*
* The per-query values double as the add-on identities the builder renders
* (`data-testid="query-add-on-<value>"`), so they are part of the DOM contract and must
* not be renamed to match the member names.
*/
export enum QueryBuilderField {
// Per query
Aggregation = 'aggregation',
StepInterval = 'step_interval',
Functions = 'functions',
GroupBy = 'group_by',
Having = 'having',
OrderBy = 'order_by',
Limit = 'limit',
Legend = 'legend_format',
ReduceTo = 'reduce_to',
// Builder level
Formula = 'formula',
AdditionalQueries = 'additional_queries',
}
/** `reason` is required on `disabled`: an inert control the user can see has to explain itself. */
export type QueryBuilderFieldRule =
| { state: 'hidden' }
| { state: 'disabled'; reason: string }
| { state: 'pinned' };
/**
* A caller's narrowing of the builder's surface. The builder works out which fields suit
* the current data source and panel type first; this can only take away from that set.
*/
export type QueryBuilderFieldsConfig = Partial<
Record<QueryBuilderField, QueryBuilderFieldRule>
>;

View File

@@ -1,88 +0,0 @@
import {
QueryBuilderField,
QueryBuilderFieldRule,
QueryBuilderFieldsConfig,
} from './queryBuilderFields.types';
export interface ResolvedQueryBuilderField {
hidden: boolean;
disabled: boolean;
reason?: string;
/** Rendered open, not dismissable, and kept out of the add-on toggle bar. */
pinned: boolean;
}
const AVAILABLE: ResolvedQueryBuilderField = {
hidden: false,
disabled: false,
pinned: false,
};
function fromRule(rule: QueryBuilderFieldRule): ResolvedQueryBuilderField {
switch (rule.state) {
case 'hidden':
return { hidden: true, disabled: false, pinned: false };
case 'disabled':
return {
hidden: false,
disabled: true,
reason: rule.reason,
pinned: false,
};
case 'pinned':
return { hidden: false, disabled: false, pinned: true };
default:
return AVAILABLE;
}
}
export function resolveQueryBuilderField(
field: QueryBuilderField,
config?: QueryBuilderFieldsConfig,
): ResolvedQueryBuilderField {
const rule = config?.[field];
return rule ? fromRule(rule) : AVAILABLE;
}
/**
* Fields absent from `supported` are hidden whatever the config says, so a config can
* only ever take away.
*/
export function resolveQueryBuilderFields(
supported: readonly QueryBuilderField[],
config?: QueryBuilderFieldsConfig,
): Map<QueryBuilderField, ResolvedQueryBuilderField> {
return new Map(
supported.map((field) => [field, resolveQueryBuilderField(field, config)]),
);
}
/**
* The surface a raw-row builder starts from, layered under a caller's own config.
* `AdditionalQueries` is deliberately absent — a raw trace builder still takes several
* queries when trace matching is on.
*/
export const RAW_QUERY_FIELDS: QueryBuilderFieldsConfig = {
[QueryBuilderField.Aggregation]: { state: 'hidden' },
[QueryBuilderField.StepInterval]: { state: 'hidden' },
[QueryBuilderField.Functions]: { state: 'hidden' },
[QueryBuilderField.GroupBy]: { state: 'hidden' },
[QueryBuilderField.Having]: { state: 'hidden' },
[QueryBuilderField.Limit]: { state: 'hidden' },
[QueryBuilderField.Legend]: { state: 'hidden' },
[QueryBuilderField.ReduceTo]: { state: 'hidden' },
[QueryBuilderField.Formula]: { state: 'hidden' },
[QueryBuilderField.OrderBy]: { state: 'pinned' },
};
export function mergeQueryBuilderFieldsConfig(
baseline: QueryBuilderFieldsConfig | undefined,
override: QueryBuilderFieldsConfig | undefined,
): QueryBuilderFieldsConfig | undefined {
if (!baseline) {
return override;
}
return override ? { ...baseline, ...override } : baseline;
}

View File

@@ -34,6 +34,7 @@ export function mockFieldsValuesAPI(response: {
relatedValues?: (string | null)[];
stringValues?: (string | null)[];
numberValues?: (number | null)[];
boolValues?: (boolean | null)[];
}): void {
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
@@ -46,6 +47,7 @@ export function mockFieldsValuesAPI(response: {
relatedValues: response.relatedValues ?? [],
stringValues: response.stringValues ?? [],
numberValues: response.numberValues ?? [],
boolValues: response.boolValues ?? [],
},
},
}),

View File

@@ -22,7 +22,10 @@ interface UseFieldValuesReturn {
isFetching: boolean;
}
const DATA_SOURCE_TO_SIGNAL: Record<DataSource, TelemetrytypesSignalDTO> = {
export const DATA_SOURCE_TO_SIGNAL: Record<
DataSource,
TelemetrytypesSignalDTO
> = {
[DataSource.METRICS]: TelemetrytypesSignalDTO.metrics,
[DataSource.TRACES]: TelemetrytypesSignalDTO.traces,
[DataSource.LOGS]: TelemetrytypesSignalDTO.logs,
@@ -89,8 +92,12 @@ export function useFieldValues({
values.numberValues
?.filter((value): value is number => value !== null && value !== undefined)
.map((value) => value.toString()) || [];
const boolValues =
values.boolValues
?.filter((value): value is boolean => value !== null && value !== undefined)
.map((value) => value.toString()) || [];
return [...stringValues, ...numberValues];
return [...stringValues, ...numberValues, ...boolValues];
}, [data]);
return { relatedValues, allValues, isLoading, isFetching };

View File

@@ -17,7 +17,7 @@ import { CSS } from '@dnd-kit/utilities';
import { Button } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical } from '@signozhq/icons';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
function SortableFilter({
filter,
@@ -25,13 +25,13 @@ function SortableFilter({
allowDrag,
allowRemove,
}: {
filter: FilterType;
onRemove: (filter: FilterType) => void;
filter: TelemetryFieldKey;
onRemove: (filter: TelemetryFieldKey) => void;
allowDrag: boolean;
allowRemove: boolean;
}): JSX.Element {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: filter.key });
useSortable({ id: filter.key as string });
const style = {
transform: CSS.Transform.toString(transform),
@@ -46,14 +46,14 @@ function SortableFilter({
>
<div {...attributes} {...listeners} className="drag-handle">
{allowDrag && <GripVertical size={16} />}
{filter.key}
{filter.name}
</div>
{allowRemove && (
<Button
className="remove-filter-btn periscope-btn"
size="small"
onClick={(): void => {
onRemove(filter as FilterType);
onRemove(filter);
}}
>
Remove
@@ -69,8 +69,8 @@ function AddedFilters({
setAddedFilters,
}: {
inputValue: string;
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
addedFilters: TelemetryFieldKey[];
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
}): JSX.Element {
const sensors = useSensors(useSensor(PointerSensor));
@@ -90,12 +90,12 @@ function AddedFilters({
const filteredAddedFilters = useMemo(
() =>
addedFilters.filter((filter) =>
filter.key.toLowerCase().includes(inputValue.toLowerCase()),
filter.name.toLowerCase().includes(inputValue.toLowerCase()),
),
[addedFilters, inputValue],
);
const handleRemoveFilter = (filter: FilterType): void => {
const handleRemoveFilter = (filter: TelemetryFieldKey): void => {
setAddedFilters((prev) => prev.filter((f) => f.key !== filter.key));
};
@@ -116,7 +116,7 @@ function AddedFilters({
<div className="no-values-found">No values found</div>
) : (
<SortableContext
items={addedFilters.map((f) => f.key)}
items={addedFilters.map((f) => f.key as string)}
strategy={verticalListSortingStrategy}
disabled={!allowDrag}
>

View File

@@ -1,17 +1,17 @@
import { useMemo } from 'react';
import { Button, Skeleton } from 'antd';
import { useGetFieldsKeys } from 'api/generated/services/fields';
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { DATA_SOURCE_TO_SIGNAL } from 'components/QuickFilters/FilterRenderers/Checkbox/v2/useFieldValues';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SignalType } from 'components/QuickFilters/types';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { DataSource } from 'types/common/queryBuilder';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import {
FieldContext,
FieldDataType,
TelemetryFieldKey,
} from 'types/api/v5/queryRange';
function OtherFiltersSkeleton(): JSX.Element {
return (
@@ -37,106 +37,48 @@ function OtherFilters({
}: {
signal: SignalType | undefined;
inputValue: string;
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
addedFilters: TelemetryFieldKey[];
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
}): JSX.Element {
const isLogDataSource = useMemo(
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
[signal],
);
const isMeterDataSource = useMemo(
() => signal && signal === SignalType.METER_EXPLORER,
[signal],
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
const { data, isFetching } = useGetFieldsKeys(
{
searchText: inputValue,
signal: signal
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
: undefined,
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
},
{ query: { enabled: !!signal } },
);
const { data: suggestionsData, isFetching: isFetchingSuggestions } =
useGetAttributeSuggestions(
{
searchText: inputValue,
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
filters: {} as TagFilter,
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && isLogDataSource,
},
);
const otherFilters = useMemo<TelemetryFieldKey[]>(() => {
const rawSuggestions = Object.values(data?.data?.keys ?? {}).flat();
// Normalize: synthesize the composite `key` once so downstream reads (dedupe,
// add, render) can trust it.
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
name: attr.name,
signal: attr.signal as TelemetryFieldKey['signal'],
fieldContext: attr.fieldContext as FieldContext,
fieldDataType: attr.fieldDataType as FieldDataType,
key: buildCompositeKey(attr.name, attr.fieldContext, attr.fieldDataType),
}));
const { data: aggregateKeysData, isFetching: isFetchingAggregateKeys } =
useGetAggregateKeys(
{
searchText: inputValue,
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
aggregateOperator: 'noop',
aggregateAttribute: '',
tagType: '',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && !isLogDataSource && !isMeterDataSource,
},
const addedKeys = new Set(
addedFilters.map((filter) =>
buildCompositeKey(filter.name, filter.fieldContext, filter.fieldDataType),
),
);
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
}, [data, addedFilters]);
const { data: fieldKeysData, isLoading: isLoadingFieldKeys } =
useGetQueryKeySuggestions(
{
searchText: inputValue,
signal: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
signalSource: 'meter',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && isMeterDataSource,
},
);
const otherFilters = useMemo(() => {
let filterAttributes;
if (isLogDataSource) {
filterAttributes = suggestionsData?.payload?.attributes || [];
} else if (isMeterDataSource) {
const fieldKeys: QueryKeyDataSuggestionsProps[] = Object.values(
fieldKeysData?.data?.data?.keys || {},
)?.flat();
filterAttributes = fieldKeys.map(
(attr) =>
({
key: attr.name,
dataType: attr.fieldDataType,
type: attr.fieldContext,
signal: attr.signal,
}) as BaseAutocompleteData,
);
} else {
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
}
return filterAttributes?.filter(
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
);
}, [
suggestionsData,
aggregateKeysData,
addedFilters,
isLogDataSource,
fieldKeysData,
isMeterDataSource,
]);
const handleAddFilter = (filter: FilterType): void => {
setAddedFilters((prev) => [
...prev,
{
key: filter.key,
dataType: filter.dataType,
type: filter.type,
},
]);
const handleAddFilter = (filter: TelemetryFieldKey): void => {
setAddedFilters((prev) => [...prev, filter]);
};
const renderFilters = (): React.ReactNode => {
const isLoading =
isFetchingSuggestions || isFetchingAggregateKeys || isLoadingFieldKeys;
if (isLoading) {
if (isFetching) {
return <OtherFiltersSkeleton />;
}
if (!otherFilters?.length) {
@@ -145,11 +87,11 @@ function OtherFilters({
return otherFilters.map((filter) => (
<div key={filter.key} className="qf-filter-item other-filters-item">
<div className="qf-filter-key">{filter.key}</div>
<div className="qf-filter-key">{filter.name}</div>
<Button
className="add-filter-btn periscope-btn"
size="small"
onClick={(): void => handleAddFilter(filter as FilterType)}
onClick={(): void => handleAddFilter(filter)}
>
Add
</Button>

View File

@@ -1,8 +1,7 @@
import { useMemo } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { SignalType } from '../types';
import AddedFilters from './AddedFilters';
@@ -19,7 +18,7 @@ function QuickFiltersSettings({
}: {
signal: SignalType | undefined;
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
customFilters: FilterType[];
customFilters: TelemetryFieldKey[];
refetchCustomFilters: () => void;
}): JSX.Element {
const {
@@ -28,6 +27,7 @@ function QuickFiltersSettings({
addedFilters,
setAddedFilters,
handleSaveChanges,
hasUnsavedChanges,
isUpdatingCustomFilters,
inputValue,
handleInputChange,
@@ -39,18 +39,6 @@ function QuickFiltersSettings({
signal,
});
const hasUnsavedChanges = useMemo(
() =>
// check if both arrays have the same length and same order of elements
!(
addedFilters.length === customFilters.length &&
addedFilters.every(
(filter, index) => filter.key === customFilters[index].key,
)
),
[addedFilters, customFilters],
);
return (
<>
<div className="qf-header">

View File

@@ -1,27 +1,31 @@
import { useCallback, useState } from 'react';
import { useMutation } from 'react-query';
import { useCallback, useMemo, useState } from 'react';
import { useUpdateQuickFilters } from 'api/generated/services/quick-filter';
import logEvent from 'api/common/logEvent';
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
import axios, { AxiosError } from 'axios';
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesFieldDataTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SignalType } from 'components/QuickFilters/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
interface UseQuickFilterSettingsProps {
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
customFilters: FilterType[];
customFilters: TelemetryFieldKey[];
refetchCustomFilters: () => void;
signal?: SignalType;
}
interface UseQuickFilterSettingsReturn {
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
addedFilters: TelemetryFieldKey[];
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
handleSettingsClose: () => void;
handleDiscardChanges: () => void;
handleSaveChanges: () => void;
hasUnsavedChanges: boolean;
isUpdatingCustomFilters: boolean;
inputValue: string;
setInputValue: React.Dispatch<React.SetStateAction<string>>;
@@ -37,27 +41,43 @@ const useQuickFilterSettings = ({
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
const [inputValue, setInputValue] = useState<string>('');
const [debouncedInputValue, setDebouncedInputValue] = useState<string>('');
const [addedFilters, setAddedFilters] = useState<FilterType[]>(customFilters);
const normalizedCustomFilters = useMemo<TelemetryFieldKey[]>(
() =>
customFilters.map((filter) => ({
...filter,
key: buildCompositeKey(
filter.name,
filter.fieldContext,
filter.fieldDataType,
),
})),
[customFilters],
);
const [addedFilters, setAddedFilters] = useState<TelemetryFieldKey[]>(
normalizedCustomFilters,
);
const { notifications } = useNotifications();
const { mutate: updateCustomFilters, isLoading: isUpdatingCustomFilters } =
useMutation(updateCustomFiltersAPI, {
onSuccess: () => {
setIsSettingsOpen(false);
refetchCustomFilters();
logEvent('Quick Filters Settings: changes saved', {
addedFilters,
});
notifications.success({
message: 'Quick filters updated successfully',
placement: 'bottomRight',
});
},
onError: (error: AxiosError) => {
notifications.error({
message: axios.isAxiosError(error) ? error.message : SOMETHING_WENT_WRONG,
placement: 'bottomRight',
});
useUpdateQuickFilters({
mutation: {
onSuccess: () => {
setIsSettingsOpen(false);
refetchCustomFilters();
void logEvent('Quick Filters Settings: changes saved', {
addedFilters,
});
notifications.success({
message: 'Quick filters updated successfully',
placement: 'bottomRight',
});
},
onError: (error) => {
notifications.error({
message: error.message || SOMETHING_WENT_WRONG,
placement: 'bottomRight',
});
},
},
});
const debouncedUpdate = useDebouncedFn((value) => {
@@ -78,19 +98,32 @@ const useQuickFilterSettings = ({
}, [setIsSettingsOpen]);
const handleDiscardChanges = useCallback((): void => {
setAddedFilters(customFilters);
}, [customFilters, setAddedFilters]);
setAddedFilters(normalizedCustomFilters);
}, [normalizedCustomFilters, setAddedFilters]);
const hasUnsavedChanges = useMemo(
() =>
!(
addedFilters.length === normalizedCustomFilters.length &&
addedFilters.every(
(filter, index) => filter.key === normalizedCustomFilters[index].key,
)
),
[addedFilters, normalizedCustomFilters],
);
const handleSaveChanges = useCallback((): void => {
if (signal) {
updateCustomFilters({
pathParams: { source: signal },
data: {
// Send only the stored TelemetryFieldKey fields; the composite `key`
// is UI-only.
filters: addedFilters.map((filter) => ({
key: filter.key,
datatype: filter.dataType,
type: filter.type,
name: filter.name,
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
})),
signal,
},
});
}
@@ -102,6 +135,7 @@ const useQuickFilterSettings = ({
addedFilters,
setAddedFilters,
handleSaveChanges,
hasUnsavedChanges,
isUpdatingCustomFilters,
inputValue,
setInputValue,

View File

@@ -1,8 +1,6 @@
import { useMemo } from 'react';
import { useQuery } from 'react-query';
import getCustomFilters from 'api/quickFilters/getCustomFilters';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { IQuickFiltersConfig, SignalType } from '../types';
import { getFilterConfig } from '../utils';
@@ -13,7 +11,7 @@ interface UseFilterConfigProps {
}
interface UseFilterConfigReturn {
filterConfig: IQuickFiltersConfig[];
customFilters: FilterType[];
customFilters: TelemetryFieldKey[];
isCustomFiltersLoading: boolean;
isDynamicFilters: boolean;
refetchCustomFilters: () => void;
@@ -25,17 +23,16 @@ const useFilterConfig = ({
}: UseFilterConfigProps): UseFilterConfigReturn => {
const {
isFetching: isCustomFiltersLoading,
data: customFilters = [],
data,
refetch,
} = useQuery<FilterType[], Error>(
[REACT_QUERY_KEY.GET_CUSTOM_FILTERS, signal],
async () => {
const res = await getCustomFilters({ signal: signal || '' });
return 'payload' in res && res.payload?.filters ? res.payload.filters : [];
},
{
enabled: !!signal,
},
} = useGetQuickFilters(
{ source: signal ?? '' },
{ query: { enabled: !!signal } },
);
const customFilters = useMemo<TelemetryFieldKey[]>(
() => (data?.data?.filters ?? []) as TelemetryFieldKey[],
[data],
);
const isDynamicFilters = useMemo(

View File

@@ -11,7 +11,7 @@ import {
} from 'mocks-server/__mockdata__/customQuickFilters';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import '@testing-library/jest-dom';
@@ -34,9 +34,9 @@ const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
const BASE_URL = ENVIRONMENT.baseURL;
const SIGNAL = SignalType.LOGS;
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v1/fields/keys`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
@@ -338,6 +338,63 @@ describe('Quick Filters with custom filters', () => {
);
});
it('keeps same-name fields with different context as distinct entries', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get(quickFiltersSuggestionsURL, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
complete: true,
keys: {
level: [
{
name: 'level',
fieldContext: 'attribute',
fieldDataType: 'string',
signal: 'logs',
},
{
name: 'level',
fieldContext: 'span',
fieldDataType: 'string',
signal: 'logs',
},
],
},
},
}),
),
),
);
render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
const otherSection = screen.getByText(OTHER_FILTERS_LABEL).parentElement!;
// Both `level` variants are shown despite sharing a name.
await waitFor(() =>
expect(within(otherSection).getAllByText('level')).toHaveLength(2),
);
// Adding one variant removes only that one; the other stays.
const firstLevel = within(otherSection).getAllByText('level')[0];
const addButton = firstLevel.parentElement?.querySelector('button');
await user.click(addButton as HTMLButtonElement);
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
await waitFor(() => {
expect(within(addedSection).getAllByText('level')).toHaveLength(1);
expect(within(otherSection).getAllByText('level')).toHaveLength(1);
});
});
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -456,12 +513,10 @@ describe('Quick Filters with custom filters', () => {
});
const requestBody = putHandler.mock.calls[0][0];
expect(requestBody.filters).toStrictEqual(
expect.arrayContaining([
expect.not.objectContaining({ key: FILTER_OS_DESCRIPTION }),
]),
expect(requestBody.filters).not.toContainEqual(
expect.objectContaining({ name: FILTER_OS_DESCRIPTION }),
);
expect(requestBody.signal).toBe(SIGNAL);
expect(requestBody.filters).toHaveLength(10);
});
it('should render duration slider for duration_nono filter', async () => {
@@ -612,9 +667,9 @@ describe('Quick Filters refetch behavior', () => {
filters: [
...(quickFiltersListResponse.data.filters ?? []),
{
key: 'new.custom.filter',
dataType: 'string',
type: 'resource',
name: 'new.custom.filter',
fieldDataType: 'string',
fieldContext: 'resource',
} as const,
],
},

View File

@@ -1,17 +1,33 @@
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
const FILTER_TITLE_MAP: Record<string, string> = {
duration_nano: 'Duration',
hasError: 'Has Error (Status)',
has_error: 'Has Error (Status)',
};
const FILTER_TYPE_MAP: Record<string, FiltersType> = {
duration_nano: FiltersType.DURATION,
};
// The map below exists only for the old v3 attribute-values fetch
// (useCheckboxFilterValues), the sole reader of attributeKey.dataType/type.
// Once the values fetch moves to fields/values, remove this and reduce
// attributeKey to { id, key }.
const FIELD_CONTEXT_TO_ATTRIBUTE_TYPE: Record<string, string> = {
[TelemetrytypesFieldContextDTO.attribute]: 'tag',
[TelemetrytypesFieldContextDTO.resource]: 'resource',
};
const mapFieldContext = (fieldContext?: string): string =>
(fieldContext && FIELD_CONTEXT_TO_ATTRIBUTE_TYPE[fieldContext]) || '';
const getFilterName = (str: string): string => {
if (FILTER_TITLE_MAP[str]) {
return FILTER_TITLE_MAP[str];
@@ -26,16 +42,16 @@ const getFilterName = (str: string): string => {
.join(' ');
};
const getFilterType = (att: FilterType): FiltersType => {
if (FILTER_TYPE_MAP[att.key]) {
return FILTER_TYPE_MAP[att.key];
const getFilterType = (att: TelemetryFieldKey): FiltersType => {
if (FILTER_TYPE_MAP[att.name]) {
return FILTER_TYPE_MAP[att.name];
}
return FiltersType.CHECKBOX;
};
export const getFilterConfig = (
signal?: SignalType,
customFilters?: FilterType[],
customFilters?: TelemetryFieldKey[],
config?: IQuickFiltersConfig[],
): IQuickFiltersConfig[] => {
if (!customFilters?.length || !signal) {
@@ -46,13 +62,13 @@ export const getFilterConfig = (
(att, index) =>
({
type: getFilterType(att),
title: getFilterName(att.key),
title: getFilterName(att.name),
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
attributeKey: {
id: att.key,
key: att.key,
dataType: att.dataType,
type: att.type,
id: att.name,
key: att.name,
dataType: fieldDataTypeToDataType(att.fieldDataType),
type: mapFieldContext(att.fieldContext),
},
defaultOpen: index < 2,
}) as IQuickFiltersConfig,

View File

@@ -32,6 +32,7 @@ import {
MeterAggregateOperator,
MetricAggregateOperator,
NumberOperators,
QueryAdditionalFilter,
QueryBuilderData,
ReduceOperators,
StringOperators,
@@ -103,6 +104,43 @@ export const metricsSpaceAggregationOperatorsByType = {
ExponentialHistogram: metricsHistogramSpaceAggregateOperatorOptions,
};
export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = {
metrics: [
{ text: 'Aggregation interval', field: 'stepInterval' },
{ text: 'Having', field: 'having' },
],
logs: [
{ text: 'Order by', field: 'orderBy' },
{ text: 'Limit', field: 'limit' },
{ text: 'Having', field: 'having' },
{ text: 'Aggregation interval', field: 'stepInterval' },
],
traces: [
{ text: 'Order by', field: 'orderBy' },
{ text: 'Limit', field: 'limit' },
{ text: 'Having', field: 'having' },
{ text: 'Aggregation interval', field: 'stepInterval' },
],
};
const commonFormulaFilters: QueryAdditionalFilter[] = [
{
text: 'Having',
field: 'having',
},
{ text: 'Order by', field: 'orderBy' },
{ text: 'Limit', field: 'limit' },
];
export const mapOfFormulaToFilters: Record<
DataSource,
QueryAdditionalFilter[]
> = {
metrics: commonFormulaFilters,
logs: commonFormulaFilters,
traces: commonFormulaFilters,
};
export const REDUCE_TO_VALUES: SelectOption<ReduceOperators, string>[] = [
{ value: ReduceOperators.LAST, label: 'Latest of values in timeframe' },
{ value: ReduceOperators.SUM, label: 'Sum of values in timeframe' },

View File

@@ -184,6 +184,11 @@
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
&[disabled] {
opacity: 0.6;
text-decoration: none;
}
}
.workspace-restricted-banner,

View File

@@ -16,7 +16,6 @@ import * as Sentry from '@sentry/react';
import { Toaster } from '@signozhq/ui/sonner';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import { Flex } from 'antd';
import { Button } from '@signozhq/ui/button';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
@@ -820,14 +819,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{' '}
Please{' '}
<AuthZTooltip checks={SubscriptionManagePermissions}>
<Button
variant="link"
color="none"
className="upgrade-link"
onClick={handleFailedPayment}
>
<a className="upgrade-link" onClick={handleFailedPayment}>
pay the bill
</Button>
</a>
</AuthZTooltip>
to continue using SigNoz features.
<span className="refresh-payment-status">

View File

@@ -60,6 +60,10 @@
margin: var(--spacing-12) var(--spacing-4);
}
.usageDenied {
margin-bottom: var(--spacing-4);
}
.billingDetails {
margin: var(--spacing-4) 0;
border: 1px solid var(--l1-border);

View File

@@ -37,6 +37,7 @@ import { isEmpty, pick } from 'lodash-es';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
import {
SubscriptionCreatePermission,
SubscriptionManagePermissions,
@@ -509,7 +510,15 @@ export default function BillingContainer(): JSX.Element {
))}
</Card>
<AuthZGuardContent checks={[SubscriptionReadPermission]}>
<AuthZGuardContent
checks={[SubscriptionReadPermission]}
fallback={({ deniedPermissions }): JSX.Element => (
<PermissionDeniedCallout
deniedPermissions={deniedPermissions}
className={styles.usageDenied}
/>
)}
>
<>
<div className={styles.billingGraphSection}>
{!isLoading && !isFetchingBillingData ? (

View File

@@ -333,6 +333,7 @@ describe('AttributeMappingsTab (integration)', () => {
context: FieldContext.attribute,
operation: MapperOperation.copy,
priority,
enabled: true,
})),
},
}),

View File

@@ -1,10 +1,12 @@
import { Typography } from '@signozhq/ui/typography';
import { ConditionKey } from 'container/LLMObservability/AttributeMapping/types';
import styles from './ConditionsTooltip.module.scss';
interface ConditionsTooltipProps {
attributes: string[];
resource: string[];
attributes: ConditionKey[];
resource: ConditionKey[];
}
function ConditionsTooltip({
@@ -33,8 +35,8 @@ function ConditionsTooltip({
</Typography.Text>
<div className={styles.keyList}>
{attributes.map((key) => (
<code key={key} className={styles.key}>
{key}
<code key={`${key.origin}-${key.value}`} className={styles.key}>
{key.value}
</code>
))}
</div>
@@ -47,8 +49,8 @@ function ConditionsTooltip({
</Typography.Text>
<div className={styles.keyList}>
{resource.map((key) => (
<code key={key} className={styles.key}>
{key}
<code key={`${key.origin}-${key.value}`} className={styles.key}>
{key.value}
</code>
))}
</div>

View File

@@ -3,6 +3,7 @@ import {
SpantypesSpanMapperDTO as Mapper,
SpantypesSpanMapperGroupDTO as MapperGroup,
SpantypesSpanMapperOperationDTO as MapperOperation,
SpantypesSpanMapperOriginDTO as MapperOrigin,
SpantypesSpanMapperTestSpanDTO as TestSpan,
} from 'api/generated/services/sigNoz.schemas';
@@ -21,9 +22,15 @@ export function makeGroup(overrides: Partial<MapperGroup> = {}): MapperGroup {
orgId: 'org-1',
name: 'demo',
enabled: true,
origin: MapperOrigin.user,
version: 0,
condition: {
attributes: ['ai.embeddings'],
resource: ['cloud.account.id'],
attributes: [
{ value: 'ai.embeddings', enabled: true, origin: MapperOrigin.user },
],
resource: [
{ value: 'cloud.account.id', enabled: true, origin: MapperOrigin.user },
],
},
...overrides,
};
@@ -35,6 +42,7 @@ export function makeMapper(overrides: Partial<Mapper> = {}): Mapper {
groupId: 'group-1',
name: 'gen_ai.request.model',
enabled: true,
origin: MapperOrigin.user,
fieldContext: FieldContext.attribute,
config: {
sources: [
@@ -43,12 +51,16 @@ export function makeMapper(overrides: Partial<Mapper> = {}): Mapper {
context: FieldContext.attribute,
operation: MapperOperation.copy,
priority: 2,
enabled: true,
origin: MapperOrigin.user,
},
{
key: 'llm.model',
context: FieldContext.attribute,
operation: MapperOperation.move,
priority: 1,
enabled: true,
origin: MapperOrigin.user,
},
],
},
@@ -85,8 +97,12 @@ export const mockGroups: MapperGroup[] = [
id: 'group-1',
name: 'demo',
condition: {
attributes: ['ai.embeddings'],
resource: ['cloud.account.id'],
attributes: [
{ value: 'ai.embeddings', enabled: true, origin: MapperOrigin.user },
],
resource: [
{ value: 'cloud.account.id', enabled: true, origin: MapperOrigin.user },
],
},
}),
makeGroup({

View File

@@ -1,19 +1,23 @@
import { Button } from '@signozhq/ui/button';
import { Plus, X } from '@signozhq/icons';
import { FieldContextValue } from 'container/LLMObservability/AttributeMapping/types';
import {
ConditionKey,
FieldContextValue,
} from 'container/LLMObservability/AttributeMapping/types';
import { createConditionKey } from 'container/LLMObservability/AttributeMapping/utils';
import KeySearchInput from '../../../KeySearchInput/KeySearchInput';
import styles from './ConditionKeyList.module.scss';
interface ConditionKeyListProps {
label: string;
labelHint?: string;
keys: string[];
keys: ConditionKey[];
placeholder: string;
addLabel: string;
testIdPrefix: string;
fieldContext: FieldContextValue;
onChange: (keys: string[]) => void;
onChange: (keys: ConditionKey[]) => void;
}
function ConditionKeyList({
@@ -27,11 +31,11 @@ function ConditionKeyList({
onChange,
}: ConditionKeyListProps): JSX.Element {
const updateKey = (index: number, value: string): void => {
onChange(keys.map((key, i) => (i === index ? value : key)));
onChange(keys.map((key, i) => (i === index ? { ...key, value } : key)));
};
const addKey = (): void => {
onChange([...keys, '']);
onChange([...keys, createConditionKey()]);
};
const removeKey = (index: number): void => {
@@ -53,7 +57,7 @@ function ConditionKeyList({
<KeySearchInput
className={styles.keyInput}
placeholder={placeholder}
value={key}
value={key.value}
fieldContext={fieldContext}
onChange={(next): void => updateKey(index, next)}
testId={`${testIdPrefix}-${index}`}

View File

@@ -42,7 +42,9 @@ function sourcesEqual(a: SourceConfig[], b: SourceConfig[]): boolean {
(source, index) =>
source.key === b[index].key &&
source.context === b[index].context &&
source.operation === b[index].operation,
source.operation === b[index].operation &&
source.enabled === b[index].enabled &&
source.origin === b[index].origin,
)
);
}

View File

@@ -3,6 +3,7 @@ import {
SpantypesSpanMapperDTO,
SpantypesSpanMapperGroupDTO,
SpantypesSpanMapperOperationDTO,
SpantypesSpanMapperOriginDTO,
} from 'api/generated/services/sigNoz.schemas';
export type MapperGroup = SpantypesSpanMapperGroupDTO;
@@ -11,6 +12,16 @@ export const FieldContext = SpantypesFieldContextDTO;
export type FieldContextValue = SpantypesFieldContextDTO;
export const MapperOperation = SpantypesSpanMapperOperationDTO;
export type MapperOperationValue = SpantypesSpanMapperOperationDTO;
export const MapperOrigin = SpantypesSpanMapperOriginDTO;
export type MapperOriginValue = SpantypesSpanMapperOriginDTO;
// One condition substring. Shipped (system) keys are read-only apart from
// `enabled`; user keys are fully editable.
export interface ConditionKey {
value: string;
enabled: boolean;
origin: MapperOriginValue;
}
export type MapperDraftMode = 'add' | 'edit';
@@ -18,6 +29,8 @@ export interface SourceConfig {
key: string;
context: SpantypesFieldContextDTO;
operation: SpantypesSpanMapperOperationDTO;
enabled: boolean;
origin: MapperOriginValue;
}
// Editable form state for a mapper. `sources` is ordered highest priority
@@ -33,8 +46,8 @@ export interface MapperDraft {
export interface GroupDraft {
id: string | null;
name: string;
attributes: string[];
resource: string[];
attributes: ConditionKey[];
resource: ConditionKey[];
enabled: boolean;
}
@@ -51,8 +64,8 @@ export interface DraftGroup {
localId: string;
serverId: string | null;
name: string;
attributes: string[];
resource: string[];
attributes: ConditionKey[];
resource: ConditionKey[];
enabled: boolean;
mappers: DraftMapper[];
}

View File

@@ -1,12 +1,14 @@
import {
SpantypesPostableSpanMapperDTO,
SpantypesPostableSpanMapperGroupDTO,
SpantypesSpanMapperGroupConditionKeyDTO,
SpantypesUpdatableSpanMapperDTO,
SpantypesUpdatableSpanMapperGroupDTO,
} from 'api/generated/services/sigNoz.schemas';
import { v4 as uuid } from 'uuid';
import {
ConditionKey,
DraftGroup,
DraftMapper,
FieldContext,
@@ -15,6 +17,7 @@ import {
MapperDraft,
MapperGroup,
MapperOperation,
MapperOrigin,
SourceConfig,
} from './types';
@@ -24,20 +27,36 @@ function genLocalId(prefix: 'group' | 'mapper'): string {
return `local-${prefix}-${uuid()}`;
}
// Trimmed, de-duplicated, non-empty keys preserving input order.
function cleanKeys(keys: string[]): string[] {
export function createConditionKey(value = ''): ConditionKey {
return { value, enabled: true, origin: MapperOrigin.user };
}
// Trimmed, de-duplicated, non-empty keys preserving input order. A shipped and
// a user key may share a value, so the origin is part of the identity.
function cleanKeys(keys: ConditionKey[]): ConditionKey[] {
const seen = new Set<string>();
const result: string[] = [];
const result: ConditionKey[] = [];
keys.forEach((raw) => {
const key = raw.trim();
if (key && !seen.has(key)) {
seen.add(key);
result.push(key);
const value = raw.value.trim();
const dedupeKey = `${raw.origin}:${value}`;
if (value && !seen.has(dedupeKey)) {
seen.add(dedupeKey);
result.push({ ...raw, value });
}
});
return result;
}
function fromConditionKeys(
keys: SpantypesSpanMapperGroupConditionKeyDTO[] | null | undefined,
): ConditionKey[] {
return (keys ?? []).map((key) => ({
value: key.value,
enabled: key.enabled,
origin: key.origin ?? MapperOrigin.user,
}));
}
// Source configs for a mapper, highest priority first (first match wins at
// evaluation time).
function getMapperSources(mapper: Mapper): SourceConfig[] {
@@ -48,6 +67,8 @@ function getMapperSources(mapper: Mapper): SourceConfig[] {
key: source.key,
context: source.context,
operation: source.operation,
enabled: source.enabled,
origin: source.origin ?? MapperOrigin.user,
}));
}
@@ -56,6 +77,8 @@ export function createEmptySource(): SourceConfig {
key: '',
context: FieldContext.attribute,
operation: MapperOperation.copy,
enabled: true,
origin: MapperOrigin.user,
};
}
@@ -72,7 +95,7 @@ function getCleanSources(draft: MapperDraft): SourceConfig[] {
const result: SourceConfig[] = [];
draft.sources.forEach((source) => {
const key = source.key.trim();
const dedupeKey = `${source.context}:${key}`;
const dedupeKey = `${source.origin}:${source.context}:${key}`;
if (key && !seen.has(dedupeKey)) {
seen.add(dedupeKey);
result.push({ ...source, key });
@@ -95,6 +118,8 @@ function buildSources(
context: source.context,
operation: source.operation,
priority: sources.length - index,
enabled: source.enabled,
origin: source.origin,
}));
}
@@ -123,7 +148,7 @@ export function buildUpdatableMapper(
export const EMPTY_GROUP_DRAFT: GroupDraft = {
id: null,
name: '',
attributes: [''],
attributes: [createConditionKey()],
resource: [],
enabled: true,
};
@@ -170,8 +195,8 @@ export function buildDraftGroup(
localId: group.id,
serverId: group.id,
name: group.name,
attributes: group.condition?.attributes ?? [],
resource: group.condition?.resource ?? [],
attributes: fromConditionKeys(group.condition?.attributes),
resource: fromConditionKeys(group.condition?.resource),
enabled: group.enabled,
mappers: mappers.map(buildDraftMapper),
};
@@ -182,7 +207,8 @@ export function groupDraftFromNode(group: DraftGroup): GroupDraft {
return {
id: group.localId,
name: group.name,
attributes: group.attributes.length > 0 ? group.attributes : [''],
attributes:
group.attributes.length > 0 ? group.attributes : [createConditionKey()],
resource: group.resource,
enabled: group.enabled,
};

View File

@@ -1,23 +1,35 @@
import { memo, useMemo } from 'react';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { DataSource } from 'types/common/queryBuilder';
function QuerySection(): JSX.Element {
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const isRawQuery = useMemo(
// Only reaches the builder for timeseries/table; list/trace panels use QueryBuilderV2's listViewTracesFilterConfigs.
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(
() => ({
stepInterval: { isHidden: false, isDisabled: false },
limit: { isHidden: false, isDisabled: true },
having: { isHidden: false, isDisabled: true },
}),
[],
);
const isListViewPanel = useMemo(
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
[panelTypes],
);
return (
<QueryBuilderV2
isRawQuery={isRawQuery}
isListViewPanel={isListViewPanel}
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
panelType={panelTypes}
showOnlyWhereClause={isRawQuery}
filterConfigs={filterConfigs}
showOnlyWhereClause={isListViewPanel}
version="v3" // setting this to v3 as we this is rendered in logs explorer
/>
);

View File

@@ -1,6 +1,13 @@
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import {
initialQueriesMap,
OPERATORS,
PANEL_TYPES,
} from 'constants/queryBuilder';
import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
@@ -29,11 +36,42 @@ function LogExplorerQuerySection({
useShareBuilderUrl({ defaultValue });
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const isTable = panelTypes === PANEL_TYPES.TABLE;
const isList = panelTypes === PANEL_TYPES.LIST;
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: isTable, isDisabled: false },
having: { isHidden: isList, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
};
return config;
}, [panelTypes]);
const renderOrderBy = useCallback(
({ query, onChange }: OrderByFilterProps): JSX.Element => (
<ExplorerOrderBy query={query} onChange={onChange} />
),
[],
);
const queryComponents = useMemo(
(): QueryBuilderProps['queryComponents'] => ({
...(panelTypes === PANEL_TYPES.LIST ? { renderOrderBy } : {}),
}),
[panelTypes, renderOrderBy],
);
return (
<QueryBuilderV2
isRawQuery={panelTypes === PANEL_TYPES.LIST}
isListViewPanel={panelTypes === PANEL_TYPES.LIST}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
panelType={panelTypes}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
showOnlyWhereClause={selectedView === ExplorerViews.LIST}
version="v3" // setting this to v3 as we this is rendered in logs explorer
/>

View File

@@ -108,7 +108,6 @@ function LogsExplorerViewsContainer({
const [page, setPage] = useState<number>(1);
const [logs, setLogs] = useState<ILog[]>([]);
const [requestData, setRequestData] = useState<Query | null>(null);
const [queryId, setQueryId] = useState<string>(v4());
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
@@ -180,12 +179,7 @@ function LogsExplorerViewsContainer({
},
undefined,
listQueryKeyRef,
{
...(!isEmpty(queryId) &&
selectedPanelType !== PANEL_TYPES.LIST && {
'X-SIGNOZ-QUERY-ID': queryId,
}),
},
undefined,
// custom selected time interval to prevent recalculating the start and end timestamps before fetching next pages
'custom',
);
@@ -250,10 +244,6 @@ function LogsExplorerViewsContainer({
setRequestData(newRequestData);
}, [isLimit, logs, listQuery, pageSize, stagedQuery, getRequestData, page]);
useEffect(() => {
setQueryId(v4());
}, [data]);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {

View File

@@ -11,6 +11,7 @@ import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
@@ -117,6 +118,11 @@ function Explorer(): JSX.Element {
});
}, []);
const queryComponents = useMemo(
(): QueryBuilderProps['queryComponents'] => ({}),
[],
);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div
@@ -172,6 +178,7 @@ function Explorer(): JSX.Element {
signalSource: 'meter',
}}
panelType={PANEL_TYPES.TIME_SERIES}
queryComponents={queryComponents}
showFunctions={false}
version="v3"
/>

View File

@@ -12,6 +12,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
@@ -322,6 +323,11 @@ function Explorer(): JSX.Element {
});
}, []);
const queryComponents = useMemo(
(): QueryBuilderProps['queryComponents'] => ({}),
[],
);
const [warning, setWarning] = useState<Warning | undefined>();
const oneChartPerQueryDisabledTooltip = useMemo(() => {
@@ -375,6 +381,7 @@ function Explorer(): JSX.Element {
<QueryBuilderV2
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
panelType={PANEL_TYPES.TIME_SERIES}
queryComponents={queryComponents}
showFunctions={false}
version="v3"
/>

View File

@@ -1,9 +1,22 @@
import { ReactNode } from 'react';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { QueryBuilderFieldsConfig } from 'components/QueryBuilderV2/queryBuilderFields.types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
export type WhereClauseConfig = {
customKey: string;
customOp: string;
};
type FilterConfigs = {
[Key in keyof Omit<IBuilderQuery, 'filters'>]: {
isHidden: boolean;
isDisabled: boolean;
};
} & { filters: WhereClauseConfig };
export type QueryBuilderConfig =
| {
queryVariant: 'static';
@@ -16,16 +29,9 @@ export type QueryBuilderProps = {
config?: QueryBuilderConfig;
panelType: PANEL_TYPES;
actions?: ReactNode;
fieldsConfig?: QueryBuilderFieldsConfig;
/**
* The builder edits raw rows rather than an aggregation: a single query unless trace
* matching is on, no formulas, data-source switches reset to the raw-query template,
* and order by resolves keys without an aggregate attribute. Supplies the defaults for
* `fieldsConfig` and `allowedDataSources`, which override it per field.
*/
isRawQuery?: boolean;
/** Defaults to every signal. */
allowedDataSources?: TelemetrytypesSignalDTO[];
filterConfigs?: Partial<FilterConfigs>;
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
isListViewPanel?: boolean;
showFunctions?: boolean;
showOnlyWhereClause?: boolean;
showOnlyTraceOperator?: boolean;

View File

@@ -0,0 +1,6 @@
import { ReactNode } from 'react';
export type AdditionalFiltersProps = {
listOfAdditionalFilter: string[];
children: ReactNode;
};

View File

@@ -0,0 +1,38 @@
import { SquareMinus, SquarePlus } from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
import { Col } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import styled, { css } from 'styled-components';
const IconCss = css`
margin-right: 0.6875rem;
transition: all 0.2s ease;
`;
export const StyledIconOpen = styled(SquarePlus)`
${IconCss}
`;
export const StyledIconClose = styled(SquareMinus)`
${IconCss}
`;
export const StyledInner = styled(Col)`
width: fit-content;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 0.875rem;
min-height: 1.375rem;
cursor: pointer;
&:hover {
${StyledIconOpen}, ${StyledIconClose} {
opacity: 0.7;
}
}
`;
export const StyledLink = styled(Typography.Link)`
pointer-events: none;
color: ${Color.BG_ROBIN_400} !important;
`;

View File

@@ -0,0 +1,15 @@
.filter-toggler {
margin-right: 8px;
}
.additinal-filters-container {
.action-btn {
background: var(--primary-background);
width: 16px;
height: 16px;
border-radius: 3px;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -0,0 +1,66 @@
import { Fragment, memo, ReactNode, useState } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Col, Row } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Minus, Plus } from '@signozhq/icons';
// ** Types
import { AdditionalFiltersProps } from './AdditionalFiltersToggler.interfaces';
// ** Styles
import { StyledInner, StyledLink } from './AdditionalFiltersToggler.styled';
import './AdditionalFiltersToggler.styles.scss';
export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
children,
listOfAdditionalFilter,
}: AdditionalFiltersProps): JSX.Element {
const [isOpenedFilters, setIsOpenedFilters] = useState<boolean>(false);
const handleToggleOpenFilters = (): void => {
setIsOpenedFilters((prevState) => !prevState);
};
const filtersTexts: ReactNode = listOfAdditionalFilter?.map((str, index) => {
const isNextLast = index + 1 === listOfAdditionalFilter.length - 1;
if (index === listOfAdditionalFilter.length - 1) {
return (
<Fragment key={str}>
{listOfAdditionalFilter?.length > 1 && 'and'}{' '}
<StyledLink>{str.toUpperCase()}</StyledLink>
</Fragment>
);
}
return (
<span key={str}>
<StyledLink>{str.toUpperCase()}</StyledLink>
{isNextLast ? ' ' : ', '}
</span>
);
});
return (
<Row className="additinal-filters-container">
<Col span={24}>
<StyledInner onClick={handleToggleOpenFilters} style={{ marginBottom: 0 }}>
{isOpenedFilters ? (
<span className="action-btn">
<Minus size={14} color={Color.BG_INK_500} />
</span>
) : (
<span className="action-btn">
<Plus size={14} color={Color.BG_INK_500} />
</span>
)}
{!isOpenedFilters && (
<Typography>Add conditions for {filtersTexts}</Typography>
)}
</StyledInner>
</Col>
{isOpenedFilters && <Col span={24}>{children}</Col>}
</Row>
);
});

View File

@@ -0,0 +1 @@
export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';

View File

@@ -1,10 +1,8 @@
import { SelectProps } from 'antd';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { DataSource } from 'types/common/queryBuilder';
export type QueryLabelProps = {
onChange: (value: DataSource) => void;
/** Defaults to every signal. */
allowedDataSources?: TelemetrytypesSignalDTO[];
isListViewPanel?: boolean;
'data-testid'?: string;
} & Omit<SelectProps, 'onChange'>;

View File

@@ -1,6 +1,5 @@
import { memo } from 'react';
import { Select } from 'antd';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
// ** Helpers
@@ -8,24 +7,25 @@ import { transformToUpperCase } from 'utils/transformToUpperCase';
// ** Types
import { QueryLabelProps } from './DataSourceDropdown.interfaces';
import { signalsToDataSources } from './DataSourceDropdown.utils';
const ALL_SIGNALS = [
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.traces,
];
const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES];
const exploreDataSourceMap = [DataSource.LOGS, DataSource.TRACES];
export const DataSourceDropdown = memo(function DataSourceDropdown(
props: QueryLabelProps,
): JSX.Element {
const { onChange, value, style, allowedDataSources = ALL_SIGNALS } = props;
const { onChange, value, style, isListViewPanel = false } = props;
const dataSourceOptions: SelectOption<DataSource, string>[] =
signalsToDataSources(allowedDataSources).map((source) => ({
label: transformToUpperCase(source),
value: source,
}));
const dataSourceOptions: SelectOption<DataSource, string>[] = isListViewPanel
? exploreDataSourceMap.map((source) => ({
label: transformToUpperCase(source),
value: source,
}))
: dataSourceMap.map((source) => ({
label: transformToUpperCase(source),
value: source,
}));
return (
<Select

View File

@@ -1,23 +0,0 @@
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { DataSource } from 'types/common/queryBuilder';
// Total, not Partial, so a signal added to the generated enum has to be mapped here
// before it compiles.
const SIGNAL_TO_DATA_SOURCE: Record<
TelemetrytypesSignalDTO,
DataSource | undefined
> = {
[TelemetrytypesSignalDTO.logs]: DataSource.LOGS,
[TelemetrytypesSignalDTO.metrics]: DataSource.METRICS,
[TelemetrytypesSignalDTO.traces]: DataSource.TRACES,
// The "unset" member: not a data source a query can be built against.
[TelemetrytypesSignalDTO['']]: undefined,
};
export function signalsToDataSources(
signals: readonly TelemetrytypesSignalDTO[],
): DataSource[] {
return signals
.map((signal) => SIGNAL_TO_DATA_SOURCE[signal])
.filter((dataSource): dataSource is DataSource => Boolean(dataSource));
}

View File

@@ -0,0 +1,6 @@
import { CSSProperties } from 'react';
export type FilterLabelProps = {
label: string;
style?: CSSProperties;
};

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
interface Props {
isDarkMode: boolean;
children?: React.ReactNode;
}
export const StyledLabel = styled.div<Props>`
padding: 0 0.6875rem;
min-height: 2rem;
min-width: 5.625rem;
display: inline-flex;
white-space: nowrap;
align-items: center;
border-radius: 0.125rem;
`;

View File

@@ -0,0 +1,26 @@
import { memo } from 'react';
import { Typography } from '@signozhq/ui/typography';
import { useIsDarkMode } from 'hooks/useDarkMode';
// ** Types
import { FilterLabelProps } from './FilterLabel.interfaces';
// ** Styles
import { StyledLabel } from './FilterLabel.styled';
export const FilterLabel = memo(function FilterLabel({
label,
}: FilterLabelProps): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<StyledLabel isDarkMode={isDarkMode}>
<Typography
style={{
color: 'var(--bg-vanilla-400)',
}}
>
{label}
</Typography>
</StyledLabel>
);
});

View File

@@ -0,0 +1 @@
export { FilterLabel } from './FilterLabel';

View File

@@ -1,3 +1,4 @@
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import {
IBuilderFormula,
IBuilderQuery,
@@ -7,5 +8,7 @@ export type FormulaProps = {
formula: IBuilderFormula;
index: number;
query: IBuilderQuery;
filterConfigs: Partial<QueryBuilderProps['filterConfigs']>;
isAdditionalFilterEnable: boolean;
isQBV2?: boolean;
};

View File

@@ -2,6 +2,11 @@ import { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { Col, Input, Row, Select } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { LEGEND } from 'constants/global';
// ** Components
import { FilterLabel } from 'container/QueryBuilder/components';
import HavingFilter from 'container/QueryBuilder/filters/Formula/Having/HavingFilter';
import LimitFilter from 'container/QueryBuilder/filters/Formula/Limit/Limit';
import OrderByFilter from 'container/QueryBuilder/filters/Formula/OrderBy/OrderByFilter';
// ** Hooks
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
@@ -12,6 +17,7 @@ import {
import { getFormatedLegend } from 'utils/getFormatedLegend';
import { popupContainer } from 'utils/selectPopupContainer';
import { AdditionalFiltersToggler } from '../AdditionalFiltersToggler';
import QBEntityOptions from '../QBEntityOptions/QBEntityOptions';
// ** Types
import { FormulaProps } from './Formula.interfaces';
@@ -21,18 +27,22 @@ import './Formula.styles.scss';
export function Formula({
index,
formula,
filterConfigs,
query,
isAdditionalFilterEnable,
isQBV2,
}: FormulaProps): JSX.Element {
const { removeQueryBuilderEntityByIndex, handleSetFormulaData } =
useQueryBuilder();
const { handleChangeFormulaData } = useQueryOperations({
index,
query,
formula,
entityVersion: '',
});
const { listOfAdditionalFormulaFilters, handleChangeFormulaData } =
useQueryOperations({
index,
query,
filterConfigs,
formula,
entityVersion: '',
});
const [isCollapse, setIsCollapsed] = useState(false);
@@ -73,6 +83,20 @@ export function Formula({
[handleChangeFormulaData],
);
const handleChangeHavingFilter = useCallback(
(value: IBuilderFormula['having']) => {
handleChangeFormulaData('having', value);
},
[handleChangeFormulaData],
);
const handleChangeOrderByFilter = useCallback(
(value: IBuilderFormula['orderBy']) => {
handleChangeFormulaData('orderBy', value);
},
[handleChangeFormulaData],
);
const handleQBV2OrderByChange = useCallback(
(value: string) => {
const [columnName, order] = value.split(' ');
@@ -98,6 +122,54 @@ export function Formula({
[formula.orderBy],
);
const renderAdditionalFilters = useMemo(
() => (
<>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Limit" />
</Col>
<Col flex="1 1 12.5rem">
<LimitFilter formula={formula} onChange={handleChangeLimit} />
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="HAVING" />
</Col>
<Col flex="1 1 12.5rem">
<HavingFilter formula={formula} onChange={handleChangeHavingFilter} />
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Order by" />
</Col>
<Col flex="1 1 12.5rem">
<OrderByFilter
query={query}
formula={formula}
onChange={handleChangeOrderByFilter}
/>
</Col>
</Row>
</Col>
</>
),
[
formula,
handleChangeHavingFilter,
handleChangeLimit,
handleChangeOrderByFilter,
query,
],
);
return (
<Row gutter={[0, 15]}>
<QBEntityOptions
@@ -134,6 +206,17 @@ export function Formula({
addonBefore="Legend Format"
/>
</Col>
{isAdditionalFilterEnable && (
<Col span={24}>
<AdditionalFiltersToggler
listOfAdditionalFilter={listOfAdditionalFormulaFilters}
>
<Row gutter={[0, 11]} justify="space-between">
{renderAdditionalFilters}
</Row>
</AdditionalFiltersToggler>
</Col>
)}
{isQBV2 && (
<Col span={24}>
<div className="formula-qbv2-container">

View File

@@ -84,14 +84,5 @@
.options-group {
max-width: 100%;
}
.query-functions-container--disabled {
opacity: 0.45;
cursor: not-allowed;
> * {
pointer-events: none;
}
}
}
}

View File

@@ -1,6 +1,5 @@
import { useLocation } from 'react-router-dom';
import { Button, Col, Tooltip } from 'antd';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -27,8 +26,6 @@ interface QBEntityOptionsProps {
query?: IBuilderQuery;
isMetricsDataSource?: boolean;
showFunctions?: boolean;
areFunctionsDisabled?: boolean;
functionsDisabledReason?: string;
isCollapsed: boolean;
entityType: string;
entityData: any;
@@ -39,8 +36,7 @@ interface QBEntityOptionsProps {
onQueryFunctionsUpdates?: (functions: QueryFunction[]) => void;
showDeleteButton?: boolean;
showCloneOption?: boolean;
isRawQuery?: boolean;
allowedDataSources?: TelemetrytypesSignalDTO[];
isListViewPanel?: boolean;
index?: number;
showTraceOperator?: boolean;
hasTraceOperator?: boolean;
@@ -54,15 +50,12 @@ export default function QBEntityOptions({
isMetricsDataSource,
isCollapsed,
showFunctions,
areFunctionsDisabled,
functionsDisabledReason,
entityType,
entityData,
onToggleVisibility,
onCollapseEntity,
onQueryFunctionsUpdates,
isRawQuery,
allowedDataSources,
isListViewPanel,
onDelete,
showDeleteButton,
showCloneOption,
@@ -107,7 +100,7 @@ export default function QBEntityOptions({
value="query-builder"
className="periscope-btn visibility-toggle"
onClick={onToggleVisibility}
disabled={isRawQuery && !showTraceOperator}
disabled={isListViewPanel && !showTraceOperator}
>
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
</Button>
@@ -126,7 +119,7 @@ export default function QBEntityOptions({
'periscope-btn',
entityType === 'query' ? 'query-name' : 'formula-name',
query?.dataSource === DataSource.TRACES &&
(hasTraceOperator || (showTraceOperator && isRawQuery))
(hasTraceOperator || (showTraceOperator && isListViewPanel))
? 'has-trace-operator'
: '',
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
@@ -145,33 +138,24 @@ export default function QBEntityOptions({
}}
data-testid={`query-data-source-selector-${index}`}
value={query?.dataSource || DataSource.METRICS}
allowedDataSources={allowedDataSources}
isListViewPanel={isListViewPanel}
className="query-data-source-dropdown"
/>
</div>
)}
{showFunctions &&
!isRawQuery &&
!isListViewPanel &&
(isMetricsDataSource || isLogsDataSource) &&
query &&
onQueryFunctionsUpdates && (
<Tooltip title={functionsDisabledReason}>
<div
className={cx('query-functions-container', {
'query-functions-container--disabled': areFunctionsDisabled,
})}
aria-disabled={areFunctionsDisabled}
>
<QueryFunctions
query={query}
queryFunctions={query.functions || []}
key={query.functions?.toString()}
onChange={onQueryFunctionsUpdates}
maxFunctions={isLogsDataSource ? 1 : 3}
/>
</div>
</Tooltip>
<QueryFunctions
query={query}
queryFunctions={query.functions || []}
key={query.functions?.toString()}
onChange={onQueryFunctionsUpdates}
maxFunctions={isLogsDataSource ? 1 : 3}
/>
)}
</Button.Group>
</div>
@@ -184,7 +168,7 @@ export default function QBEntityOptions({
)}
</div>
{showDeleteButton && !isRawQuery && (
{showDeleteButton && !isListViewPanel && (
<Button className="periscope-btn ghost" onClick={onDelete}>
<Trash2 size={14} />
</Button>
@@ -195,14 +179,11 @@ export default function QBEntityOptions({
}
QBEntityOptions.defaultProps = {
isRawQuery: false,
allowedDataSources: undefined,
isListViewPanel: false,
query: undefined,
isMetricsDataSource: false,
onQueryFunctionsUpdates: undefined,
showFunctions: false,
areFunctionsDisabled: false,
functionsDisabledReason: undefined,
onCloneQuery: noop,
index: 0,
onDelete: noop,

View File

@@ -1,4 +1,6 @@
export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';
export { DataSourceDropdown } from './DataSourceDropdown';
export { FilterLabel } from './FilterLabel';
export { Formula } from './Formula';
export { HavingFilterTag } from './HavingFilterTag';
export { ListItemWrapper } from './ListItemWrapper';

View File

@@ -0,0 +1,198 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Select } from 'antd';
import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
import { HavingFilterTag } from 'container/QueryBuilder/components';
import { useTagValidation } from 'hooks/queryBuilder/useTagValidation';
import {
transformFromStringToHaving,
transformHavingToStringValue,
} from 'lib/query/transformQueryBuilderData';
import { Having, HavingForm } from 'types/api/queryBuilder/queryBuilderData';
import { SelectOption } from 'types/common/select';
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
import { getHavingObject, isValidHavingValue } from '../../utils';
import { HavingFilterProps, HavingTagRenderProps } from './types';
function HavingFilter({ formula, onChange }: HavingFilterProps): JSX.Element {
const getPopupContainer = useSelectPopupContainer();
const { having } = formula;
const [searchText, setSearchText] = useState<string>('');
const [localValues, setLocalValues] = useState<string[]>([]);
const [currentFormValue, setCurrentFormValue] =
useState<HavingForm>(initialHavingValues);
const [options, setOptions] = useState<SelectOption<string, string>[]>([]);
const { isMulti } = useTagValidation(
currentFormValue.op,
currentFormValue.value,
);
const columnName = formula.expression.replace(/ /g, '').toUpperCase();
const aggregatorOptions: SelectOption<string, string>[] = useMemo(
() => [{ label: columnName, value: columnName }],
[columnName],
);
const handleUpdateTag = useCallback(
(value: string) => {
const filteredValues = localValues.filter(
(currentValue) => currentValue !== value,
);
const having: Having[] = filteredValues.map(transformFromStringToHaving);
onChange(having);
setSearchText(value);
},
[localValues, onChange],
);
const generateOptions = useCallback(
(currentString: string) => {
const [aggregator = '', op = '', ...restValue] = currentString.split(' ');
let newOptions: SelectOption<string, string>[] = [];
const isAggregatorExist = columnName
.toLowerCase()
.includes(currentString.toLowerCase());
const isAggregatorChosen = aggregator === columnName;
if (isAggregatorExist || aggregator === '') {
newOptions = aggregatorOptions;
}
if ((isAggregatorChosen && op === '') || op) {
const filteredOperators = HAVING_OPERATORS.filter((num) =>
num.toLowerCase().includes(op.toLowerCase()),
);
newOptions = filteredOperators.map((opt) => ({
label: `${columnName} ${opt} ${restValue && restValue.join(' ')}`,
value: `${columnName} ${opt} ${restValue && restValue.join(' ')}`,
}));
}
setOptions(newOptions);
},
[aggregatorOptions, columnName],
);
const parseSearchText = useCallback(
(text: string) => {
const { columnName, op, value } = getHavingObject(text);
setCurrentFormValue({ columnName, op, value });
generateOptions(text);
},
[generateOptions],
);
const tagRender = ({
label,
value,
closable,
disabled,
onClose,
}: HavingTagRenderProps): JSX.Element => {
const handleClose = (): void => {
onClose();
setSearchText('');
};
return (
<HavingFilterTag
label={label}
value={value}
closable={closable}
disabled={disabled}
onClose={handleClose}
onUpdate={handleUpdateTag}
/>
);
};
const handleSearch = (search: string): void => {
const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
const currentSearch = isMulti
? trimmedSearch
: trimmedSearch.split(' ').slice(0, 3).join(' ');
const isValidSearch = isValidHavingValue(currentSearch);
if (isValidSearch) {
setSearchText(currentSearch);
}
};
useEffect(() => {
setLocalValues(transformHavingToStringValue(having || []));
}, [having]);
useEffect(() => {
parseSearchText(searchText);
}, [searchText, parseSearchText]);
const resetChanges = (): void => {
setSearchText('');
setCurrentFormValue(initialHavingValues);
setOptions(aggregatorOptions);
};
const handleDeselect = (value: string): void => {
const result = localValues.filter((item) => item !== value);
const having: Having[] = result.map(transformFromStringToHaving);
onChange(having);
resetChanges();
};
const handleSelect = (currentValue: string): void => {
const { columnName, op, value } = getHavingObject(currentValue);
const isCompletedValue = value.every((item) => !!item);
const isClearSearch = isCompletedValue && columnName && op;
setSearchText(isClearSearch ? '' : currentValue);
};
const handleChange = (values: string[]): void => {
const having: Having[] = values.map(transformFromStringToHaving);
const isSelectable =
currentFormValue.value.length > 0 &&
currentFormValue.value.every((value) => !!value);
if (isSelectable) {
onChange(having);
resetChanges();
}
};
return (
<Select
getPopupContainer={getPopupContainer}
autoClearSearchValue={false}
mode="multiple"
onSearch={handleSearch}
searchValue={searchText}
data-testid="havingSelectFormula"
placeholder="Count(operation) > 5"
style={{ width: '100%' }}
tagRender={tagRender}
onDeselect={handleDeselect}
onSelect={handleSelect}
onChange={handleChange}
value={localValues}
>
{options.map((opt) => (
<Select.Option key={opt.value} value={opt.value} title="havingOption">
{opt.label}
</Select.Option>
))}
</Select>
);
}
export default HavingFilter;

View File

@@ -0,0 +1,12 @@
import { HavingFilterTagProps } from 'container/QueryBuilder/components/HavingFilterTag/HavingFilterTag.interfaces';
import {
Having,
IBuilderFormula,
} from 'types/api/queryBuilder/queryBuilderData';
export type HavingFilterProps = {
formula: IBuilderFormula;
onChange: (having: Having[]) => void;
};
export type HavingTagRenderProps = Omit<HavingFilterTagProps, 'onUpdate'>;

View File

@@ -0,0 +1,20 @@
import { InputNumber } from 'antd';
import { selectStyle } from '../../QueryBuilderSearchV2/config';
import { handleKeyDownLimitFilter } from '../../utils';
import { LimitFilterProps } from './types';
function LimitFilter({ onChange, formula }: LimitFilterProps): JSX.Element {
return (
<InputNumber
min={1}
type="number"
value={formula.limit}
style={selectStyle}
onChange={onChange}
onKeyDown={handleKeyDownLimitFilter}
/>
);
}
export default LimitFilter;

View File

@@ -0,0 +1,6 @@
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
export interface LimitFilterProps {
onChange: (values: number | null) => void;
formula: IBuilderFormula;
}

View File

@@ -0,0 +1,85 @@
import { useMemo } from 'react';
import { Select, Spin } from 'antd';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { MetricAggregateOperator } from 'types/common/queryBuilder';
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
import { selectStyle } from '../../QueryBuilderSearchV2/config';
import { OrderByProps } from './types';
import { useOrderByFormulaFilter } from './useOrderByFormulaFilter';
function OrderByFilter({
formula,
onChange,
query,
}: OrderByProps): JSX.Element {
const getPopupContainer = useSelectPopupContainer();
const {
debouncedSearchText,
createOptions,
aggregationOptions,
handleChange,
handleSearchKeys,
selectedValue,
generateOptions,
} = useOrderByFormulaFilter({
query,
onChange,
formula,
});
const { data, isFetching } = useGetAggregateKeys(
{
aggregateAttribute: query.aggregateAttribute?.key || '',
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator || '',
searchText: debouncedSearchText,
},
{
enabled: !!query.aggregateAttribute?.key,
keepPreviousData: true,
},
);
const optionsData = useMemo(() => {
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
const groupByOptions = createOptions(query.groupBy);
const options =
query.aggregateOperator === MetricAggregateOperator.NOOP
? keyOptions
: [...groupByOptions, ...aggregationOptions];
return generateOptions(options);
}, [
aggregationOptions,
createOptions,
data?.payload?.attributeKeys,
generateOptions,
query.aggregateOperator,
query.groupBy,
]);
const isDisabledSelect =
!query.aggregateAttribute?.key ||
query.aggregateOperator === MetricAggregateOperator.NOOP;
return (
<Select
getPopupContainer={getPopupContainer}
mode="tags"
style={selectStyle}
onSearch={handleSearchKeys}
showSearch
disabled={isDisabledSelect}
showArrow={false}
value={selectedValue}
labelInValue
filterOption={false}
options={optionsData}
notFoundContent={isFetching ? <Spin size="small" /> : null}
onChange={handleChange}
/>
);
}
export default OrderByFilter;

View File

@@ -0,0 +1,12 @@
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
export interface OrderByProps {
formula: IBuilderFormula;
query: IBuilderQuery;
onChange: (value: IBuilderFormula['orderBy']) => void;
}
export type IOrderByFormulaFilterProps = OrderByProps;

View File

@@ -0,0 +1,129 @@
import { useMemo, useState } from 'react';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import useDebounce from 'hooks/useDebounce';
import { IOption } from 'hooks/useResourceAttribute/types';
import isEqual from 'lodash-es/isEqual';
import uniqWith from 'lodash-es/uniqWith';
import { parse } from 'papaparse';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import { ORDERBY_FILTERS } from '../../OrderByFilter/config';
import { SIGNOZ_VALUE } from '../../OrderByFilter/constants';
import { UseOrderByFilterResult } from '../../OrderByFilter/useOrderByFilter';
import {
getLabelFromValue,
mapLabelValuePairs,
orderByValueDelimiter,
} from '../../OrderByFilter/utils';
import { getRemoveOrderFromValue } from '../../QueryBuilderSearchV2/utils';
import { getUniqueOrderByValues, getValidOrderByResult } from '../../utils';
import { IOrderByFormulaFilterProps } from './types';
import { transformToOrderByStringValuesByFormula } from './utils';
export const useOrderByFormulaFilter = ({
onChange,
formula,
}: IOrderByFormulaFilterProps): UseOrderByFilterResult => {
const [searchText, setSearchText] = useState<string>('');
const debouncedSearchText = useDebounce(searchText, DEBOUNCE_DELAY);
const handleSearchKeys = (searchText: string): void =>
setSearchText(searchText);
const handleChange = (values: IOption[]): void => {
const validResult = getValidOrderByResult(values);
const result = getUniqueOrderByValues(validResult);
const orderByValues: OrderByPayload[] = result.map((item) => {
const match = parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) {
return {
columnName: item.value,
order: ORDERBY_FILTERS.ASC,
};
}
const [columnName, order] = match.data.flat() as string[];
const columnNameValue =
columnName === SIGNOZ_VALUE ? SIGNOZ_VALUE : columnName;
const orderValue = order ?? ORDERBY_FILTERS.ASC;
return {
columnName: columnNameValue,
order: orderValue,
};
});
setSearchText('');
onChange(orderByValues);
};
const aggregationOptions = [
{
label: `${formula.expression} ${ORDERBY_FILTERS.ASC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
},
{
label: `${formula.expression} ${ORDERBY_FILTERS.DESC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
},
];
const selectedValue = transformToOrderByStringValuesByFormula(formula);
const createOptions = (data: BaseAutocompleteData[]): IOption[] =>
mapLabelValuePairs(data).flat();
const customValue: IOption[] = useMemo(() => {
if (!searchText) {
return [];
}
return [
{
label: `${searchText} ${ORDERBY_FILTERS.ASC}`,
value: `${searchText}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
},
{
label: `${searchText} ${ORDERBY_FILTERS.DESC}`,
value: `${searchText}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
},
];
}, [searchText]);
const generateOptions = (options: IOption[]): IOption[] => {
const currentCustomValue = options.find(
(keyOption) =>
getRemoveOrderFromValue(keyOption.value) === debouncedSearchText,
)
? []
: customValue;
const result = [...currentCustomValue, ...options];
const uniqResult = uniqWith(result, isEqual);
return uniqResult.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(
getRemoveOrderFromValue(option.value),
),
);
};
return {
searchText,
debouncedSearchText,
selectedValue,
aggregationOptions,
createOptions,
handleChange,
handleSearchKeys,
generateOptions,
};
};

View File

@@ -0,0 +1,26 @@
import { IOption } from 'hooks/useResourceAttribute/types';
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
import { SIGNOZ_VALUE } from '../../OrderByFilter/constants';
import { orderByValueDelimiter } from '../../OrderByFilter/utils';
export const transformToOrderByStringValuesByFormula = (
formula: IBuilderFormula,
): IOption[] => {
const prepareSelectedValue: IOption[] =
formula?.orderBy?.map((item) => {
if (item.columnName === SIGNOZ_VALUE) {
return {
label: `${formula.expression} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
}
return {
label: `${item.columnName} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
}) || [];
return prepareSelectedValue;
};

View File

@@ -6,7 +6,7 @@ import {
export type OrderByFilterProps = {
query: IBuilderQuery;
onChange: (values: OrderByPayload[]) => void;
isRawQuery?: boolean;
isListViewPanel?: boolean;
entityVersion?: string;
isNewQueryV2?: boolean;
};

View File

@@ -12,7 +12,7 @@ import { useOrderByFilter } from './useOrderByFilter';
export function OrderByFilter({
query,
onChange,
isRawQuery = false,
isListViewPanel = false,
entityVersion,
isNewQueryV2 = false,
}: OrderByFilterProps): JSX.Element {
@@ -35,7 +35,7 @@ export function OrderByFilter({
searchText: debouncedSearchText,
},
{
enabled: !!query.aggregateAttribute?.key || isRawQuery,
enabled: !!query.aggregateAttribute?.key || isListViewPanel,
keepPreviousData: true,
},
);

View File

@@ -19,6 +19,7 @@ import {
QUERY_BUILDER_SEARCH_VALUES,
} from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import type { WhereClauseConfig } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
@@ -87,6 +88,7 @@ interface CustomTagProps {
interface QueryBuilderSearchV2Props {
query: IBuilderQuery;
onChange: (value: TagFilter) => void;
whereClauseConfig?: WhereClauseConfig;
placeholder?: string;
className?: string;
suffixIcon?: React.ReactNode;
@@ -143,6 +145,7 @@ function QueryBuilderSearchV2(
placeholder,
className,
suffixIcon,
whereClauseConfig,
hardcodedAttributeKeys,
hasPopupContainer,
rootClassName,
@@ -474,7 +477,31 @@ function QueryBuilderSearchV2(
if (searchValue) {
const operatorType =
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
// if key is added and operator is not present then convert to body CONTAINS key
if (
currentFilterItem?.key &&
isEmpty(currentFilterItem?.op) &&
whereClauseConfig?.customKey === 'body' &&
whereClauseConfig?.customOp === OPERATORS.CONTAINS
) {
// eslint-disable-next-line sonarjs/no-identical-functions
setTags((prev) => [
...prev,
{
key: {
key: 'body',
dataType: DataTypes.String,
type: '',
id: 'body--string----true',
},
op: OPERATORS.CONTAINS,
value: currentFilterItem?.key?.key,
},
]);
setCurrentFilterItem(undefined);
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if (
currentFilterItem?.op === OPERATORS.EXISTS ||
currentFilterItem?.op === OPERATORS.NOT_EXISTS
) {
@@ -516,6 +543,8 @@ function QueryBuilderSearchV2(
currentFilterItem?.op,
currentFilterItem?.value,
searchValue,
whereClauseConfig?.customKey,
whereClauseConfig?.customOp,
]);
// this useEffect takes care of tokenisation based on the search state
@@ -1056,6 +1085,7 @@ QueryBuilderSearchV2.defaultProps = {
placeholder: PLACEHOLDER,
className: '',
suffixIcon: null,
whereClauseConfig: {},
hasPopupContainer: true,
rootClassName: '',
hardcodedAttributeKeys: undefined,

View File

@@ -26,7 +26,7 @@ export type QueryProps = {
isAvailableToDisable: boolean;
query: IBuilderQuery;
queryVariant?: 'static' | 'dropdown';
isRawQuery?: boolean;
isListViewPanel?: boolean;
showFunctions?: boolean;
version: string;
showSpanScopeSelector?: boolean;
@@ -35,4 +35,4 @@ export type QueryProps = {
hasTraceOperator?: boolean;
signalSource?: string;
isMultiQueryAllowed?: boolean;
} & Pick<QueryBuilderProps, 'fieldsConfig' | 'allowedDataSources'>;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@@ -1,23 +1,55 @@
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { DataSource } from 'types/common/queryBuilder';
function QuerySection(): JSX.Element {
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const isRawQuery = useMemo(
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const isList = panelTypes === PANEL_TYPES.LIST;
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: false, isDisabled: false },
limit: { isHidden: isList, isDisabled: true },
having: { isHidden: isList, isDisabled: true },
};
return config;
}, [panelTypes]);
const renderOrderBy = useCallback(
({ query, onChange }: OrderByFilterProps) => (
<ExplorerOrderBy query={query} onChange={onChange} />
),
[],
);
const queryComponents = useMemo((): QueryBuilderProps['queryComponents'] => {
const shouldRenderCustomOrderBy =
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE;
return {
...(shouldRenderCustomOrderBy ? { renderOrderBy } : {}),
};
}, [panelTypes, renderOrderBy]);
const isListViewPanel = useMemo(
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
[panelTypes],
);
return (
<QueryBuilderV2
isRawQuery={isRawQuery}
isListViewPanel={isListViewPanel}
showTraceOperator
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
queryComponents={queryComponents}
panelType={panelTypes}
filterConfigs={filterConfigs}
showOnlyWhereClause={
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
}

View File

@@ -316,7 +316,7 @@ function FullView({
<QueryBuilderV2
panelType={selectedPanelType}
version="v3"
isRawQuery={selectedPanelType === PANEL_TYPES.LIST}
isListViewPanel={selectedPanelType === PANEL_TYPES.LIST}
signalSourceChangeEnabled
// filterConfigs={filterConfigs}
// queryComponents={queryComponents}

View File

@@ -14,6 +14,8 @@ import {
initialQueryBuilderFormValuesMap,
listViewInitialLogQuery,
listViewInitialTraceQuery,
mapOfFormulaToFilters,
mapOfQueryFilters,
PANEL_TYPES,
} from 'constants/queryBuilder';
import {
@@ -57,8 +59,9 @@ import { getFormatedLegend } from 'utils/getFormatedLegend';
export const useQueryOperations: UseQueryOperations = ({
query,
index,
filterConfigs,
formula,
isRawQuery = false,
isListViewPanel = false,
entityVersion,
isForTraceOperator = false,
savePreviousQuery = false,
@@ -102,7 +105,46 @@ export const useQueryOperations: UseQueryOperations = ({
}
}, [query]);
const { dataSource } = query;
const { dataSource, aggregateOperator } = query;
const getNewListOfAdditionalFilters = useCallback(
(dataSource: DataSource, isQuery: boolean): string[] => {
const additionalFiltersKeys: (keyof Pick<
IBuilderQuery,
'orderBy' | 'limit' | 'having' | 'stepInterval'
>)[] = ['having', 'limit', 'orderBy', 'stepInterval'];
const mapsOfFilters = isQuery ? mapOfQueryFilters : mapOfFormulaToFilters;
const result: string[] = mapsOfFilters[dataSource]?.reduce<string[]>(
(acc, item) => {
if (
filterConfigs &&
filterConfigs[item.field as (typeof additionalFiltersKeys)[number]]
?.isHidden
) {
return acc;
}
acc.push(item.text);
return acc;
},
[],
);
return result;
},
[filterConfigs],
);
const [listOfAdditionalFilters, setListOfAdditionalFilters] = useState<
string[]
>(getNewListOfAdditionalFilters(dataSource, true));
const [listOfAdditionalFormulaFilters, setListOfAdditionalFormulaFilters] =
useState<string[]>(getNewListOfAdditionalFilters(dataSource, false));
const handleChangeOperator = useCallback(
(value: string): void => {
@@ -418,7 +460,7 @@ export const useQueryOperations: UseQueryOperations = ({
removeKeyFromPreviousQuery(newKey);
}
if (isRawQuery) {
if (isListViewPanel) {
let listPanelQuery: Query | null = null;
if (nextSource === DataSource.LOGS) {
@@ -464,7 +506,7 @@ export const useQueryOperations: UseQueryOperations = ({
handleSetQueryData(index, newQueryData);
},
[
isRawQuery,
isListViewPanel,
panelType,
query,
handleSetQueryData,
@@ -583,18 +625,32 @@ export const useQueryOperations: UseQueryOperations = ({
handleMetricAggregateAtributeTypes,
]);
useEffect(() => {
const additionalFilters = getNewListOfAdditionalFilters(dataSource, true);
setListOfAdditionalFilters(additionalFilters);
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
useEffect(() => {
const additionalFilters = getNewListOfAdditionalFilters(dataSource, false);
setListOfAdditionalFormulaFilters(additionalFilters);
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
return {
isTracePanelType,
isMetricsDataSource,
isLogsDataSource,
operators,
spaceAggregationOptions,
listOfAdditionalFilters,
handleChangeOperator,
handleSpaceAggregationChange,
handleChangeAggregatorAttribute,
handleChangeDataSource,
handleDeleteQuery,
handleChangeQueryData,
listOfAdditionalFormulaFilters,
handleChangeFormulaData,
handleQueryFunctionsUpdates,
};

View File

@@ -4,114 +4,85 @@ export const quickFiltersListResponse = {
signal: 'logs',
filters: [
{
key: 'os.description',
dataType: 'string',
type: 'resource',
name: 'os.description',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'service.name',
dataType: 'string',
type: 'resource',
name: 'service.name',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'duration_nano',
dataType: 'float64',
type: 'tag',
name: 'duration_nano',
fieldDataType: 'float64',
fieldContext: 'attribute',
},
{
key: 'quantity',
dataType: 'float64',
type: 'tag',
name: 'quantity',
fieldDataType: 'float64',
fieldContext: 'attribute',
},
{
key: 'body',
dataType: 'string',
type: '',
name: 'body',
fieldDataType: 'string',
fieldContext: '',
},
{
key: 'deployment.environment',
dataType: 'string',
type: 'resource',
name: 'deployment.environment',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'service.namespace',
dataType: 'string',
type: 'resource',
name: 'service.namespace',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'k8s.namespace.name',
dataType: 'string',
type: 'resource',
name: 'k8s.namespace.name',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'service.instance.id',
dataType: 'string',
type: 'resource',
name: 'service.instance.id',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'k8s.pod.name',
dataType: 'string',
type: 'resource',
name: 'k8s.pod.name',
fieldDataType: 'string',
fieldContext: 'resource',
},
{
key: 'process.owner',
dataType: 'string',
type: 'resource',
name: 'process.owner',
fieldDataType: 'string',
fieldContext: 'resource',
},
],
},
};
const otherFilterName = (name: string): { [k: string]: unknown[] } => ({
[name]: [
{ name, fieldContext: 'resource', fieldDataType: 'string', signal: 'logs' },
],
});
export const otherFiltersResponse = {
status: 'success',
data: {
attributes: [
{
key: 'service.name',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.deployment.name',
dataType: 'string',
type: 'resource',
},
{
key: 'deployment.environment',
dataType: 'string',
type: 'resource',
},
{
key: 'service.namespace',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.namespace.name',
dataType: 'string',
type: 'resource',
},
{
key: 'service.instance.id',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.pod.name',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.pod.uid',
dataType: 'string',
type: 'resource',
},
{
key: 'os.description',
dataType: 'string',
type: 'resource',
},
],
complete: true,
keys: {
...otherFilterName('service.name'),
...otherFilterName('k8s.deployment.name'),
...otherFilterName('deployment.environment'),
...otherFilterName('service.namespace'),
...otherFilterName('k8s.namespace.name'),
...otherFilterName('service.instance.id'),
...otherFilterName('k8s.pod.name'),
...otherFilterName('k8s.pod.uid'),
...otherFilterName('os.description'),
},
},
};

View File

@@ -9,6 +9,7 @@ import { Atom, Terminal } from '@signozhq/icons';
import { Tabs } from 'antd';
import cx from 'classnames';
import { Typography } from '@signozhq/ui/typography';
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import PromQLIcon from 'assets/Dashboard/PromQl';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import TextToolTip from 'components/TextToolTip';
@@ -21,9 +22,8 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { EQueryType } from 'types/common/dashboard';
import {
getQueryBuilderFields,
getHiddenQueryBuilderFields,
getSupportedQueryTypes,
isRawQueryKind,
} from '../../Panels/capabilities';
import {
PANEL_KIND_TO_PANEL_TYPE,
@@ -35,6 +35,8 @@ import styles from './PanelEditorQueryBuilder.module.scss';
interface PanelEditorQueryBuilderProps {
/** The edited panel's visualization kind — drives supported query types + field visibility via the capabilities guard. */
panelKind: PanelKind;
/** The panel's current signal; selects per-signal query-builder field rules. */
signal: TelemetrytypesSignalDTO;
/** 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. */
@@ -54,6 +56,7 @@ interface PanelEditorQueryBuilderProps {
*/
function PanelEditorQueryBuilder({
panelKind,
signal,
isLoadingQueries,
onStageRunQuery,
onCancelQuery,
@@ -65,7 +68,7 @@ function PanelEditorQueryBuilder({
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelKind];
// Raw rows: the builder drops its aggregation controls, and with them the trace
// operator that combines aggregated trace queries (V1 parity).
const isRawQuery = isRawQueryKind(panelKind);
const isListViewPanel = panelKind === 'signoz/ListPanel';
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const isDarkMode = useIsDarkMode();
@@ -93,9 +96,11 @@ function PanelEditorQueryBuilder({
[onStageRunQuery],
);
const fieldsConfig: QueryBuilderProps['fieldsConfig'] = useMemo(
() => getQueryBuilderFields(panelKind),
[panelKind],
// Per-kind query-builder field rules from the guard (e.g. List hides step interval
// and having), passed to QueryBuilderV2 as its `filterConfigs`.
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(
() => getHiddenQueryBuilderFields(panelKind, signal),
[panelKind, signal],
);
const items = useMemo(() => {
@@ -109,10 +114,11 @@ function PanelEditorQueryBuilder({
<div className="query-builder-v2-container">
<QueryBuilderV2
panelType={panelType}
fieldsConfig={fieldsConfig}
showTraceOperator={!isRawQuery}
filterConfigs={filterConfigs}
showTraceOperator={!isListViewPanel}
version="v3"
isRawQuery={isRawQuery}
isListViewPanel={isListViewPanel}
queryComponents={{}}
signalSourceChangeEnabled
savePreviousQuery
/>
@@ -145,7 +151,7 @@ function PanelEditorQueryBuilder({
),
children: queryTypeComponents[queryType].component,
}));
}, [panelKind, panelType, fieldsConfig, isDarkMode, isRawQuery]);
}, [panelKind, panelType, filterConfigs, isDarkMode, isListViewPanel]);
return (
<div

View File

@@ -1,4 +1,6 @@
import { render, screen } from '@testing-library/react';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { OPERATORS } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { EQueryType } from 'types/common/dashboard';
@@ -40,10 +42,14 @@ jest.mock('assets/Dashboard/PromQl', () => ({
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
function renderBuilder(panelKind: string): void {
function renderBuilder(
panelKind: string,
signal: TelemetrytypesSignalDTO = TelemetrytypesSignalDTO.logs,
): void {
render(
<PanelEditorQueryBuilder
panelKind={panelKind as never}
signal={signal}
isLoadingQueries={false}
onStageRunQuery={jest.fn()}
onCancelQuery={jest.fn()}
@@ -53,9 +59,9 @@ function renderBuilder(panelKind: string): void {
function lastQueryBuilderProps(): {
panelType: string;
isRawQuery: boolean;
isListViewPanel: boolean;
showTraceOperator: boolean;
fieldsConfig: unknown;
filterConfigs: unknown;
} {
const calls = mockQueryBuilderV2.mock.calls;
return calls[calls.length - 1][0];
@@ -71,7 +77,7 @@ describe('PanelEditorQueryBuilder query-type tabs (driven by the capabilities gu
});
it('shows only the Query Builder tab for the List kind', () => {
renderBuilder('signoz/ListPanel');
renderBuilder('signoz/ListPanel', TelemetrytypesSignalDTO.logs);
expect(screen.getByText('Query Builder')).toBeInTheDocument();
expect(screen.queryByText('ClickHouse Query')).not.toBeInTheDocument();
@@ -105,24 +111,40 @@ describe('PanelEditorQueryBuilder field visibility (driven by the capabilities g
});
it('passes empty field config + non-list flag for a non-list kind', () => {
renderBuilder('signoz/TimeSeriesPanel');
renderBuilder('signoz/TimeSeriesPanel', TelemetrytypesSignalDTO.metrics);
const props = lastQueryBuilderProps();
expect(props.panelType).toBe('graph');
expect(props.isRawQuery).toBe(false);
expect(props.isListViewPanel).toBe(false);
// The trace operator combines aggregated trace queries, so it rides along with
// the aggregation controls.
expect(props.showTraceOperator).toBe(true);
expect(props.fieldsConfig).toStrictEqual({});
expect(props.filterConfigs).toStrictEqual({});
});
it('marks List raw and leaves the field surface to the raw baseline', () => {
renderBuilder('signoz/ListPanel');
it('hides step interval / having and sets body-contains for List + logs', () => {
renderBuilder('signoz/ListPanel', TelemetrytypesSignalDTO.logs);
const props = lastQueryBuilderProps();
expect(props.panelType).toBe('list');
expect(props.isRawQuery).toBe(true);
expect(props.isListViewPanel).toBe(true);
expect(props.showTraceOperator).toBe(false);
expect(props.fieldsConfig).toStrictEqual({});
expect(props.filterConfigs).toStrictEqual({
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
});
});
it('additionally hides limit for List + traces', () => {
renderBuilder('signoz/ListPanel', TelemetrytypesSignalDTO.traces);
const props = lastQueryBuilderProps();
expect(props.filterConfigs).toStrictEqual({
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
limit: { isHidden: true, isDisabled: true },
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
});
});
});

View File

@@ -84,6 +84,7 @@ function EditorRoute(): JSX.Element {
return (
<PanelEditorQueryBuilder
panelKind="signoz/TimeSeriesPanel"
signal={TelemetrytypesSignalDTO.metrics}
isLoadingQueries={false}
onStageRunQuery={noop}
onCancelQuery={noop}

View File

@@ -320,6 +320,7 @@ function PanelEditorContainer({
<ConfigProvider getPopupContainer={getBodyPopupContainer}>
<PanelEditorQueryBuilder
panelKind={panelKind}
signal={listSignal}
isLoadingQueries={isFetching}
onStageRunQuery={runQuery}
onCancelQuery={cancelQuery}

View File

@@ -2,6 +2,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { OPERATORS } from 'constants/queryBuilder';
import { EQueryType } from 'types/common/dashboard';
import { UNSUPPORTED_PANEL } from '../kinds/UnsupportedPanel/definition';
@@ -9,8 +10,7 @@ import { getPanelDefinition, isPanelKindSupported } from '../registry';
import type { PanelQueryCapabilities } from '../types/panelCapabilities';
import { NO_PANEL_ACTIONS } from '../types/panelDefinition';
import {
getQueryBuilderFields,
isRawQueryKind,
getHiddenQueryBuilderFields,
getSupportedQueryTypes,
getSupportedSignals,
isPanelCombinationValid,
@@ -135,7 +135,7 @@ describe('panel capabilities guard', () => {
expect(
isPanelCombinationValid({ kind: unknownKind, queryType: QUERY_BUILDER }),
).toBe(false);
expect(getQueryBuilderFields(unknownKind)).toStrictEqual({});
expect(getHiddenQueryBuilderFields(unknownKind, logs)).toStrictEqual({});
expect(getPanelDefinition(unknownKind).sections).toStrictEqual([]);
});
@@ -258,24 +258,36 @@ describe('panel capabilities guard', () => {
});
});
describe('getQueryBuilderFields', () => {
it('returns {} for kinds that narrow nothing', () => {
expect(getQueryBuilderFields('signoz/TimeSeriesPanel')).toStrictEqual({});
expect(getQueryBuilderFields('signoz/TablePanel')).toStrictEqual({});
expect(getQueryBuilderFields('signoz/NumberPanel')).toStrictEqual({});
describe('getHiddenQueryBuilderFields', () => {
it('returns {} for kinds that declare no field rules', () => {
expect(
getHiddenQueryBuilderFields('signoz/TimeSeriesPanel', logs),
).toStrictEqual({});
expect(getHiddenQueryBuilderFields('signoz/TablePanel', logs)).toStrictEqual(
{},
);
});
it('returns {} for List, which relies on the raw baseline', () => {
expect(getQueryBuilderFields('signoz/ListPanel')).toStrictEqual({});
// Mirrors QueryBuilderV2's internal listViewLogFilterConfigs — the guard is the
// single source of truth for these values.
it('hides step interval / having and sets body-contains for List + logs', () => {
expect(getHiddenQueryBuilderFields('signoz/ListPanel', logs)).toStrictEqual({
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
});
});
});
describe('isRawQueryKind', () => {
it('is true only for the kind whose request type is raw', () => {
expect(isRawQueryKind('signoz/ListPanel')).toBe(true);
expect(isRawQueryKind('signoz/TimeSeriesPanel')).toBe(false);
expect(isRawQueryKind('signoz/TablePanel')).toBe(false);
expect(isRawQueryKind('signoz/NumberPanel')).toBe(false);
// Mirrors listViewTracesFilterConfigs — traces additionally hide `limit`.
it('additionally hides limit for List + traces', () => {
expect(
getHiddenQueryBuilderFields('signoz/ListPanel', traces),
).toStrictEqual({
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
limit: { isHidden: true, isDisabled: true },
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
});
});
});
});

View File

@@ -1,17 +1,14 @@
import {
Querybuildertypesv5RequestTypeDTO,
type TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { getPanelDefinition } from './registry';
import type { QueryBuilderFieldsConfig } from './types/panelCapabilities';
import type { FilterConfigsPartial } from './types/panelCapabilities';
import type { PanelKind } from './types/panelKind';
/**
* The single deterministic guard for V2 dashboards. Every "what works with what"
* question — panel kind × query type × signal, and how a kind narrows the query
* builder — is answered here by reading each kind's declared capabilities from the panel
* question — panel kind × query type × signal, and which query-builder fields a kind
* hides — is answered here by reading each kind's declared capabilities from the panel
* registry. Adding a new kind means declaring its capabilities once in its definition;
* these functions then cover it automatically. Pure and side-effect free.
*/
@@ -79,17 +76,16 @@ export function resolveQueryType(
return supported.includes(preferred) ? preferred : supported[0];
}
/** How a kind narrows the query builder, on top of the baseline its request type implies. */
export function getQueryBuilderFields(
/**
* Query-builder field visibility for a kind + signal: the kind's `default` rule with
* its per-signal overrides merged over it (signal wins). `{}` when the kind hides
* nothing, i.e. the builder shows every field.
*/
export function getHiddenQueryBuilderFields(
kind: PanelKind,
): QueryBuilderFieldsConfig {
return getPanelDefinition(kind).queryBuilderFields;
}
/** Read from the declared request type, so raw-ness has no second place to drift from. */
export function isRawQueryKind(kind: PanelKind): boolean {
return (
getPanelDefinition(kind).queryCapabilities.requestType ===
Querybuildertypesv5RequestTypeDTO.raw
);
signal: TelemetrytypesSignalDTO,
): FilterConfigsPartial {
const rule = getPanelDefinition(kind).queryBuilderFields;
const perSignal = signal ? rule[signal] : undefined;
return { ...rule.default, ...perSignal };
}

View File

@@ -5,6 +5,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { OPERATORS } from 'constants/queryBuilder';
import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/ListPanel'> = {
@@ -16,9 +17,21 @@ export const definition: PanelDefinition<'signoz/ListPanel'> = {
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
// Raw rows have no aggregation, so step interval / having never apply, and the
// Where clause searches the log/span body via `body CONTAINS`. Traces additionally
// hide `limit` (the server paginates raw spans). Mirrors QueryBuilderV2's internal
// list configs — the capabilities guard is the single source for both.
supportedQueryTypes: [EQueryType.QUERY_BUILDER],
// No deviation from the baseline the raw request type below already implies.
queryBuilderFields: {},
queryBuilderFields: {
default: {
stepInterval: { isHidden: true, isDisabled: true },
having: { isHidden: true, isDisabled: true },
filters: { customKey: 'body', customOp: OPERATORS.CONTAINS },
},
[TelemetrytypesSignalDTO.traces]: {
limit: { isHidden: true, isDisabled: true },
},
},
sections,
// The only kind reading raw rows: they page server-side, and the sort needs a
// tiebreaker so a duplicated sort key can't repeat or skip a row across pages.

View File

@@ -1,6 +1,26 @@
import { Querybuildertypesv5RequestTypeDTO } from 'api/generated/services/sigNoz.schemas';
import {
Querybuildertypesv5RequestTypeDTO,
type TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
export type { QueryBuilderFieldsConfig } from 'components/QueryBuilderV2/queryBuilderFields.types';
/**
* Query-builder field-visibility config a panel kind can declare, mirroring the
* shape `QueryBuilderV2` consumes via its `filterConfigs` prop. Derived from that
* prop type (the underlying `FilterConfigs` isn't exported) so the two never drift.
*/
export type FilterConfigsPartial = NonNullable<
QueryBuilderProps['filterConfigs']
>;
/**
* Per-signal query-builder field rules for a panel kind. `default` applies to every
* signal; a per-signal entry is merged over it (signal wins). The capabilities guard
* resolves this into a single `FilterConfigsPartial` via `getHiddenQueryBuilderFields`.
*/
export type QueryBuilderFieldRule = {
default?: FilterConfigsPartial;
} & Partial<Record<TelemetrytypesSignalDTO, FilterConfigsPartial>>;
/**
* How a kind's query-range request is shaped. Declared per-kind in

View File

@@ -7,7 +7,7 @@ import type { AnyPanelInteractionProps } from './interactions';
import type { PanelKind } from './panelKind';
import type {
PanelQueryCapabilities,
QueryBuilderFieldsConfig,
QueryBuilderFieldRule,
} from './panelCapabilities';
import type { BaseRendererProps, PanelRendererProps } from './rendererProps';
@@ -69,7 +69,8 @@ export interface PanelDefinition<K extends PanelKind = PanelKind> {
supportedSignals: TelemetrytypesSignalDTO[];
/** Query languages this kind supports (Query Builder / ClickHouse / PromQL). */
supportedQueryTypes: EQueryType[];
queryBuilderFields: QueryBuilderFieldsConfig;
/** Query-builder fields this kind hides/disables, optionally per signal (`{}` hides none). */
queryBuilderFields: QueryBuilderFieldRule;
/** How this kind's query-range request is shaped (request type, paging, result formatting). */
queryCapabilities: PanelQueryCapabilities;
actions: PanelActionCapabilities;

View File

@@ -154,6 +154,7 @@ function ViewPanelModalContent({
<div className={styles.queryBuilder}>
<PanelEditorQueryBuilder
panelKind={draft.spec.plugin.kind}
signal={signal}
isLoadingQueries={isFetching}
onStageRunQuery={runQuery}
onCancelQuery={cancelQuery}

View File

@@ -504,7 +504,7 @@ jest.mock('hooks/useHandleExplorerTabChange', () => ({
let capturedPayload: QueryRangePayloadV5;
describe('TracesExplorer -', () => {
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/traces`;
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/traces`;
const setupServer = (): void => {
server.use(

View File

@@ -1,14 +0,0 @@
export interface Filter {
key: string;
dataType: string;
type: string;
}
export interface Props {
signal: string;
}
export type PayloadProps = {
filters: Filter[];
signal: string;
};

View File

@@ -1,14 +0,0 @@
import { SignalType } from 'components/QuickFilters/types';
interface FilterType {
key: string;
datatype: string;
type: string;
}
export interface UpdateCustomFiltersProps {
data: {
filters: FilterType[];
signal: SignalType;
};
}

View File

@@ -1,4 +1,5 @@
import { QueryProps } from 'container/QueryBuilder/type';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderFormula,
@@ -16,13 +17,14 @@ import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from './select';
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> & {
isForTraceOperator?: boolean;
formula?: IBuilderFormula;
isRawQuery?: boolean;
entityVersion: string;
savePreviousQuery?: boolean;
};
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
Pick<QueryBuilderProps, 'filterConfigs'> & {
isForTraceOperator?: boolean;
formula?: IBuilderFormula;
isListViewPanel?: boolean;
entityVersion: string;
savePreviousQuery?: boolean;
};
// Generic type that can work with both legacy and V5 query types
export type HandleChangeQueryData<T = IBuilderQuery> = <
@@ -62,6 +64,7 @@ export type UseQueryOperations = (params: UseQueryOperationsParams) => {
isMetricsDataSource: boolean;
operators: SelectOption<string, string>[];
spaceAggregationOptions: SelectOption<string, string>[];
listOfAdditionalFilters: string[];
handleChangeOperator: (value: string) => void;
handleSpaceAggregationChange: (value: string) => void;
handleChangeAggregatorAttribute: (
@@ -73,4 +76,5 @@ export type UseQueryOperations = (params: UseQueryOperationsParams) => {
handleChangeQueryData: HandleChangeQueryData;
handleChangeFormulaData: HandleChangeFormulaData;
handleQueryFunctionsUpdates: (functions: QueryFunction[]) => void;
listOfAdditionalFormulaFilters: string[];
};

View File

@@ -306,6 +306,11 @@ export type QueryBuilderContextType = {
isDefaultQuery: (props: IsDefaultQueryProps) => boolean;
};
export type QueryAdditionalFilter = {
field: keyof IBuilderQuery;
text: string;
};
export type IsDefaultQueryProps = {
currentQuery: Query;
sourcePage: DataSource;

2
go.mod
View File

@@ -31,7 +31,6 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/huandu/go-sqlbuilder v1.39.1
github.com/jackc/pgx/v5 v5.9.2
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12
@@ -134,6 +133,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/huandu/go-clone v1.7.3 // indirect
github.com/leodido/go-urn v1.4.0 // indirect

View File

@@ -45,7 +45,7 @@ func newConfig() factory.Config {
return &Config{
Tokenizer: TokenizerConfig{
Enabled: true,
Headers: []string{"Authorization", "Sec-WebSocket-Protocol"},
Headers: []string{"Authorization"},
},
APIKeyConfig: APIKeyConfig{
Enabled: true,

View File

@@ -82,6 +82,7 @@ func (handler *handler) GetFieldsValues(rw http.ResponseWriter, req *http.Reques
values := &telemetrytypes.TelemetryFieldValues{
StringValues: allValues.StringValues,
BoolValues: allValues.BoolValues,
NumberValues: allValues.NumberValues,
RelatedValues: relatedValues,
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -16,10 +17,11 @@ type setter struct {
alertmanager alertmanager.Alertmanager
quickfilter quickfilter.Module
dashboard dashboard.Module
spanMapper spanmapper.Module
}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module, dashboard dashboard.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter, dashboard: dashboard}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module, dashboard dashboard.Module, spanMapper spanmapper.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter, dashboard: dashboard, spanMapper: spanMapper}
}
func (module *setter) Create(ctx context.Context, organization *types.Organization, createManagedRoles func(context.Context, valuer.UUID) error) error {
@@ -43,6 +45,10 @@ func (module *setter) Create(ctx context.Context, organization *types.Organizati
return err
}
if err := module.spanMapper.ReconcileSystemGroups(ctx, organization.ID); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,46 @@
package implspanmapper
import (
"embed"
"io/fs"
"path"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/spantypes"
)
const definitionsRoot = "fs/definitions"
//go:embed fs/definitions/*.json
var definitionFiles embed.FS
// NewSystemGroupRegistry parses every embedded definition. Definitions are
// build-time assets validated by a test, so a failure here means the binary
// shipped broken JSON.
func NewSystemGroupRegistry() (spantypes.SpanMapperGroupRegistry, error) {
entries, err := fs.ReadDir(definitionFiles, definitionsRoot)
if err != nil {
return spantypes.SpanMapperGroupRegistry{}, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read span mapper group definitions")
}
definitions := make([]spantypes.SpanMapperGroupDefinition, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
file := path.Join(definitionsRoot, entry.Name())
raw, err := definitionFiles.ReadFile(file)
if err != nil {
return spantypes.SpanMapperGroupRegistry{}, errors.WrapInternalf(err, errors.CodeInternal, "couldn't read %s", file)
}
definition, err := spantypes.NewSpanMapperGroupDefinition(raw)
if err != nil {
return spantypes.SpanMapperGroupRegistry{}, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "couldn't parse %s", file)
}
definitions = append(definitions, definition)
}
return spantypes.NewSpanMapperGroupRegistry(definitions)
}

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