mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-13 00:10:45 +01:00
Compare commits
30 Commits
fga/dashbo
...
feat/initi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42242a93d0 | ||
|
|
16849967c5 | ||
|
|
c8e7685f06 | ||
|
|
a3caaaf7f2 | ||
|
|
a355996a5d | ||
|
|
eea11972a9 | ||
|
|
2616885d22 | ||
|
|
52dd57074e | ||
|
|
fe94b817db | ||
|
|
ea36032d96 | ||
|
|
6ecfa839f3 | ||
|
|
62d382b3cc | ||
|
|
cc07e2fa24 | ||
|
|
d7aa63f1bc | ||
|
|
c36b748370 | ||
|
|
89bf0f953a | ||
|
|
a850f548f8 | ||
|
|
5bf6fd9192 | ||
|
|
13f2ba7d34 | ||
|
|
848046de91 | ||
|
|
68e61af0be | ||
|
|
84780acee1 | ||
|
|
c70b2be4d5 | ||
|
|
cda2955b93 | ||
|
|
1a293652f7 | ||
|
|
0f3fb71067 | ||
|
|
b4f5b3eddf | ||
|
|
b9f4fcd681 | ||
|
|
0dd9a156b9 | ||
|
|
f44d6c7c84 |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -61,6 +61,7 @@ jobs:
|
||||
- querierauthz
|
||||
- role
|
||||
- rootuser
|
||||
- savedview
|
||||
- serviceaccount
|
||||
- spanmapper
|
||||
- querier_json_body
|
||||
|
||||
@@ -1499,6 +1499,7 @@ components:
|
||||
- computeengine
|
||||
- gke
|
||||
- cloudstorage
|
||||
- cloudsql_mysql
|
||||
type: string
|
||||
CloudintegrationtypesServiceMetadata:
|
||||
properties:
|
||||
@@ -7880,17 +7881,20 @@ components:
|
||||
type: string
|
||||
SavedviewtypesPostableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
generateName:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
SavedviewtypesSavedView:
|
||||
properties:
|
||||
@@ -7899,14 +7903,16 @@ components:
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
@@ -7914,14 +7920,6 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
SavedviewtypesSavedViewData:
|
||||
properties:
|
||||
schemaVersion:
|
||||
type: string
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
@@ -7936,7 +7934,10 @@ components:
|
||||
queries:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
|
||||
minItems: 1
|
||||
type: array
|
||||
requestType:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
|
||||
selectedFields:
|
||||
items:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
@@ -7944,10 +7945,13 @@ components:
|
||||
required:
|
||||
- displayName
|
||||
- panelType
|
||||
- requestType
|
||||
- queries
|
||||
- selectedFields
|
||||
- display
|
||||
type: object
|
||||
SavedviewtypesSchemaVersion:
|
||||
enum:
|
||||
- v2
|
||||
type: string
|
||||
SavedviewtypesSource:
|
||||
enum:
|
||||
- traces
|
||||
@@ -7957,13 +7961,16 @@ components:
|
||||
type: string
|
||||
SavedviewtypesUpdatableSavedView:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
|
||||
schemaVersion:
|
||||
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
|
||||
source:
|
||||
$ref: '#/components/schemas/SavedviewtypesSource'
|
||||
spec:
|
||||
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
|
||||
required:
|
||||
- source
|
||||
- data
|
||||
- schemaVersion
|
||||
- spec
|
||||
type: object
|
||||
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
|
||||
properties:
|
||||
@@ -22776,6 +22783,12 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
|
||||
@@ -349,7 +349,7 @@ func (Step) JSONSchema() (jsonschema.Schema, error) {
|
||||
|
||||
### `oneOf` with a discriminator
|
||||
|
||||
For a sum type whose variants are keyed by a property (e.g. `kind`), expose the variants via `JSONSchemaOneOf()` and add a discriminator. Without it, code generators intersect the variants (`A & B & C`) instead of producing a clean discriminated union (`A | B | C`).
|
||||
For a sum type whose variants are keyed by a property (e.g. `kind`), expose the variants via `JSONSchemaOneOf()` and add a discriminator. Without it, code generators intersect the variants (`A & B & C`) instead of producing a clean discriminated union (`A | B | C`). How to model the sum type itself is covered in [types.md](types.md#sum-types-the-kindspec-envelope) — this section is only about its schema.
|
||||
|
||||
The parent keeps its `JSONSchemaOneOf()` (the `oneOf` itself) and *additionally* tags it via `PrepareJSONSchema` with the `x-signoz-discriminator` extension; `signoz.attachDiscriminators` then promotes that marker to a real OpenAPI 3 `discriminator` (and strips the duplicate parent properties) after reflection.
|
||||
|
||||
|
||||
@@ -99,6 +99,69 @@ Each flavor exists for a concrete reason:
|
||||
|
||||
The core `AuthDomain` holds the two live halves — `storableAuthDomain` and `authDomainConfig` — and owns business methods such as `Update(config)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromConfig`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
|
||||
|
||||
## Sum types: the kind/spec envelope
|
||||
|
||||
When a domain type is a *sum type* — exactly one of several variants, selected by a discriminator — model it as an envelope with a `kind` and a `spec`:
|
||||
|
||||
```go
|
||||
type FooConfig struct {
|
||||
Kind FooKind `json:"kind" required:"true"`
|
||||
Spec any `json:"spec" required:"true"`
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{ "kind": "bar", "spec": { "url": "...", "timeout": "30s" } }
|
||||
```
|
||||
|
||||
`Kind` is a `valuer.String` enum implementing `Enum()`; `Spec` holds exactly one concrete variant type (`BarSpec`, `BazSpec`, …). `RuleThresholdData` and `EvaluationEnvelope` in `pkg/types/ruletypes/` are the canonical in-tree examples; the dashboard panel/query/variable plugins in `pkg/types/dashboardtypes/` are the same pattern behind generics. (`QueryEnvelope` in querybuildertypes uses `type` as the discriminator key for historical reasons; new envelopes use `kind`.)
|
||||
|
||||
### The envelope goes at the point of variance, not the resource root
|
||||
|
||||
Put the envelope on the field that actually varies. The resource root is almost never a sum type — a `Foo` has a `name` and an `enabled` flag regardless of which kind it is configured with; only its configuration varies, so the envelope is the `config` field:
|
||||
|
||||
```json
|
||||
{ "name": "my-foo", "enabled": true, "config": { "kind": "bar", "spec": { "...": "..." } } }
|
||||
```
|
||||
|
||||
Hoisting `kind`/`spec` to the root would turn the whole resource into a `oneOf`: every flavor (`PostableFoo`, `UpdatableFoo`, `GettableFoo`) then needs one variant schema per kind, each repeating the common fields; every new common field has to be added to all of them; and generated clients get unions of large objects instead of one small union that narrows on `config.kind`. A root-level `kind` also collides with the resource-model meaning of the word — root `kind` conventionally answers "what resource is this" (`Dashboard`), never "which flavor of config does it hold".
|
||||
|
||||
The existing domains already follow this placement:
|
||||
|
||||
- **Rules** — plain root; envelopes on the varying fields: `thresholds: {kind, spec}` and `evaluation: {kind, spec}`.
|
||||
- **Dashboards** — metadata at the root plus one typed `spec`; the unions sit deep inside, at each panel/query/variable plugin (`{kind, spec}` in `perses_plugin_wrappers.go`).
|
||||
- **Saved views** — root `{schemaVersion, spec}`, where `spec` is a *versioning* envelope holding one fixed type, not a union; the unions are inside it (`spec.queries: [{type, spec}]`). Same word, different job — a versioned body is not a discriminated union.
|
||||
|
||||
### Why this tagging style
|
||||
|
||||
Of the union encodings in common use, the envelope is the *adjacently tagged* one — tag and payload side by side. Variant payloads stay collision-free, and each kind maps to a named wrapper schema that carries the discriminator, which is exactly what OpenAPI generators need. The alternatives lose on those points: *internally tagged* (`{"kind": "bar", ...fields flattened}`) mixes common and variant fields, admits cross-variant key collisions, and forces every variant schema to redeclare the discriminator; *sibling optional fields* (`{"kind": "bar", "barConfig": {}, "bazConfig": {}}`) is the anti-pattern the first rule below exists to prevent.
|
||||
|
||||
The rules that make the envelope work:
|
||||
|
||||
- **Never model variants as sibling fields.** A struct with `Bar *BarSpec`, `Baz *BazSpec` next to a discriminator cannot be expressed as an OpenAPI discriminated union, forces nilability checks on every consumer, and silently admits contradictory payloads (kind=bar with a baz spec). The chosen variant *is* the payload.
|
||||
- **The envelope owns `UnmarshalJSON`.** Decode `kind` first, then switch on it to decode and validate the matching concrete type into `Spec`. Unknown kinds and missing specs are rejected at the boundary:
|
||||
|
||||
```go
|
||||
func (typ *FooConfig) UnmarshalJSON(data []byte) error {
|
||||
var raw map[string]json.RawMessage
|
||||
// ... unmarshal raw, decode raw["kind"] ...
|
||||
switch kind {
|
||||
case FooKindBar:
|
||||
spec := BarSpec{}
|
||||
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
|
||||
return err
|
||||
}
|
||||
typ.Spec = spec
|
||||
// ... one case per kind, default rejects ...
|
||||
}
|
||||
typ.Kind = kind
|
||||
return nil
|
||||
}
|
||||
```
|
||||
- **Consumers type-assert on `Spec`** (`config.Spec.(BarSpec)`) after switching on `Kind`. If assertion sites multiply, add typed accessors on the envelope (see `EvaluationEnvelope.GetEvaluation()`).
|
||||
- **OpenAPI needs one unexported variant struct per kind** (`fooConfigBar{Kind; Spec BarSpec}`), exposed via `JSONSchemaOneOf()` and mapped via `PrepareJSONSchema` with the `x-signoz-discriminator` extension. The schema mechanics are covered in [handler.md](handler.md#oneof-with-a-discriminator).
|
||||
- **A legacy persisted shape gets a data migration or a `StorableX`.** When rows were written before the envelope existed, prefer an idempotent `sqlmigration` that rewrites them into the new shape, so the storable type simply nests the envelope. Only when the old shape must keep being written (external writers, rollback windows) keep it in a storable twin and convert at the type boundary.
|
||||
|
||||
## Conventions that tie the flavors together
|
||||
|
||||
- **Conversions** use either a `New<Output>From<Input>` constructor — e.g. `NewChannelFromReceiver`, `NewGettableAuthDomainFromAuthDomain` — or a receiver-style `ToY()` method. Both forms coexist in the codebase; use whichever fits the call site.
|
||||
@@ -139,6 +202,8 @@ Both are optional. Do not introduce them if `PostableX` already covers the case.
|
||||
|
||||
- Every domain package defines the core type `X`. Only `X` is mandatory.
|
||||
- Add `PostableX` / `GettableX` / `UpdatableX` / `StorableX` one at a time, only when the shape actually diverges from `X`.
|
||||
- Model sum types as a `{kind, spec}` envelope with a validating `UnmarshalJSON` — never as sibling variant fields next to a discriminator.
|
||||
- The envelope goes on the field that varies, never at the resource root — common fields stay on the resource, outside the union.
|
||||
- Domain logic lives on `X`, not on the flavor types.
|
||||
- Conversions can be a `New<Output>From<Input>` constructor or a receiver-style `ToY()` method — pick whichever reads best at the call site.
|
||||
- Use a type alias when two shapes are truly identical.
|
||||
|
||||
@@ -98,14 +98,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
featureSet[idx].Active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ah.Respond(w, featureSet)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,15 +17,3 @@ func GetOrDefaultEnv(key string, fallback string) string {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// constant functions that override env vars
|
||||
|
||||
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
|
||||
|
||||
var IsDotMetricsEnabled = false
|
||||
|
||||
func init() {
|
||||
if GetOrDefaultEnv(DotMetricsEnabled, "true") == "true" {
|
||||
IsDotMetricsEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,7 +376,23 @@ function App(): JSX.Element {
|
||||
tracesSampleRate: 0, // Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
|
||||
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
|
||||
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
|
||||
beforeSend(event) {
|
||||
beforeSend(event, hint) {
|
||||
const error = hint?.originalException as
|
||||
| { name?: string; code?: string | number }
|
||||
| undefined;
|
||||
|
||||
// Ignore benign aborted/cancelled requests (axios + fetch).
|
||||
if (error?.code === 'ERR_CANCELED' || error?.code === 'ECONNABORTED') {
|
||||
return null;
|
||||
}
|
||||
if (error?.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
// Ignore benign Monaco cancellation errors (name 'Canceled').
|
||||
if (error?.name === 'Canceled') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Drop the event if its level is 'warning' or 'info'
|
||||
if (event.level === 'warning' || event.level === 'info') {
|
||||
return null;
|
||||
|
||||
@@ -2818,6 +2818,7 @@ export enum CloudintegrationtypesServiceIDDTO {
|
||||
computeengine = 'computeengine',
|
||||
gke = 'gke',
|
||||
cloudstorage = 'cloudstorage',
|
||||
cloudsql_mysql = 'cloudsql_mysql',
|
||||
}
|
||||
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
|
||||
/**
|
||||
@@ -8991,8 +8992,17 @@ export enum SavedviewtypesPanelTypeDTO {
|
||||
list = 'list',
|
||||
trace = 'trace',
|
||||
}
|
||||
export enum SavedviewtypesSchemaVersionDTO {
|
||||
v2 = 'v2',
|
||||
}
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesSavedViewSpecDTO {
|
||||
display: SavedviewtypesDisplayDTO;
|
||||
display?: SavedviewtypesDisplayDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -9002,28 +9012,14 @@ export interface SavedviewtypesSavedViewSpecDTO {
|
||||
* @type array
|
||||
*/
|
||||
queries: Querybuildertypesv5QueryEnvelopeDTO[];
|
||||
requestType: Querybuildertypesv5RequestTypeDTO;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDataDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
schemaVersion: string;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export enum SavedviewtypesSourceDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
meter = 'meter',
|
||||
}
|
||||
export interface SavedviewtypesPostableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
@@ -9032,7 +9028,9 @@ export interface SavedviewtypesPostableSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface SavedviewtypesSavedViewDTO {
|
||||
@@ -9045,7 +9043,6 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
data?: SavedviewtypesSavedViewDataDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -9054,7 +9051,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source?: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
@@ -9067,8 +9066,9 @@ export interface SavedviewtypesSavedViewDTO {
|
||||
}
|
||||
|
||||
export interface SavedviewtypesUpdatableSavedViewDTO {
|
||||
data: SavedviewtypesSavedViewDataDTO;
|
||||
schemaVersion: SavedviewtypesSchemaVersionDTO;
|
||||
source: SavedviewtypesSourceDTO;
|
||||
spec: SavedviewtypesSavedViewSpecDTO;
|
||||
}
|
||||
|
||||
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
|
||||
|
||||
@@ -24,19 +24,3 @@ export const Logout = async (): Promise<void> => {
|
||||
window.dispatchEvent(new CustomEvent('LOGOUT'));
|
||||
history.push(ROUTES.LOGIN);
|
||||
};
|
||||
|
||||
export const UnderscoreToDotMap: Record<string, string> = {
|
||||
k8s_cluster_name: 'k8s.cluster.name',
|
||||
k8s_cluster_uid: 'k8s.cluster.uid',
|
||||
k8s_namespace_name: 'k8s.namespace.name',
|
||||
k8s_node_name: 'k8s.node.name',
|
||||
k8s_node_uid: 'k8s.node.uid',
|
||||
k8s_pod_name: 'k8s.pod.name',
|
||||
k8s_pod_uid: 'k8s.pod.uid',
|
||||
k8s_deployment_name: 'k8s.deployment.name',
|
||||
k8s_daemonset_name: 'k8s.daemonset.name',
|
||||
k8s_statefulset_name: 'k8s.statefulset.name',
|
||||
k8s_cronjob_name: 'k8s.cronjob.name',
|
||||
k8s_job_name: 'k8s.job.name',
|
||||
k8s_persistentvolumeclaim_name: 'k8s.persistentvolumeclaim.name',
|
||||
};
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/user/resetPassword';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useResetPassword` hook (or `resetPassword` fetcher) from
|
||||
* `api/generated/services/users` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const resetPassword = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>(`/resetPassword`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default resetPassword;
|
||||
@@ -1,31 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/user/setInvite';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useCreateInvite` hook (or `createInvite` fetcher) from
|
||||
* `api/generated/services/users` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
const sendInvite = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||
try {
|
||||
const response = await axios.post<PayloadProps>(`/invite`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default sendInvite;
|
||||
@@ -5,10 +5,9 @@ import {
|
||||
useCreateResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useGetResetPasswordToken,
|
||||
useGetRolesByUserID,
|
||||
useCreateUserRole,
|
||||
useDeleteUserRole,
|
||||
useGetUser,
|
||||
useRemoveUserRoleByUserIDAndRoleID,
|
||||
useSetRoleByUserID,
|
||||
useUpdateMyUserV2,
|
||||
useUpdateUser,
|
||||
} from 'api/generated/services/users';
|
||||
@@ -25,15 +24,14 @@ import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
useDeleteUser: jest.fn(),
|
||||
useGetUser: jest.fn(),
|
||||
useGetRolesByUserID: jest.fn(),
|
||||
useRemoveUserRoleByUserIDAndRoleID: jest.fn(),
|
||||
useDeleteUserRole: jest.fn(),
|
||||
useUpdateUser: jest.fn(),
|
||||
useUpdateMyUserV2: jest.fn(),
|
||||
useSetRoleByUserID: jest.fn(),
|
||||
useCreateUserRole: jest.fn(),
|
||||
useGetResetPasswordToken: jest.fn(),
|
||||
useCreateResetPasswordToken: jest.fn(),
|
||||
getGetRolesByUserIDQueryKey: ({ id }: { id: string }): string[] => [
|
||||
`/api/v2/users/${id}/roles`,
|
||||
getGetUserQueryKey: ({ id }: { id: string }): string[] => [
|
||||
`/api/v2/users/${id}`,
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -194,11 +192,7 @@ describe('EditMemberDrawer', () => {
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
(useGetRolesByUserID as jest.Mock).mockReturnValue({
|
||||
data: { data: [managedRoles[0]] },
|
||||
isLoading: false,
|
||||
});
|
||||
(useRemoveUserRoleByUserIDAndRoleID as jest.Mock).mockReturnValue({
|
||||
(useDeleteUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockRemoveMutateAsync.mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -210,7 +204,7 @@ describe('EditMemberDrawer', () => {
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
(useCreateUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -312,12 +306,12 @@ describe('EditMemberDrawer', () => {
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('adding a new role calls setRole without removing existing ones', async () => {
|
||||
it('adding a new role creates a user role without removing existing ones', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockSet = jest.fn().mockResolvedValue({});
|
||||
|
||||
(useSetRoleByUserID as jest.Mock).mockReturnValue({
|
||||
(useCreateUserRole as jest.Mock).mockReturnValue({
|
||||
mutateAsync: mockSet,
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -334,15 +328,14 @@ describe('EditMemberDrawer', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1' },
|
||||
data: { name: 'signoz-editor' },
|
||||
data: { userId: 'user-1', roleId: managedRoles[1].id },
|
||||
});
|
||||
expect(mockRemoveMutateAsync).not.toHaveBeenCalled();
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('deselecting a role calls removeRole with the role id', async () => {
|
||||
it('deselecting a role deletes the user role by its assignment id', async () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
@@ -361,7 +354,7 @@ describe('EditMemberDrawer', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRemoveMutateAsync).toHaveBeenCalledWith({
|
||||
pathParams: { id: 'user-1', roleId: managedRoles[0].id },
|
||||
pathParams: { id: 'ur-1' },
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
.highlights {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
padding: 12px 0;
|
||||
|
||||
// Constrain each KeyValueLabel (the grid items) to its cell.
|
||||
:global(.key-value-label) {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.valueBadge {
|
||||
--badge-font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// Truncating text inside a badge
|
||||
.badgeText {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.serviceDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-forest);
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.traceLink {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { LOG_HIGHLIGHTS } from './config';
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
interface LogHighlightsProps {
|
||||
log: ILog;
|
||||
}
|
||||
|
||||
function LogHighlights({ log }: LogHighlightsProps): JSX.Element | null {
|
||||
const fields = LOG_HIGHLIGHTS.map((field) => ({
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
value: field.render(log),
|
||||
})).filter((field) => field.value != null);
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.highlights} data-testid="log-details-highlights">
|
||||
{fields.map((field) => (
|
||||
<KeyValueLabel
|
||||
key={field.key}
|
||||
badgeKey={field.label}
|
||||
badgeValue={field.value}
|
||||
direction="column"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogHighlights;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
|
||||
interface TraceIdFieldProps {
|
||||
traceId: string;
|
||||
}
|
||||
|
||||
function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
|
||||
return (
|
||||
<Link
|
||||
to={{ pathname: `/trace/${traceId}` }}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.traceLink}
|
||||
title={traceId}
|
||||
>
|
||||
{traceId}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceIdField;
|
||||
102
frontend/src/components/LogDetail/LogHighlights/config.tsx
Normal file
102
frontend/src/components/LogDetail/LogHighlights/config.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Badge, BadgeColor } from '@signozhq/ui/badge';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import styles from './LogHighlights.module.scss';
|
||||
import TraceIdField from './TraceIdField';
|
||||
|
||||
// Severity badge color mirrors the LogStateIndicator bar
|
||||
const SEVERITY_COLOR: Record<string, BadgeColor> = {
|
||||
[LogType.TRACE]: 'forest',
|
||||
[LogType.DEBUG]: 'aqua',
|
||||
[LogType.INFO]: 'robin',
|
||||
[LogType.WARN]: 'amber',
|
||||
[LogType.ERROR]: 'cherry',
|
||||
[LogType.FATAL]: 'sakura',
|
||||
};
|
||||
|
||||
export interface LogHighlightConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
render: (log: ILog) => ReactNode | null;
|
||||
}
|
||||
|
||||
// Resource/attribute lookup (keys like `service.name` live in resources_string,
|
||||
// occasionally attributes_string). Typed loosely as these are string maps.
|
||||
const getAttr = (log: ILog, key: string): string =>
|
||||
(log.resources_string as unknown as Record<string, string>)?.[key] ||
|
||||
(log.attributes_string as unknown as Record<string, string>)?.[key] ||
|
||||
'';
|
||||
|
||||
const valueBadge = (
|
||||
value: string,
|
||||
options?: { prefix?: ReactNode; color?: BadgeColor },
|
||||
): ReactNode => (
|
||||
<Badge color={options?.color ?? 'vanilla'} className={styles.valueBadge}>
|
||||
{options?.prefix}
|
||||
<span className={styles.badgeText} title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export const LOG_HIGHLIGHTS: LogHighlightConfig[] = [
|
||||
{
|
||||
key: 'service',
|
||||
label: 'SERVICE',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'service.name');
|
||||
return value
|
||||
? valueBadge(value, {
|
||||
prefix: <span className={styles.serviceDot} />,
|
||||
})
|
||||
: null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'severity',
|
||||
label: 'SEVERITY',
|
||||
render: (log): ReactNode | null => {
|
||||
if (!log.severity_text) {
|
||||
return null;
|
||||
}
|
||||
return valueBadge(log.severity_text, {
|
||||
color: SEVERITY_COLOR[getLogIndicatorType(log)] ?? 'vanilla',
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'namespace',
|
||||
label: 'NAMESPACE',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'service.namespace');
|
||||
return value ? valueBadge(value) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'environment',
|
||||
label: 'ENVIRONMENT',
|
||||
render: (log): ReactNode | null => {
|
||||
const value = getAttr(log, 'deployment.environment');
|
||||
return value ? valueBadge(value) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'traceId',
|
||||
label: 'TRACE ID',
|
||||
render: (log): ReactNode | null => {
|
||||
const traceId = log.trace_id || log.traceId;
|
||||
return traceId ? <TraceIdField traceId={traceId} /> : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'spanId',
|
||||
label: 'SPAN ID',
|
||||
render: (log): ReactNode | null => {
|
||||
const spanId = log.span_id || log.spanID;
|
||||
return spanId ? valueBadge(spanId) : null;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -115,6 +115,45 @@ describe('LogDetail drawer — header (isLogDetailsV2)', () => {
|
||||
expect(screen.queryByText('Open in Explorer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Highlights for fields present on the log, omitting absent ones', () => {
|
||||
const logWithMeta = {
|
||||
...mockLog,
|
||||
severity_text: 'ERROR',
|
||||
trace_id: 'trace-abc',
|
||||
resources_string: {
|
||||
'service.name': 'checkout',
|
||||
'deployment.environment': 'production',
|
||||
},
|
||||
} as unknown as ILog;
|
||||
|
||||
renderDrawer({ log: logWithMeta });
|
||||
|
||||
const highlights = screen.getByTestId('log-details-highlights');
|
||||
expect(highlights).toHaveTextContent('SEVERITY');
|
||||
expect(highlights).toHaveTextContent('ERROR');
|
||||
expect(highlights).toHaveTextContent('SERVICE');
|
||||
expect(highlights).toHaveTextContent('checkout');
|
||||
expect(highlights).toHaveTextContent('ENVIRONMENT');
|
||||
expect(highlights).toHaveTextContent('production');
|
||||
expect(highlights).toHaveTextContent('TRACE ID');
|
||||
// Absent fields are omitted (no namespace / span id on this log).
|
||||
expect(highlights).not.toHaveTextContent('NAMESPACE');
|
||||
expect(highlights).not.toHaveTextContent('SPAN ID');
|
||||
});
|
||||
|
||||
it('links the trace id highlight to the trace detail in a new tab', () => {
|
||||
const logWithTrace = {
|
||||
...mockLog,
|
||||
trace_id: 'trace-abc',
|
||||
} as unknown as ILog;
|
||||
|
||||
renderDrawer({ log: logWithTrace });
|
||||
|
||||
const link = screen.getByRole('link', { name: 'trace-abc' });
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link.getAttribute('href')).toContain('/trace/trace-abc');
|
||||
});
|
||||
|
||||
it('navigates to the next / previous log with the Down / Up arrow keys', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const logs = [makeLog('log-0'), makeLog('log-1'), makeLog('log-2')];
|
||||
|
||||
@@ -55,6 +55,7 @@ import { isLogDetailsV2, RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||
import LogDetailsHeader from './LogDetailsHeader/LogDetailsHeader';
|
||||
import { useLogNavigation } from './LogDetailsHeader/useLogNavigation';
|
||||
import LogHighlights from './LogHighlights/LogHighlights';
|
||||
|
||||
import './LogDetails.styles.scss';
|
||||
|
||||
@@ -399,6 +400,8 @@ function LogDetailInner({
|
||||
<div className="log-overflow-shadow"> </div>
|
||||
</div>
|
||||
|
||||
{isLogDetailsV2 && <LogHighlights log={log} />}
|
||||
|
||||
<div className="tabs-and-search">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
|
||||
@@ -10,6 +10,10 @@ jest.mock('providers/Timezone', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/App/App', () => ({
|
||||
useAppContext: (): { featureFlags: [] } => ({ featureFlags: [] }),
|
||||
}));
|
||||
|
||||
const field = (name: string, type = ''): IField => ({
|
||||
name,
|
||||
type,
|
||||
|
||||
@@ -2,13 +2,15 @@ import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { getLogFieldValue } from 'lib/logs/flatLogData';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
@@ -26,6 +28,10 @@ export function useLogsTableColumns({
|
||||
fontSize,
|
||||
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
const { featureFlags } = useAppContext();
|
||||
const isBodyJsonEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
return useMemo<TableColumnDef<ILog>[]>(() => {
|
||||
const stateIndicatorCol: TableColumnDef<ILog> = {
|
||||
@@ -88,7 +94,8 @@ export function useLogsTableColumns({
|
||||
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
|
||||
id: buildCompositeKey(f.name, f.type),
|
||||
header: f.name,
|
||||
accessorFn: (log): unknown => FlatLogData(log)[f.name],
|
||||
accessorFn: (log): unknown =>
|
||||
getLogFieldValue(log, f.name, isBodyJsonEnabled),
|
||||
enableRemove: true,
|
||||
width: { min: 192 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
@@ -115,5 +122,5 @@ export function useLogsTableColumns({
|
||||
.filter((c): c is TableColumnDef<ILog> => c !== null);
|
||||
|
||||
return [stateIndicatorCol, ...fieldCols];
|
||||
}, [fields, fontSize, formatTimezoneAdjustedTimestamp]);
|
||||
}, [fields, fontSize, formatTimezoneAdjustedTimestamp, isBodyJsonEnabled]);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getRecentOptions,
|
||||
isSupportedFunction,
|
||||
renderRecentDeleteButton,
|
||||
} from './utils';
|
||||
|
||||
@@ -183,15 +184,14 @@ function QuerySearch({
|
||||
isProgrammaticChangeRef.current = true;
|
||||
}
|
||||
|
||||
const changes = view.state.changes({
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
});
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: currentValue.length,
|
||||
insert: value,
|
||||
},
|
||||
selection: {
|
||||
anchor: value.length,
|
||||
},
|
||||
changes,
|
||||
selection: { anchor: changes.newLength },
|
||||
});
|
||||
},
|
||||
[],
|
||||
@@ -1276,11 +1276,13 @@ function QuerySearch({
|
||||
}
|
||||
|
||||
if (queryContext.isInFunction) {
|
||||
options = Object.values(QUERY_BUILDER_FUNCTIONS).map((option) => ({
|
||||
label: option,
|
||||
apply: `${option}()`,
|
||||
type: 'function',
|
||||
}));
|
||||
options = Object.values(QUERY_BUILDER_FUNCTIONS)
|
||||
.filter((option) => isSupportedFunction(option, dataSource))
|
||||
.map((option) => ({
|
||||
label: option,
|
||||
apply: `${option}()`,
|
||||
type: 'function',
|
||||
}));
|
||||
|
||||
// Add space after selection for functions
|
||||
const optionsWithSpace = addSpaceToOptions(options);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
getFieldContextPrefix,
|
||||
getUserExpressionFromCombined,
|
||||
isSupportedFunction,
|
||||
} from '../utils';
|
||||
|
||||
describe('entityLogsExpression', () => {
|
||||
@@ -118,3 +122,19 @@ describe('dedupeOptionsByLabel', () => {
|
||||
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSupportedFunction', () => {
|
||||
const { HASANY, SEARCH } = QUERY_BUILDER_FUNCTIONS;
|
||||
|
||||
it('allows the has family on every signal', () => {
|
||||
[DataSource.LOGS, DataSource.TRACES, DataSource.METRICS].forEach((signal) => {
|
||||
expect(isSupportedFunction(HASANY, signal)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows search on logs only', () => {
|
||||
expect(isSupportedFunction(SEARCH, DataSource.LOGS)).toBe(true);
|
||||
expect(isSupportedFunction(SEARCH, DataSource.TRACES)).toBe(false);
|
||||
expect(isSupportedFunction(SEARCH, DataSource.METRICS)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { closeCompletion, startCompletion } from '@codemirror/autocomplete';
|
||||
import type { Completion } from '@codemirror/autocomplete';
|
||||
import type { EditorView } from '@uiw/react-codemirror';
|
||||
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
|
||||
import dayjs from 'dayjs';
|
||||
import { normalizeFilterExpression } from 'lib/recentQueries/normalize';
|
||||
import * as recentQueriesStore from 'lib/recentQueries/recentQueriesStore';
|
||||
@@ -15,6 +16,15 @@ import {
|
||||
RECENTS_SECTION,
|
||||
} from './constants';
|
||||
|
||||
// search() lives in the logs condition builder only; traces and metrics reject it
|
||||
// as an unsupported operator. Every other function is implemented for all signals.
|
||||
export function isSupportedFunction(
|
||||
functionName: string,
|
||||
signal: SignalType,
|
||||
): boolean {
|
||||
return functionName !== QUERY_BUILDER_FUNCTIONS.SEARCH || signal === 'logs';
|
||||
}
|
||||
|
||||
export interface FieldContextPrefixMatch {
|
||||
context: string;
|
||||
remainder: string;
|
||||
|
||||
@@ -301,6 +301,66 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not crash when the expression contains CRLF line breaks (issue #5869)', async () => {
|
||||
const dispatchSpy = jest.spyOn(EditorView.prototype, 'dispatch');
|
||||
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
|
||||
const initialExpression = "service.name = 'frontend'";
|
||||
// Filtering on a multi-line log value (CRLF) used to throw
|
||||
// "RangeError: Selection points outside of document".
|
||||
const crlfExpression = "body CONTAINS 'line1\r\nline2\r\nline3'";
|
||||
|
||||
const baseQueryData = {
|
||||
...initialQueriesMap.logs.builder.queryData[0],
|
||||
filter: { expression: initialExpression },
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={baseQueryData}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const editorContent = document.querySelector(
|
||||
CM_EDITOR_SELECTOR,
|
||||
) as HTMLElement;
|
||||
expect(editorContent.textContent || '').toBe(initialExpression);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
rerender(
|
||||
<QuerySearch
|
||||
onChange={onChange}
|
||||
queryData={{ ...baseQueryData, filter: { expression: crlfExpression } }}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The programmatic replace dispatched without throwing, and the selection anchor
|
||||
// stayed within the CRLF-normalized document (the bug set it past the end).
|
||||
await waitFor(() => {
|
||||
const spec = dispatchSpy.mock.calls
|
||||
.map(
|
||||
(call) =>
|
||||
call[0] as {
|
||||
selection?: { anchor?: number };
|
||||
changes?: { newLength?: number };
|
||||
},
|
||||
)
|
||||
.find((s) => s?.selection?.anchor != null && s?.changes?.newLength != null);
|
||||
expect(spec).toBeDefined();
|
||||
expect(spec?.selection?.anchor).toBeLessThanOrEqual(
|
||||
spec?.changes?.newLength as number,
|
||||
);
|
||||
});
|
||||
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
|
||||
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
|
||||
@@ -41,6 +41,7 @@ export const QUERY_BUILDER_FUNCTIONS = {
|
||||
HASANY: 'hasAny',
|
||||
HASALL: 'hasAll',
|
||||
HASTOKEN: 'hasToken',
|
||||
SEARCH: 'search',
|
||||
};
|
||||
|
||||
export function negateOperator(operatorOrFunction: string): string {
|
||||
|
||||
@@ -7,7 +7,6 @@ export enum FeatureKeys {
|
||||
GATEWAY = 'gateway',
|
||||
PREMIUM_SUPPORT = 'premium_support',
|
||||
ANOMALY_DETECTION = 'anomaly_detection',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
|
||||
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
|
||||
|
||||
@@ -11,6 +11,8 @@ export enum LOCALSTORAGE {
|
||||
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
|
||||
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
AI_OBSERVABILITY_LIST_COLUMNS = 'AI_OBSERVABILITY_LIST_COLUMNS',
|
||||
AI_OBSERVABILITY_TRACE_VIEW_COLUMNS = 'AI_OBSERVABILITY_TRACE_VIEW_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOG_DETAILS_V2 = 'LOG_DETAILS_V2',
|
||||
|
||||
@@ -437,6 +437,17 @@ describe('Create Alert Channel', () => {
|
||||
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
|
||||
});
|
||||
|
||||
// paste instead of type: a per-keystroke re-render of the whole form
|
||||
// pushes these tests past the 5s jest timeout on slower CI runners
|
||||
async function fillField(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
testId: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
await user.click(screen.getByTestId(testId));
|
||||
await user.paste(value);
|
||||
}
|
||||
|
||||
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
|
||||
expect(screen.getByText('Google Chat')).toBeInTheDocument();
|
||||
});
|
||||
@@ -463,14 +474,8 @@ describe('Create Alert Channel', () => {
|
||||
it('Should check if saving with a webhook url outside chat.googleapis.com displays error notification', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('channel-name-textbox'),
|
||||
'gchat-channel',
|
||||
);
|
||||
await user.type(
|
||||
screen.getByTestId('webhook-url-textbox'),
|
||||
'https://example.com/webhook',
|
||||
);
|
||||
await fillField(user, 'channel-name-textbox', 'gchat-channel');
|
||||
await fillField(user, 'webhook-url-textbox', 'https://example.com/webhook');
|
||||
|
||||
await user.click(screen.getByTestId('save-channel-button'));
|
||||
|
||||
@@ -496,11 +501,8 @@ describe('Create Alert Channel', () => {
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.type(
|
||||
screen.getByTestId('channel-name-textbox'),
|
||||
'gchat-channel',
|
||||
);
|
||||
await user.type(screen.getByTestId('webhook-url-textbox'), validWebhookUrl);
|
||||
await fillField(user, 'channel-name-textbox', 'gchat-channel');
|
||||
await fillField(user, 'webhook-url-textbox', validWebhookUrl);
|
||||
|
||||
await user.click(screen.getByTestId('save-channel-button'));
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Exception, PayloadProps } from 'types/api/errors/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import { FilterDropdownExtendsProps } from './types';
|
||||
import {
|
||||
extractFilterValues,
|
||||
@@ -418,11 +416,6 @@ function AllErrors(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
|
||||
(
|
||||
paginations: TablePaginationConfig,
|
||||
@@ -458,7 +451,7 @@ function AllErrors(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!isUndefined(errorCountResponse.data?.payload)) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('Exception: List page visited', {
|
||||
|
||||
@@ -130,6 +130,28 @@ describe('Footer utils', () => {
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeNull();
|
||||
});
|
||||
|
||||
it('when threshold channels are null', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
thresholdState: {
|
||||
...args.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...args.thresholdState.thresholds[0],
|
||||
channels: null as unknown as string[],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBe(
|
||||
'Please select at least one channel for each threshold or enable routing policies',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotificationSettingsProps', () => {
|
||||
|
||||
@@ -44,7 +44,8 @@ export function validateCreateAlertState(
|
||||
if (!threshold.label) {
|
||||
return 'Please enter a label for each threshold';
|
||||
}
|
||||
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
|
||||
// this runs during render, so a throw here takes down the whole page
|
||||
if (!notificationSettings.routingPolicies && !threshold.channels?.length) {
|
||||
return 'Please select at least one channel for each threshold or enable routing policies';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,34 @@ describe('CreateAlertV2 utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThresholdStateFromAlertDef null channels', () => {
|
||||
it('falls back to an empty array so downstream consumers never see null', () => {
|
||||
const def: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
target: 1,
|
||||
targetUnit: UniversalYAxisUnit.MINUTES,
|
||||
channels: null as unknown as string[],
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
op: AlertThresholdOperator.IS_ABOVE,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
getThresholdStateFromAlertDef(def).thresholds[0].channels,
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeOperator', () => {
|
||||
it.each([
|
||||
['1', AlertThresholdOperator.IS_ABOVE],
|
||||
|
||||
@@ -258,7 +258,9 @@ export function getThresholdStateFromAlertDef(
|
||||
recoveryThresholdValue: null,
|
||||
unit: threshold.targetUnit,
|
||||
color: getColorForThreshold(threshold.name),
|
||||
channels: threshold.channels,
|
||||
// rules created outside the UI can come back with a null channels
|
||||
// field; drop the guard once the API enforces the schema
|
||||
channels: threshold.channels ?? [],
|
||||
})) || [],
|
||||
selectedQuery: alertDef.condition.selectedQueryName || '',
|
||||
operator:
|
||||
|
||||
@@ -35,7 +35,6 @@ import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { DOCS_LINKS } from '../constants';
|
||||
import { columns, TIME_PICKER_OPTIONS } from './constants';
|
||||
|
||||
@@ -212,19 +211,13 @@ function ServiceMetrics({
|
||||
|
||||
const topLevelOperations = useMemo(() => Object.entries(data || {}), [data]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const queryRangeRequestData = useMemo(
|
||||
() =>
|
||||
getQueryRangeRequestData({
|
||||
topLevelOperations,
|
||||
globalSelectedInterval,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
|
||||
[globalSelectedInterval, topLevelOperations],
|
||||
);
|
||||
|
||||
const dataQueries = useGetQueriesRange(
|
||||
|
||||
@@ -562,13 +562,9 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
|
||||
op: '=',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 1`,
|
||||
},
|
||||
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -648,13 +644,9 @@ export const getClusterMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
|
||||
op: '=',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 0`,
|
||||
},
|
||||
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -22,6 +22,7 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
|
||||
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
@@ -116,6 +117,7 @@ function EntityEventsContent({
|
||||
: newUserExpression || '',
|
||||
);
|
||||
if (validation.isValid) {
|
||||
saveRecentQueryByExpression(DataSource.LOGS, newUserExpression);
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
@@ -132,6 +133,7 @@ function EntityLogsContent({
|
||||
);
|
||||
|
||||
if (validation.isValid) {
|
||||
saveRecentQueryByExpression(DataSource.LOGS, newUserExpression);
|
||||
querySearchOnRun(newUserExpression);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
|
||||
@@ -121,12 +121,6 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
plan_version: 'test-plan-version',
|
||||
},
|
||||
},
|
||||
featureFlags: [
|
||||
{
|
||||
name: 'DOT_METRICS_ENABLED',
|
||||
active: false,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
|
||||
const mockEntity = {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
@@ -98,6 +99,7 @@ function EntityTracesContent({
|
||||
: newUserExpression || '',
|
||||
);
|
||||
if (validation.isValid) {
|
||||
saveRecentQueryByExpression(DataSource.TRACES, newUserExpression);
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
|
||||
@@ -1208,13 +1208,9 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED}) > 0`,
|
||||
},
|
||||
legend: 'desired',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -1261,13 +1257,9 @@ export const getNamespaceMetricsQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: {
|
||||
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_AVAILABLE}) > 0`,
|
||||
},
|
||||
legend: 'available',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.3rem;
|
||||
margin: var(--spacing-4) 0;
|
||||
}
|
||||
|
||||
.optionsTrigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
cursor: pointer;
|
||||
|
||||
// Was a bare div in the traces explorer; a <button> for keyboard access
|
||||
// needs its chrome reset back to the original look.
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Settings } from '@signozhq/icons';
|
||||
import FieldsSelector from 'components/FieldsSelector';
|
||||
import Controls, { ControlsProps } from 'container/Controls';
|
||||
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import useQueryPagination from 'hooks/queryPagination/useQueryPagination';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import styles from './Controls.module.scss';
|
||||
|
||||
function ExplorerControls({
|
||||
isLoading,
|
||||
totalCount,
|
||||
perPageOptions,
|
||||
config,
|
||||
showSizeChanger = true,
|
||||
}: ExplorerControlsProps): JSX.Element | null {
|
||||
const { t } = useTranslation(['trace']);
|
||||
const [isFieldsSelectorOpen, setIsFieldsSelectorOpen] = useState(false);
|
||||
|
||||
const {
|
||||
pagination,
|
||||
handleCountItemsPerPageChange,
|
||||
handleNavigateNext,
|
||||
handleNavigatePrevious,
|
||||
} = useQueryPagination(totalCount, perPageOptions);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{config?.fieldsSelector && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.optionsTrigger}
|
||||
onClick={(): void => setIsFieldsSelectorOpen(true)}
|
||||
data-testid="explorer-controls-options"
|
||||
>
|
||||
{t('options_menu.options')}
|
||||
<Settings size="md" />
|
||||
</button>
|
||||
<FieldsSelector
|
||||
isOpen={isFieldsSelectorOpen}
|
||||
title="Edit columns"
|
||||
fields={config.fieldsSelector.value}
|
||||
onFieldsChange={config.fieldsSelector.onFieldsChange}
|
||||
onClose={(): void => setIsFieldsSelectorOpen(false)}
|
||||
signal={DataSource.TRACES}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Controls
|
||||
isLoading={isLoading}
|
||||
totalCount={totalCount}
|
||||
offset={pagination.offset}
|
||||
countPerPage={pagination.limit}
|
||||
perPageOptions={perPageOptions}
|
||||
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
showSizeChanger={showSizeChanger}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ExplorerControlsProps = Pick<
|
||||
ControlsProps,
|
||||
'isLoading' | 'totalCount' | 'perPageOptions'
|
||||
> & {
|
||||
config?: OptionsMenuConfig | null;
|
||||
showSizeChanger?: boolean;
|
||||
};
|
||||
|
||||
ExplorerControls.defaultProps = {
|
||||
config: null,
|
||||
showSizeChanger: true,
|
||||
};
|
||||
|
||||
export default memo(ExplorerControls);
|
||||
@@ -1,11 +1,56 @@
|
||||
.explorer {
|
||||
.explorerPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-2) var(--spacing-0);
|
||||
|
||||
// Meant to fix the query builder colors
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
--input-border-color: var(--l2-border);
|
||||
--input-hover-border-color: var(--internal-ant-border-color-hover);
|
||||
--input-focus-border-color: var(--internal-ant-border-color-hover);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-base);
|
||||
.filter {
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
border: 1px solid var(--l1-border);
|
||||
border-right: 0px;
|
||||
background-color: var(--l1-background);
|
||||
|
||||
> :global(.ant-card-body) {
|
||||
padding: 0;
|
||||
width: 258px;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer {
|
||||
width: 100%;
|
||||
background: var(--l1-background);
|
||||
border-color: var(--l1-border);
|
||||
|
||||
> :global(.ant-card-body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.isFiltersExpanded {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
|
||||
.views {
|
||||
padding: var(--spacing-4);
|
||||
// Room the traces explorer reserves for its floating options bar, which this
|
||||
// explorer doesn't render yet. No spacing token for 60px.
|
||||
padding-bottom: 60px;
|
||||
margin-bottom: var(--spacing-12);
|
||||
|
||||
:global(.ant-tabs-tabpane) {
|
||||
padding: 0 var(--spacing-4);
|
||||
}
|
||||
}
|
||||
|
||||
.view {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,253 @@
|
||||
import styles from './Explorer.module.scss';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { QueryKey, useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import {
|
||||
ICurrentQueryData,
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { Warning } from 'types/api';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import {
|
||||
explorerViewToPanelType,
|
||||
getExplorerViewFromUrl,
|
||||
} from 'utils/explorerUtils';
|
||||
|
||||
// Shell for the AI Observability Explorer tab. Owns the
|
||||
// /ai-observability/explorer route and is intentionally empty for now: the
|
||||
// query builder + results surface land in a follow-up.
|
||||
import { defaultSelectedColumns, TOOLBAR_VIEWS } from './constants';
|
||||
import styles from './Explorer.module.scss';
|
||||
import ListView from './ListView/ListView';
|
||||
import QuerySection from './QuerySection/QuerySection';
|
||||
import TableView from './TableView/TableView';
|
||||
import TimeSeriesView from './TimeSeriesView/TimeSeriesView';
|
||||
import TracesView from './TracesView/TracesView';
|
||||
|
||||
// AI Observability Explorer tab. Owns the /ai-observability/explorer route.
|
||||
// Forked from the Traces Explorer (GenAI spans are trace data) and diverges
|
||||
// from it as the GenAI query surface lands, so the two are deliberately not
|
||||
// sharing a component.
|
||||
function Explorer(): JSX.Element {
|
||||
const {
|
||||
updateAllQueriesOperators,
|
||||
handleRunQuery,
|
||||
stagedQuery,
|
||||
handleSetConfig,
|
||||
} = useQueryBuilder();
|
||||
|
||||
// Return value unused: this seeds the URL `options` param on the views that
|
||||
// don't mount ListView (which runs its own useOptionsMenu).
|
||||
useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
initialOptions: {
|
||||
selectColumns: defaultSelectedColumns,
|
||||
},
|
||||
});
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
const listQueryKeyRef = useRef<QueryKey>();
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
const [isLoadingQueries, setIsLoadingQueries] = useState<boolean>(false);
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingQueries) {
|
||||
setIsCancelled(false);
|
||||
}
|
||||
}, [isLoadingQueries]);
|
||||
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
if (listQueryKeyRef.current) {
|
||||
void queryClient.cancelQueries(listQueryKeyRef.current);
|
||||
}
|
||||
setIsCancelled(true);
|
||||
// Reset loading state — the active view unmounts when cancelled, so no
|
||||
// child will call setIsLoadingQueries(false) otherwise.
|
||||
setIsLoadingQueries(false);
|
||||
}, [queryClient]);
|
||||
|
||||
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
|
||||
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
|
||||
);
|
||||
|
||||
const [warning, setWarning] = useState<Warning | undefined>();
|
||||
const [isOpen, setOpen] = useState<boolean>(true);
|
||||
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
const handleChangeSelectedView = useCallback(
|
||||
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
|
||||
handleSetConfig(explorerViewToPanelType[view], DataSource.TRACES);
|
||||
|
||||
setSelectedView(view);
|
||||
|
||||
handleExplorerTabChange(
|
||||
explorerViewToPanelType[view],
|
||||
querySearchParameters,
|
||||
);
|
||||
},
|
||||
[handleExplorerTabChange, handleSetConfig],
|
||||
);
|
||||
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
void logEvent('AI Observability Explorer: Page visited', {});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isFilterApplied = useMemo(() => {
|
||||
// if any of the non-disabled queries has filters applied, return true
|
||||
const result = stagedQuery?.builder?.queryData?.filter(
|
||||
(item) => !isEmpty(item.filters?.items) && !item.disabled,
|
||||
);
|
||||
return !!result?.length;
|
||||
}, [stagedQuery]);
|
||||
|
||||
return (
|
||||
<div className={styles.explorer} data-testid="llm-observability-explorer">
|
||||
<div className={styles.placeholder}>Explorer coming soon.</div>
|
||||
</div>
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div
|
||||
className={styles.explorerPage}
|
||||
data-testid="llm-observability-explorer"
|
||||
>
|
||||
<Card className={styles.filter} hidden={!isOpen}>
|
||||
<QuickFilters
|
||||
className="qf-traces-explorer"
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
signal={SignalType.TRACES}
|
||||
handleFilterVisibilityChange={(): void => {
|
||||
setOpen(!isOpen);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<div
|
||||
className={cx(styles.explorer, {
|
||||
[styles.isFiltersExpanded]: isOpen,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Toolbar
|
||||
showAutoRefresh
|
||||
leftActions={
|
||||
<LeftToolbarActions
|
||||
showFilter={isOpen}
|
||||
handleFilterVisibilityChange={(): void => setOpen(!isOpen)}
|
||||
items={TOOLBAR_VIEWS}
|
||||
selectedView={selectedView}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
}
|
||||
warningElement={
|
||||
!isEmpty(warning) ? <WarningPopover warningData={warning} /> : <div />
|
||||
}
|
||||
rightActions={
|
||||
<RightToolbarActions
|
||||
onStageRunQuery={(): void => {
|
||||
setIsCancelled(false);
|
||||
handleRunQuery();
|
||||
}}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>
|
||||
<div className="query-section-container">
|
||||
<QuerySection />
|
||||
</div>
|
||||
</ExplorerCard>
|
||||
|
||||
<div className={styles.views}>
|
||||
{isCancelled && (
|
||||
<QueryCancelledPlaceholder subText='Click "Run Query" to load traces.' />
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.LIST && (
|
||||
<div className={styles.view}>
|
||||
<ListView
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TRACE && (
|
||||
<div className={styles.view}>
|
||||
<TracesView
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TIMESERIES && (
|
||||
<div className={styles.view}>
|
||||
<TimeSeriesView
|
||||
dataSource={DataSource.TRACES}
|
||||
isFilterApplied={isFilterApplied}
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCancelled && selectedView === ExplorerViews.TABLE && (
|
||||
<div className={styles.view}>
|
||||
<TableView
|
||||
setWarning={setWarning}
|
||||
setIsLoadingQueries={setIsLoadingQueries}
|
||||
queryKeyRef={listQueryKeyRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--typography-color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.orderByContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.orderByLabel {
|
||||
color: var(--muted-foreground);
|
||||
// Between --periscope-font-size-small (11px) and -base (13px), so literal.
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 133.333% */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { getDefaultPaginationConfig } from 'hooks/queryPagination/utils';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { ArrowUp10, Minus } from '@signozhq/icons';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
import { defaultSelectedColumns, PER_PAGE_OPTIONS } from '../constants';
|
||||
import ExplorerControls from '../Controls/Controls';
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import { TracesLoading } from '../TraceLoading/TraceLoading';
|
||||
import styles from './ListView.module.scss';
|
||||
import { useListTableColumns } from './useListTableColumns';
|
||||
import { TraceListRow } from '../tableUtils';
|
||||
import { getTraceLink, getTraceRowKey, transformDataWithDate } from './utils';
|
||||
|
||||
interface ListViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}
|
||||
|
||||
function ListView({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: ListViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType: panelTypeFromQueryBuilder } =
|
||||
useQueryBuilder();
|
||||
|
||||
const panelType = panelTypeFromQueryBuilder || PANEL_TYPES.LIST;
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
loading: timeRangeUpdateLoading,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
initialOptions: {
|
||||
selectColumns: defaultSelectedColumns,
|
||||
},
|
||||
});
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
const paginationConfig =
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
// TEMP — remove after traces moves to TanStack table.
|
||||
// - Drag updates selectColumns; raw queryKey would churn on reorder.
|
||||
// - Trace API fetches only listed columns → add/remove must refetch.
|
||||
// - Sorted-name signature: stable on reorder, changes on add/remove.
|
||||
const selectColumnsSignature = useMemo(
|
||||
() =>
|
||||
(options?.selectColumns ?? [])
|
||||
.map((c) => c.name)
|
||||
.sort()
|
||||
.join(','),
|
||||
[options?.selectColumns],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
orderBy,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
panelType,
|
||||
globalSelectedTime,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
orderBy,
|
||||
],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isFetching, isLoading, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: requestQuery,
|
||||
graphType: panelType,
|
||||
selectedTime: 'GLOBAL_TIME' as const,
|
||||
globalSelectedInterval: globalSelectedTime as CustomTimeType,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationConfig,
|
||||
selectColumns: options?.selectColumns,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled:
|
||||
// don't make api call while the time range state in redux is loading
|
||||
!timeRangeUpdateLoading &&
|
||||
!!stagedQuery &&
|
||||
panelType === PANEL_TYPES.LIST &&
|
||||
!!options?.selectColumns?.length,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
const dataLength =
|
||||
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
||||
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
|
||||
|
||||
const queryTableDataResult = data?.payload?.data?.newResult?.data?.result;
|
||||
const queryTableData = useMemo(
|
||||
() => queryTableDataResult || [],
|
||||
[queryTableDataResult],
|
||||
);
|
||||
|
||||
const columns = useListTableColumns(options?.selectColumns || []);
|
||||
|
||||
const transformedQueryTableData = useMemo(
|
||||
() => transformDataWithDate(queryTableData) || [],
|
||||
[queryTableData],
|
||||
);
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleColumnOrderChange = useCallback(
|
||||
(reordered: TableColumnDef<TraceListRow>[]): void => {
|
||||
// Column ids are composite (fieldContext.name) — disambiguates same-name fields.
|
||||
config?.addColumn?.onReorder(reordered.map((column) => column.id));
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(row: TraceListRow): void => {
|
||||
safeNavigate(getTraceLink(row));
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
const handleRowClickNewTab = useCallback((row: TraceListRow): void => {
|
||||
window.open(getAbsoluteUrl(getTraceLink(row)), '_blank');
|
||||
}, []);
|
||||
|
||||
const handleOrderChange = useCallback((value: string) => {
|
||||
setOrderBy(value);
|
||||
}, []);
|
||||
|
||||
const isDataAbsent =
|
||||
!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
transformedQueryTableData.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
transformedQueryTableData.length !== 0
|
||||
) {
|
||||
void logEvent('AI Observability Explorer: Data present', {
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
}, [isLoading, isFetching, isError, transformedQueryTableData, panelType]);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.orderByContainer}>
|
||||
<div className={styles.orderByLabel}>
|
||||
Order by <Minus size={14} /> <ArrowUp10 size={14} />
|
||||
</div>
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={handleOrderChange}
|
||||
dataSource={DataSource.TRACES}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
selectedColumns={options?.selectColumns}
|
||||
/>
|
||||
|
||||
<ExplorerControls
|
||||
isLoading={isFetching}
|
||||
totalCount={totalCount}
|
||||
config={config}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
|
||||
{(isLoading || (isFetching && transformedQueryTableData.length === 0)) && (
|
||||
<TracesLoading />
|
||||
)}
|
||||
|
||||
{isDataAbsent && !isFilterApplied && (
|
||||
<NoLogs dataSource={DataSource.TRACES} />
|
||||
)}
|
||||
|
||||
{isDataAbsent && isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="LIST" />
|
||||
)}
|
||||
|
||||
{!isError && transformedQueryTableData.length !== 0 && (
|
||||
<TanStackTable<TraceListRow>
|
||||
data={transformedQueryTableData}
|
||||
columns={columns}
|
||||
columnStorageKey={LOCALSTORAGE.AI_OBSERVABILITY_LIST_COLUMNS}
|
||||
respectColumnOrder={false}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onColumnRemove={config?.addColumn?.onRemove}
|
||||
isLoading={isFetching}
|
||||
getRowKey={getTraceRowKey}
|
||||
onRowClick={handleRowClick}
|
||||
onRowClickNewTab={handleRowClickNewTab}
|
||||
// Rows are already page-sized by the query; virtualizing would need a
|
||||
// fixed-height container this page doesn't provide.
|
||||
disableVirtualScroll
|
||||
testId="ai-observability-list-view-table"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ListView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(ListView);
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import { formatCellValue, TraceListRow } from '../tableUtils';
|
||||
|
||||
/** Older callers passed `{ key, type }` where v5 uses `{ name, fieldContext }`. */
|
||||
interface LegacyFieldKey {
|
||||
key?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
const BADGE_FIELDS = new Set([
|
||||
'httpMethod',
|
||||
'responseStatusCode',
|
||||
'response_status_code',
|
||||
'http_method',
|
||||
]);
|
||||
|
||||
const DURATION_FIELDS = new Set(['durationNano', 'duration_nano']);
|
||||
|
||||
const TIMESTAMP_COLUMN_ID = 'date';
|
||||
|
||||
/**
|
||||
* Column definitions for the list view's TanStack table.
|
||||
*
|
||||
* Cell renderers must tolerate missing values: the table feeds skeleton
|
||||
* placeholder rows through them before the first response lands.
|
||||
*/
|
||||
export function useListTableColumns(
|
||||
selectedColumns: TelemetryFieldKey[],
|
||||
): TableColumnDef<TraceListRow>[] {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
return useMemo<TableColumnDef<TraceListRow>[]>(() => {
|
||||
const timestampColumn: TableColumnDef<TraceListRow> = {
|
||||
id: TIMESTAMP_COLUMN_ID,
|
||||
header: 'Timestamp',
|
||||
accessorFn: (row): unknown => row?.date,
|
||||
canBeHidden: false,
|
||||
enableRemove: false,
|
||||
width: { default: 145, min: 145 },
|
||||
cell: ({ value }): ReactElement => {
|
||||
const timestamp = value as string | number | undefined;
|
||||
if (timestamp === undefined || timestamp === null) {
|
||||
return <TanStackTable.Text> </TanStackTable.Text>;
|
||||
}
|
||||
const formatted =
|
||||
typeof timestamp === 'string'
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
timestamp,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
)
|
||||
: formatTimezoneAdjustedTimestamp(
|
||||
timestamp / 1e6,
|
||||
DATE_TIME_FORMATS.ISO_DATETIME_MS,
|
||||
);
|
||||
return <TanStackTable.Text>{String(formatted)}</TanStackTable.Text>;
|
||||
},
|
||||
};
|
||||
|
||||
const fieldColumns = selectedColumns.map(
|
||||
(field): TableColumnDef<TraceListRow> => {
|
||||
const legacy = field as TelemetryFieldKey & LegacyFieldKey;
|
||||
const name = field?.name || legacy?.key || '';
|
||||
const fieldContext = field?.fieldContext || legacy?.type;
|
||||
|
||||
return {
|
||||
id: buildCompositeKey(name, fieldContext),
|
||||
header: name,
|
||||
accessorFn: (row): unknown => row?.[name],
|
||||
enableRemove: true,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return <TanStackTable.Text data-testid={name}>N/A</TanStackTable.Text>;
|
||||
}
|
||||
|
||||
if (BADGE_FIELDS.has(name)) {
|
||||
return (
|
||||
<Badge data-testid={name} color="sakura" variant="outline">
|
||||
{formatCellValue(value)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (DURATION_FIELDS.has(name)) {
|
||||
return (
|
||||
<TanStackTable.Text data-testid={name}>
|
||||
{getMs(formatCellValue(value))}ms
|
||||
</TanStackTable.Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span data-testid={name}>
|
||||
<LineClampedText text={formatCellValue(value)} lines={3} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return [timestampColumn, ...fieldColumns];
|
||||
}, [selectedColumns, formatTimezoneAdjustedTimestamp]);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import { formUrlParams } from 'container/TraceDetail/utils';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { formatCellValue, TraceListRow } from '../tableUtils';
|
||||
|
||||
/** Rows carry the span's attributes plus `date` (the list item's timestamp). */
|
||||
export const transformDataWithDate = (data: QueryDataV3[]): TraceListRow[] =>
|
||||
data[0]?.list?.map(({ data, timestamp }) => ({ ...data, date: timestamp })) ||
|
||||
[];
|
||||
|
||||
export const getTraceLink = (record: TraceListRow): string =>
|
||||
`${ROUTES.TRACE}/${formatCellValue(record.traceID || record.trace_id)}${formUrlParams(
|
||||
{
|
||||
spanId: record.spanID || record.span_id,
|
||||
levelUp: 0,
|
||||
levelDown: 0,
|
||||
},
|
||||
)}`;
|
||||
|
||||
/**
|
||||
* Row identity for the table. Spans are unique per row; the trace id is the
|
||||
* fallback for root-only rows.
|
||||
*/
|
||||
export const getTraceRowKey = (record: TraceListRow): string =>
|
||||
formatCellValue(
|
||||
record?.spanID ?? record?.span_id ?? record?.traceID ?? record?.trace_id,
|
||||
);
|
||||
@@ -0,0 +1,61 @@
|
||||
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 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
|
||||
isListViewPanel={isListViewPanel}
|
||||
showTraceOperator
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
queryComponents={queryComponents}
|
||||
panelType={panelTypes}
|
||||
filterConfigs={filterConfigs}
|
||||
showOnlyWhereClause={
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||
}
|
||||
version="v3"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(QuerySection);
|
||||
@@ -0,0 +1,7 @@
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: var(--spacing-6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Space } from 'antd';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ExportMenu from 'components/ExportMenu/ExportMenu';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import styles from './TableView.module.scss';
|
||||
|
||||
function TableView({
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: {
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
[globalSelectedTime, maxTime, minTime, stagedQuery],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
graphType: panelType || PANEL_TYPES.TABLE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TABLE,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
const queryTableData = useMemo(
|
||||
() =>
|
||||
data?.payload?.data?.newResult?.data?.result ||
|
||||
data?.payload.data.result ||
|
||||
[],
|
||||
[data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
return (
|
||||
<Space.Compact block direction="vertical">
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
{!isError && data && (
|
||||
<div className={styles.header}>
|
||||
<ExportMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
data={data}
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
fileName="traces-table"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isError && (
|
||||
<QueryTable
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
queryTableData={queryTableData as QueryDataV3[]}
|
||||
loading={isLoading}
|
||||
sticky
|
||||
/>
|
||||
)}
|
||||
</Space.Compact>
|
||||
);
|
||||
}
|
||||
|
||||
TableView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(TableView);
|
||||
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
function TimeSeriesViewContainer({
|
||||
dataSource = DataSource.TRACES,
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const isValidToConvertToMs = useMemo(() => {
|
||||
const isValid: boolean[] = [];
|
||||
|
||||
currentQuery.builder.queryData.forEach(
|
||||
({ aggregateAttribute, aggregateOperator }) => {
|
||||
const isExistDurationNanoAttribute =
|
||||
aggregateAttribute?.key === 'durationNano' ||
|
||||
aggregateAttribute?.key === 'duration_nano';
|
||||
|
||||
const isCountOperator =
|
||||
aggregateOperator === 'count' || aggregateOperator === 'count_distinct';
|
||||
|
||||
isValid.push(!isCountOperator && isExistDurationNanoAttribute);
|
||||
},
|
||||
);
|
||||
|
||||
return isValid.every(Boolean);
|
||||
}, [currentQuery]);
|
||||
|
||||
const defaultUnit = isValidToConvertToMs ? 'ms' : 'short';
|
||||
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit(defaultUnit);
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
[globalSelectedTime, maxTime, minTime, stagedQuery],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap[dataSource],
|
||||
graphType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
const responseData = useMemo(
|
||||
() => (isValidToConvertToMs ? convertDataValueToMs(data) : data),
|
||||
[data, isValidToConvertToMs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TimeSeriesView
|
||||
isFilterApplied={isFilterApplied}
|
||||
isError={isError}
|
||||
error={error as APIError}
|
||||
isLoading={isLoading || isFetching}
|
||||
data={responseData}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onYAxisUnitChange={onUnitChange}
|
||||
dataSource={dataSource}
|
||||
setWarning={setWarning}
|
||||
allowExport
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimeSeriesViewProps {
|
||||
dataSource?: DataSource;
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}
|
||||
|
||||
TimeSeriesViewContainer.defaultProps = {
|
||||
dataSource: DataSource.TRACES,
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default TimeSeriesViewContainer;
|
||||
@@ -0,0 +1,19 @@
|
||||
.loadingTraces {
|
||||
padding: var(--spacing-12) 0;
|
||||
height: 240px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gif {
|
||||
height: 72px;
|
||||
margin-left: calc(var(--spacing-12) * -1);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import loadingPlaneUrl from '@/assets/Icons/loading-plane.gif';
|
||||
|
||||
import styles from './TraceLoading.module.scss';
|
||||
|
||||
export function TracesLoading(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
return (
|
||||
<div className={styles.loadingTraces}>
|
||||
<div className={styles.content}>
|
||||
<img className={styles.gif} src={loadingPlaneUrl} alt="wait-icon" />
|
||||
|
||||
<Typography>
|
||||
{t('pending_data_placeholder', { dataSource: DataSource.TRACES })}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// The traces explorer borrowed this rule from ListView's global stylesheet;
|
||||
// the fork keeps its own copy so the two can't drift into each other.
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
MutableRefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import { PER_PAGE_OPTIONS } from '../constants';
|
||||
import ExplorerControls from '../Controls/Controls';
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import { TraceListRow } from '../tableUtils';
|
||||
import { TracesLoading } from '../TraceLoading/TraceLoading';
|
||||
import { columns } from './configs';
|
||||
import { ActionsContainer, Container } from './styles';
|
||||
import styles from './TracesView.module.scss';
|
||||
import { getRootSpanRowKey } from './utils';
|
||||
|
||||
interface TracesViewProps {
|
||||
isFilterApplied: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
|
||||
queryKeyRef?: MutableRefObject<QueryKey | undefined>;
|
||||
}
|
||||
|
||||
function TracesView({
|
||||
isFilterApplied,
|
||||
setWarning,
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TracesViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
|
||||
const transformedQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces),
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
[
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
);
|
||||
|
||||
if (queryKeyRef) {
|
||||
queryKeyRef.current = queryKey;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
|
||||
{
|
||||
query: transformedQuery,
|
||||
graphType: panelType || PANEL_TYPES.TRACE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationQueryData,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TRACE,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload) {
|
||||
setWarning(data?.warning);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload, data?.warning]);
|
||||
|
||||
const responseData = data?.payload?.data?.newResult?.data?.result[0]?.list;
|
||||
const tableData = useMemo(
|
||||
(): TraceListRow[] => responseData?.map((listItem) => listItem.data) ?? [],
|
||||
[responseData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || isFetching) {
|
||||
setIsLoadingQueries(true);
|
||||
} else {
|
||||
setIsLoadingQueries(false);
|
||||
}
|
||||
}, [isLoading, isFetching, setIsLoadingQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isFetching && !isError && tableData.length !== 0) {
|
||||
void logEvent('AI Observability Explorer: Data present', {
|
||||
panelType: 'TRACE',
|
||||
});
|
||||
}
|
||||
}, [isLoading, isFetching, isError, panelType, tableData]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{tableData.length !== 0 && (
|
||||
<ActionsContainer>
|
||||
<Typography>
|
||||
This tab only shows Root Spans. More details
|
||||
<Typography.Link href={DOCLINKS.TRACES_DETAILS_LINK} target="_blank">
|
||||
{' '}
|
||||
here
|
||||
</Typography.Link>
|
||||
</Typography>
|
||||
|
||||
<div className={styles.controls}>
|
||||
<DownloadOptionsMenu
|
||||
dataSource={DataSource.TRACES}
|
||||
panelType={PANEL_TYPES.TRACE}
|
||||
/>
|
||||
|
||||
<ExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={responseData?.length || 0}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
</ActionsContainer>
|
||||
)}
|
||||
|
||||
{isError && error && <ErrorInPlace error={error as APIError} />}
|
||||
|
||||
{(isLoading || (isFetching && tableData.length === 0)) && <TracesLoading />}
|
||||
|
||||
{!isLoading &&
|
||||
!isFetching &&
|
||||
!isError &&
|
||||
!isFilterApplied &&
|
||||
tableData.length === 0 && <NoLogs dataSource={DataSource.TRACES} />}
|
||||
|
||||
{!isLoading &&
|
||||
!isFetching &&
|
||||
tableData.length === 0 &&
|
||||
!isError &&
|
||||
isFilterApplied && (
|
||||
<EmptyLogsSearch dataSource={DataSource.TRACES} panelType="TRACE" />
|
||||
)}
|
||||
|
||||
{tableData.length !== 0 && (
|
||||
<TanStackTable<TraceListRow>
|
||||
data={tableData}
|
||||
columns={columns}
|
||||
columnStorageKey={LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS}
|
||||
isLoading={isLoading}
|
||||
getRowKey={getRootSpanRowKey}
|
||||
// Rows are already page-sized by the query; virtualizing would need a
|
||||
// fixed-height container this page doesn't provide.
|
||||
disableVirtualScroll
|
||||
testId="ai-observability-traces-view-table"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
TracesView.defaultProps = {
|
||||
queryKeyRef: undefined,
|
||||
};
|
||||
|
||||
export default memo(TracesView);
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
|
||||
import { formatCellValue, TraceListRow } from '../tableUtils';
|
||||
|
||||
/**
|
||||
* Root-span columns. Fixed set — unlike the list view there are no user-selected
|
||||
* columns here, so nothing can be removed (there'd be no preference to sync to).
|
||||
*/
|
||||
export const columns: TableColumnDef<TraceListRow>[] = [
|
||||
{
|
||||
id: 'serviceName',
|
||||
header: 'Root Service Name',
|
||||
accessorFn: (row): unknown => row?.['service.name'],
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
<TanStackTable.Text>{formatCellValue(value)}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Root Operation Name',
|
||||
accessorFn: (row): unknown => row?.name,
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
<TanStackTable.Text>{formatCellValue(value)}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'durationNano',
|
||||
header: 'Root Duration (in ms)',
|
||||
accessorFn: (row): unknown => row?.duration_nano,
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
<TanStackTable.Text>
|
||||
{value === undefined || value === null
|
||||
? ''
|
||||
: `${getMs(formatCellValue(value))}ms`}
|
||||
</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'span_count',
|
||||
header: 'No of Spans',
|
||||
accessorFn: (row): unknown => row?.span_count,
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => (
|
||||
<TanStackTable.Text>{formatCellValue(value)}</TanStackTable.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'traceID',
|
||||
header: 'TraceID',
|
||||
accessorFn: (row): unknown => row?.trace_id,
|
||||
enableRemove: false,
|
||||
width: { min: 145 },
|
||||
cell: ({ value }): ReactElement => {
|
||||
const traceID = formatCellValue(value);
|
||||
if (!traceID) {
|
||||
return <TanStackTable.Text> </TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
to={generatePath(ROUTES.TRACE_DETAIL, { id: traceID })}
|
||||
data-testid="trace-id"
|
||||
>
|
||||
{traceID}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const ActionsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { formatCellValue, TraceListRow } from '../tableUtils';
|
||||
|
||||
/**
|
||||
* Row identity for the root-span table: one row per trace. Tolerates the
|
||||
* skeleton placeholder rows the table renders before the first response.
|
||||
*/
|
||||
export const getRootSpanRowKey = (record: TraceListRow): string =>
|
||||
formatCellValue(record?.trace_id ?? record?.traceID);
|
||||
@@ -0,0 +1,49 @@
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
export const TOOLBAR_VIEWS = {
|
||||
list: {
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
show: true,
|
||||
key: 'list',
|
||||
},
|
||||
timeseries: {
|
||||
name: 'timeseries',
|
||||
label: 'Timeseries',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'timeseries',
|
||||
},
|
||||
trace: {
|
||||
name: 'trace',
|
||||
label: 'Trace',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'trace',
|
||||
},
|
||||
table: {
|
||||
name: 'table',
|
||||
label: 'Table',
|
||||
disabled: false,
|
||||
show: true,
|
||||
key: 'table',
|
||||
},
|
||||
clickhouse: {
|
||||
name: 'clickhouse',
|
||||
label: 'Clickhouse',
|
||||
disabled: false,
|
||||
show: false,
|
||||
key: 'clickhouse',
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultSelectedColumns: string[] = [
|
||||
'service.name',
|
||||
'name',
|
||||
'duration_nano',
|
||||
'http_method',
|
||||
'response_status_code',
|
||||
'timestamp',
|
||||
];
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||
@@ -0,0 +1,75 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { cloneDeep, set } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const getListViewQuery = (
|
||||
stagedQuery: Query,
|
||||
orderBy?: string,
|
||||
): Query => {
|
||||
const query = stagedQuery
|
||||
? cloneDeep(stagedQuery)
|
||||
: cloneDeep(initialQueriesMap.traces);
|
||||
|
||||
const orderByPayload: OrderByPayload[] = orderBy
|
||||
? [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
const queryData = query.builder.queryData[i];
|
||||
queryData.groupBy = [];
|
||||
queryData.having = {
|
||||
expression: '',
|
||||
};
|
||||
queryData.orderBy = orderByPayload;
|
||||
}
|
||||
|
||||
if (
|
||||
query.builder.queryTraceOperator &&
|
||||
query.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
for (let i = 0; i < query.builder.queryTraceOperator.length; i++) {
|
||||
const queryTraceOperator = query.builder.queryTraceOperator[i];
|
||||
queryTraceOperator.groupBy = [];
|
||||
queryTraceOperator.having = {
|
||||
expression: '',
|
||||
};
|
||||
queryTraceOperator.orderBy = orderByPayload;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const getQueryByPanelType = (
|
||||
stagedQuery: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE) {
|
||||
return getListViewQuery(stagedQuery);
|
||||
}
|
||||
return stagedQuery;
|
||||
};
|
||||
|
||||
export const getExportQueryData = (
|
||||
query: Query,
|
||||
panelType: PANEL_TYPES,
|
||||
options: OptionsQuery,
|
||||
): Query => {
|
||||
if (panelType === PANEL_TYPES.LIST) {
|
||||
const updatedQuery = cloneDeep(query);
|
||||
set(
|
||||
updatedQuery,
|
||||
'builder.queryData[0].selectColumns',
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
return updatedQuery;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* A table row: the span's flattened attributes. Which keys are present depends
|
||||
* on the query and the selected columns, so every field access has to tolerate
|
||||
* `undefined` — including the skeleton placeholder rows the table renders
|
||||
* before the first response lands.
|
||||
*/
|
||||
export type TraceListRow = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Renders a dynamic row value as text. Values are whatever the span carried, so
|
||||
* objects (a JSON body, say) are serialised rather than left to stringify to
|
||||
* `[object Object]`.
|
||||
*/
|
||||
export const formatCellValue = (value: unknown): string => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value);
|
||||
}
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -18,6 +18,13 @@ jest.mock('pages/DashboardPageV2/DashboardContainer', () => ({
|
||||
default: (): JSX.Element => <div data-testid="llm-overview-dashboard" />,
|
||||
}));
|
||||
|
||||
// Same for the Explorer tab, which renders a full query-builder surface
|
||||
// (toolbar + date picker).
|
||||
jest.mock('container/LLMObservability/Explorer/Explorer', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="llm-observability-explorer" />,
|
||||
}));
|
||||
|
||||
function setupList(items = mockRules): void {
|
||||
server.use(
|
||||
rest.get(LLM_PRICING_ENDPOINT, (_req, res, ctx) =>
|
||||
|
||||
@@ -17,8 +17,6 @@ import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
getNodeQueryPayload,
|
||||
@@ -53,23 +51,12 @@ function NodeMetrics({
|
||||
};
|
||||
}, [timestamp]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const queryPayloads = useMemo(() => {
|
||||
if (nodeName) {
|
||||
return getNodeQueryPayload(
|
||||
clusterName,
|
||||
nodeName,
|
||||
start,
|
||||
end,
|
||||
dotMetricsEnabled,
|
||||
);
|
||||
return getNodeQueryPayload(clusterName, nodeName, start, end);
|
||||
}
|
||||
return getHostQueryPayload(hostName, start, end, dotMetricsEnabled);
|
||||
}, [nodeName, hostName, clusterName, start, end, dotMetricsEnabled]);
|
||||
return getHostQueryPayload(hostName, start, end);
|
||||
}, [nodeName, hostName, clusterName, start, end]);
|
||||
|
||||
const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
|
||||
const queries = useQueries(
|
||||
|
||||
@@ -12,13 +12,11 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { getPodQueryPayload, podWidgetInfo } from './constants';
|
||||
|
||||
function PodMetrics({
|
||||
@@ -54,14 +52,9 @@ function PodMetrics({
|
||||
scrollLeft: 0,
|
||||
});
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const queryPayloads = useMemo(
|
||||
() => getPodQueryPayload(clusterName, podName, start, end, dotMetricsEnabled),
|
||||
[clusterName, end, podName, start, dotMetricsEnabled],
|
||||
() => getPodQueryPayload(clusterName, podName, start, end),
|
||||
[clusterName, end, podName, start],
|
||||
);
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload) => ({
|
||||
|
||||
@@ -1,56 +1,39 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import type { Having } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import type { Having as HavingV5 } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
|
||||
const buildSumGreaterThanZeroHaving = (
|
||||
metricKey: string,
|
||||
useV5HavingFormat: boolean,
|
||||
): Having[] | HavingV5 =>
|
||||
useV5HavingFormat
|
||||
? { expression: `sum(${metricKey}) > 0` }
|
||||
: [{ columnName: `SUM(${metricKey})`, op: '>', value: 0 }];
|
||||
|
||||
export const getPodQueryPayload = (
|
||||
clusterName: string,
|
||||
podName: string,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sClusterNameKey = dotMetricsEnabled
|
||||
? 'k8s.cluster.name'
|
||||
: 'k8s_cluster_name';
|
||||
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
|
||||
const containerCpuUtilKey = dotMetricsEnabled
|
||||
? 'container.cpu.usage'
|
||||
: 'container_cpu_usage';
|
||||
const containerMemUsageKey = dotMetricsEnabled
|
||||
? 'container.memory.usage'
|
||||
: 'container_memory_usage';
|
||||
const k8sContainerCpuReqKey = dotMetricsEnabled
|
||||
? 'k8s.container.cpu_request'
|
||||
: 'k8s_container_cpu_request';
|
||||
const k8sContainerCpuLimitKey = dotMetricsEnabled
|
||||
? 'k8s.container.cpu_limit'
|
||||
: 'k8s_container_cpu_limit';
|
||||
const k8sContainerMemReqKey = dotMetricsEnabled
|
||||
? 'k8s.container.memory_request'
|
||||
: 'k8s_container_memory_request';
|
||||
const k8sContainerMemLimitKey = dotMetricsEnabled
|
||||
? 'k8s.container.memory_limit'
|
||||
: 'k8s_container_memory_limit';
|
||||
const k8sPodFsAvailKey = dotMetricsEnabled
|
||||
? 'k8s.pod.filesystem.available'
|
||||
: 'k8s_pod_filesystem_available';
|
||||
const k8sPodFsCapKey = dotMetricsEnabled
|
||||
? 'k8s.pod.filesystem.capacity'
|
||||
: 'k8s_pod_filesystem_capacity';
|
||||
const k8sPodNetIoKey = dotMetricsEnabled
|
||||
? 'k8s.pod.network.io'
|
||||
: 'k8s_pod_network_io';
|
||||
const podLegendTemplate = dotMetricsEnabled
|
||||
? '{{k8s.pod.name}}'
|
||||
: '{{k8s_pod_name}}';
|
||||
const podLegendUsage = dotMetricsEnabled
|
||||
? 'usage - {{k8s.pod.name}}'
|
||||
: 'usage - {{k8s_pod_name}}';
|
||||
const podLegendLimit = dotMetricsEnabled
|
||||
? 'limit - {{k8s.pod.name}}'
|
||||
: 'limit - {{k8s_pod_name}}';
|
||||
const k8sClusterNameKey = 'k8s.cluster.name';
|
||||
const k8sPodNameKey = 'k8s.pod.name';
|
||||
const containerCpuUtilKey = 'container.cpu.usage';
|
||||
const containerMemUsageKey = 'container.memory.usage';
|
||||
const k8sContainerCpuReqKey = 'k8s.container.cpu_request';
|
||||
const k8sContainerCpuLimitKey = 'k8s.container.cpu_limit';
|
||||
const k8sContainerMemReqKey = 'k8s.container.memory_request';
|
||||
const k8sContainerMemLimitKey = 'k8s.container.memory_limit';
|
||||
const k8sPodFsAvailKey = 'k8s.pod.filesystem.available';
|
||||
const k8sPodFsCapKey = 'k8s.pod.filesystem.capacity';
|
||||
const k8sPodNetIoKey = 'k8s.pod.network.io';
|
||||
const podLegendTemplate = '{{k8s.pod.name}}';
|
||||
const podLegendUsage = 'usage - {{k8s.pod.name}}';
|
||||
const podLegendLimit = 'limit - {{k8s.pod.name}}';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1027,36 +1010,17 @@ export const getNodeQueryPayload = (
|
||||
nodeName: string,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
): GetQueryResultsProps[] => {
|
||||
const k8sClusterNameKey = dotMetricsEnabled
|
||||
? 'k8s.cluster.name'
|
||||
: 'k8s_cluster_name';
|
||||
const k8sNodeNameKey = dotMetricsEnabled ? 'k8s.node.name' : 'k8s_node_name';
|
||||
const k8sNodeCpuTimeKey = dotMetricsEnabled
|
||||
? 'k8s.node.cpu.time'
|
||||
: 'k8s_node_cpu_time';
|
||||
const k8sNodeAllocCpuKey = dotMetricsEnabled
|
||||
? 'k8s.node.allocatable_cpu'
|
||||
: 'k8s_node_allocatable_cpu';
|
||||
const k8sNodeMemWsKey = dotMetricsEnabled
|
||||
? 'k8s.node.memory.working_set'
|
||||
: 'k8s_node_memory_working_set';
|
||||
const k8sNodeAllocMemKey = dotMetricsEnabled
|
||||
? 'k8s.node.allocatable_memory'
|
||||
: 'k8s_node_allocatable_memory';
|
||||
const k8sNodeNetIoKey = dotMetricsEnabled
|
||||
? 'k8s.node.network.io'
|
||||
: 'k8s_node_network_io';
|
||||
const k8sNodeFsAvailKey = dotMetricsEnabled
|
||||
? 'k8s.node.filesystem.available'
|
||||
: 'k8s_node_filesystem_available';
|
||||
const k8sNodeFsCapKey = dotMetricsEnabled
|
||||
? 'k8s.node.filesystem.capacity'
|
||||
: 'k8s_node_filesystem_capacity';
|
||||
const podLegend = dotMetricsEnabled
|
||||
? '{{k8s.node.name}}'
|
||||
: '{{k8s_node_name}}';
|
||||
const k8sClusterNameKey = 'k8s.cluster.name';
|
||||
const k8sNodeNameKey = 'k8s.node.name';
|
||||
const k8sNodeCpuTimeKey = 'k8s.node.cpu.time';
|
||||
const k8sNodeAllocCpuKey = 'k8s.node.allocatable_cpu';
|
||||
const k8sNodeMemWsKey = 'k8s.node.memory.working_set';
|
||||
const k8sNodeAllocMemKey = 'k8s.node.allocatable_memory';
|
||||
const k8sNodeNetIoKey = 'k8s.node.network.io';
|
||||
const k8sNodeFsAvailKey = 'k8s.node.filesystem.available';
|
||||
const k8sNodeFsCapKey = 'k8s.node.filesystem.capacity';
|
||||
const podLegend = '{{k8s.node.name}}';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1586,48 +1550,24 @@ export const getHostQueryPayload = (
|
||||
hostName: string,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
useV5HavingFormat = false,
|
||||
): GetQueryResultsProps[] => {
|
||||
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
|
||||
const cpuTimeKey = dotMetricsEnabled ? 'system.cpu.time' : 'system_cpu_time';
|
||||
const memUsageKey = dotMetricsEnabled
|
||||
? 'system.memory.usage'
|
||||
: 'system_memory_usage';
|
||||
const load1mKey = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.1m'
|
||||
: 'system_cpu_load_average_1m';
|
||||
const load5mKey = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.5m'
|
||||
: 'system_cpu_load_average_5m';
|
||||
const load15mKey = dotMetricsEnabled
|
||||
? 'system.cpu.load_average.15m'
|
||||
: 'system_cpu_load_average_15m';
|
||||
const netIoKey = dotMetricsEnabled ? 'system.network.io' : 'system_network_io';
|
||||
const netPktsKey = dotMetricsEnabled
|
||||
? 'system.network.packets'
|
||||
: 'system_network_packets';
|
||||
const netErrKey = dotMetricsEnabled
|
||||
? 'system.network.errors'
|
||||
: 'system_network_errors';
|
||||
const netDropKey = dotMetricsEnabled
|
||||
? 'system.network.dropped'
|
||||
: 'system_network_dropped';
|
||||
const netConnKey = dotMetricsEnabled
|
||||
? 'system.network.connections'
|
||||
: 'system_network_connections';
|
||||
const diskIoKey = dotMetricsEnabled ? 'system.disk.io' : 'system_disk_io';
|
||||
const diskOpTimeKey = dotMetricsEnabled
|
||||
? 'system.disk.operation_time'
|
||||
: 'system_disk_operation_time';
|
||||
const diskOpsKey = dotMetricsEnabled
|
||||
? 'system.disk.operations'
|
||||
: 'system_disk_operations';
|
||||
const diskPendingKey = dotMetricsEnabled
|
||||
? 'system.disk.pending_operations'
|
||||
: 'system_disk_pending_operations';
|
||||
const fsUsageKey = dotMetricsEnabled
|
||||
? 'system.filesystem.usage'
|
||||
: 'system_filesystem_usage';
|
||||
const hostNameKey = 'host.name';
|
||||
const cpuTimeKey = 'system.cpu.time';
|
||||
const memUsageKey = 'system.memory.usage';
|
||||
const load1mKey = 'system.cpu.load_average.1m';
|
||||
const load5mKey = 'system.cpu.load_average.5m';
|
||||
const load15mKey = 'system.cpu.load_average.15m';
|
||||
const netIoKey = 'system.network.io';
|
||||
const netPktsKey = 'system.network.packets';
|
||||
const netErrKey = 'system.network.errors';
|
||||
const netDropKey = 'system.network.dropped';
|
||||
const netConnKey = 'system.network.connections';
|
||||
const diskIoKey = 'system.disk.io';
|
||||
const diskOpTimeKey = 'system.disk.operation_time';
|
||||
const diskOpsKey = 'system.disk.operations';
|
||||
const diskPendingKey = 'system.disk.pending_operations';
|
||||
const fsUsageKey = 'system.filesystem.usage';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1873,13 +1813,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${fsUsageKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
|
||||
legend: '{{mountpoint}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -1928,13 +1862,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${fsUsageKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
|
||||
legend: '{{mountpoint}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2160,13 +2088,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${netIoKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(netIoKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: 30,
|
||||
orderBy: [],
|
||||
@@ -2622,13 +2544,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskOpsKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskOpsKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2697,13 +2613,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskPendingKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskPendingKey, useV5HavingFormat),
|
||||
legend: '{{device}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
@@ -2779,13 +2689,7 @@ export const getHostQueryPayload = (
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
having: [
|
||||
{
|
||||
columnName: `SUM(${diskOpTimeKey})`,
|
||||
op: '>',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
having: buildSumGreaterThanZeroHaving(diskOpTimeKey, useV5HavingFormat),
|
||||
legend: '{{device}}::{{direction}}',
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
|
||||
@@ -21,7 +21,6 @@ export const databaseCallsRPS = ({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: DatabaseCallsRPSProps): QueryBuilderData => {
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
{
|
||||
@@ -33,7 +32,7 @@ export const databaseCallsRPS = ({
|
||||
const groupBy: BaseAutocompleteData[] = [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled ? WidgetKeys.Db_system : WidgetKeys.Db_system_norm,
|
||||
key: WidgetKeys.DbSystem,
|
||||
type: 'tag',
|
||||
},
|
||||
];
|
||||
@@ -42,9 +41,7 @@ export const databaseCallsRPS = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -75,7 +72,6 @@ export const databaseCallsRPS = ({
|
||||
export const databaseCallsAvgDuration = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: DatabaseCallProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
key: WidgetKeys.SignozDbLatencySum,
|
||||
@@ -92,9 +88,7 @@ export const databaseCallsAvgDuration = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@ export const externalCallErrorPercent = ({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
key: WidgetKeys.SignozExternalCallLatencyCount,
|
||||
@@ -49,9 +48,7 @@ export const externalCallErrorPercent = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -61,7 +58,7 @@ export const externalCallErrorPercent = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
dataType: DataTypes.Int64,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -74,9 +71,7 @@ export const externalCallErrorPercent = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -120,7 +115,6 @@ export const externalCallErrorPercent = ({
|
||||
export const externalCallDuration = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: ExternalCallProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
dataType: DataTypes.Float64,
|
||||
@@ -141,9 +135,7 @@ export const externalCallDuration = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -183,7 +175,6 @@ export const externalCallRpsByAddress = ({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
{
|
||||
@@ -198,9 +189,7 @@ export const externalCallRpsByAddress = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -231,7 +220,6 @@ export const externalCallDurationByAddress = ({
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
dataType: DataTypes.Float64,
|
||||
@@ -251,9 +239,7 @@ export const externalCallDurationByAddress = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
|
||||
@@ -37,15 +37,10 @@ export const latency = ({
|
||||
tagFilterItems,
|
||||
isSpanMetricEnable = false,
|
||||
topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
}: LatencyProps): QueryBuilderData => {
|
||||
const signozLatencyBucketMetrics = dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm;
|
||||
const signozLatencyBucketMetrics = WidgetKeys.SignozLatencyBucket;
|
||||
|
||||
const signozMetricsServiceName = dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm;
|
||||
const signozMetricsServiceName = WidgetKeys.OTelServiceName;
|
||||
const newAutoCompleteData: BaseAutocompleteData = {
|
||||
key: isSpanMetricEnable
|
||||
? signozLatencyBucketMetrics
|
||||
@@ -287,28 +282,21 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
threashold,
|
||||
delta,
|
||||
metricsBuckets,
|
||||
dotMetricsEnabled,
|
||||
}: ApDexMetricsQueryBuilderQueriesProps): QueryBuilderData => {
|
||||
const autoCompleteDataA: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.SignozLatencyCount
|
||||
: WidgetKeys.SignozLatencyCountNorm,
|
||||
key: WidgetKeys.SignozLatencyCount,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
|
||||
const autoCompleteDataB: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm,
|
||||
key: WidgetKeys.SignozLatencyBucket,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
|
||||
const autoCompleteDataC: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm,
|
||||
key: WidgetKeys.SignozLatencyBucket,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
@@ -317,9 +305,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -343,7 +329,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -363,9 +349,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -399,7 +383,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -409,9 +393,7 @@ export const apDexMetricsQueryBuilderQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -474,13 +456,10 @@ export const operationPerSec = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
dotMetricsEnabled,
|
||||
}: OperationPerSecProps): QueryBuilderData => {
|
||||
const autocompleteData: BaseAutocompleteData[] = [
|
||||
{
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.SignozLatencyCount
|
||||
: WidgetKeys.SignozLatencyCountNorm,
|
||||
key: WidgetKeys.SignozLatencyCount,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
},
|
||||
@@ -491,9 +470,7 @@ export const operationPerSec = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -534,7 +511,6 @@ export const errorPercentage = ({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations,
|
||||
dotMetricsEnabled,
|
||||
}: OperationPerSecProps): QueryBuilderData => {
|
||||
const autocompleteDataA: BaseAutocompleteData = {
|
||||
key: WidgetKeys.SignozCallsTotal,
|
||||
@@ -553,9 +529,7 @@ export const errorPercentage = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -575,7 +549,7 @@ export const errorPercentage = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
dataType: DataTypes.Int64,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
@@ -589,9 +563,7 @@ export const errorPercentage = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
|
||||
@@ -21,12 +21,9 @@ import { getQueryBuilderQuerieswithFormula } from './MetricsPageQueriesFactory';
|
||||
|
||||
export const topOperationQueries = ({
|
||||
servicename,
|
||||
dotMetricsEnabled,
|
||||
}: TopOperationQueryFactoryProps): QueryBuilderData => {
|
||||
const latencyAutoCompleteData: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm,
|
||||
key: WidgetKeys.SignozLatencyBucket,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
@@ -38,9 +35,7 @@ export const topOperationQueries = ({
|
||||
};
|
||||
|
||||
const numOfCallAutoCompleteData: BaseAutocompleteData = {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.SignozLatencyCount
|
||||
: WidgetKeys.SignozLatencyCountNorm,
|
||||
key: WidgetKeys.SignozLatencyCount,
|
||||
dataType: DataTypes.Float64,
|
||||
type: '',
|
||||
};
|
||||
@@ -49,9 +44,7 @@ export const topOperationQueries = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
dataType: DataTypes.String,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
@@ -65,9 +58,7 @@ export const topOperationQueries = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -77,7 +68,7 @@ export const topOperationQueries = ({
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.Int64,
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
|
||||
@@ -28,8 +28,6 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import {
|
||||
GraphTitle,
|
||||
MENU_ITEMS,
|
||||
@@ -89,12 +87,7 @@ function DBCall(): JSX.Element {
|
||||
[queries],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const legend = dotMetricsEnabled ? '{{db.system}}' : '{{db_system}}';
|
||||
const legend = '{{db.system}}';
|
||||
|
||||
const databaseCallsRPSWidget = useMemo(
|
||||
() =>
|
||||
@@ -106,7 +99,6 @@ function DBCall(): JSX.Element {
|
||||
servicename,
|
||||
legend,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -117,7 +109,7 @@ function DBCall(): JSX.Element {
|
||||
id: SERVICE_CHART_ID.dbCallsRPS,
|
||||
fillSpans: false,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled, legend],
|
||||
[servicename, tagFilterItems, legend],
|
||||
);
|
||||
const databaseCallsAverageDurationWidget = useMemo(
|
||||
() =>
|
||||
@@ -128,7 +120,6 @@ function DBCall(): JSX.Element {
|
||||
builder: databaseCallsAvgDuration({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -139,7 +130,7 @@ function DBCall(): JSX.Element {
|
||||
id: GraphTitle.DATABASE_CALLS_AVG_DURATION,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const stepInterval = useMemo(
|
||||
@@ -157,7 +148,7 @@ function DBCall(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
|
||||
@@ -30,8 +30,6 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import {
|
||||
GraphTitle,
|
||||
legend,
|
||||
@@ -84,10 +82,6 @@ function External(): JSX.Element {
|
||||
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
|
||||
[queries],
|
||||
);
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const externalCallErrorWidget = useMemo(
|
||||
() =>
|
||||
@@ -99,7 +93,6 @@ function External(): JSX.Element {
|
||||
servicename,
|
||||
legend: legend.address,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -109,7 +102,7 @@ function External(): JSX.Element {
|
||||
yAxisUnit: '%',
|
||||
id: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const selectedTraceTags = useMemo(
|
||||
@@ -126,7 +119,6 @@ function External(): JSX.Element {
|
||||
builder: externalCallDuration({
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -137,7 +129,7 @@ function External(): JSX.Element {
|
||||
id: GraphTitle.EXTERNAL_CALL_DURATION,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const errorApmToTraceQuery = useGetAPMToTracesQueries({
|
||||
@@ -171,7 +163,7 @@ function External(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
@@ -194,7 +186,6 @@ function External(): JSX.Element {
|
||||
servicename,
|
||||
legend: legend.address,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -205,7 +196,7 @@ function External(): JSX.Element {
|
||||
id: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const externalCallDurationAddressWidget = useMemo(
|
||||
@@ -218,7 +209,6 @@ function External(): JSX.Element {
|
||||
servicename,
|
||||
legend: legend.address,
|
||||
tagFilterItems,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -229,7 +219,7 @@ function External(): JSX.Element {
|
||||
id: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const apmToTraceQuery = useGetAPMToTracesQueries({
|
||||
|
||||
@@ -93,15 +93,12 @@ function Application(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[handleSetTimeStamp],
|
||||
);
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
logEvent('APM: Service detail page visited', {
|
||||
@@ -159,7 +156,6 @@ function Application(): JSX.Element {
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations: topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -169,7 +165,7 @@ function Application(): JSX.Element {
|
||||
yAxisUnit: 'ops',
|
||||
id: SERVICE_CHART_ID.rps,
|
||||
}),
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute],
|
||||
);
|
||||
|
||||
const errorPercentageWidget = useMemo(
|
||||
@@ -182,7 +178,6 @@ function Application(): JSX.Element {
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperations: topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -193,7 +188,7 @@ function Application(): JSX.Element {
|
||||
id: SERVICE_CHART_ID.errorPercentage,
|
||||
fillSpans: true,
|
||||
}),
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute, dotMetricsEnabled],
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute],
|
||||
);
|
||||
|
||||
const stepInterval = useMemo(
|
||||
|
||||
@@ -22,8 +22,6 @@ import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/Me
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../../../constants/features';
|
||||
import { useAppContext } from '../../../../../providers/App/App';
|
||||
import { IServiceName } from '../../types';
|
||||
import { ApDexMetricsProps } from './types';
|
||||
|
||||
@@ -38,10 +36,6 @@ function ApDexMetrics({
|
||||
}: ApDexMetricsProps): JSX.Element {
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
const apDexMetricsWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
@@ -55,7 +49,6 @@ function ApDexMetrics({
|
||||
threashold: thresholdValue || 0,
|
||||
delta: delta || false,
|
||||
metricsBuckets: metricsBuckets || [],
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -81,7 +74,6 @@ function ApDexMetrics({
|
||||
tagFilterItems,
|
||||
thresholdValue,
|
||||
topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import Spinner from 'components/Spinner';
|
||||
import { useGetMetricMeta } from 'hooks/apDex/useGetMetricMeta';
|
||||
import useErrorNotification from 'hooks/useErrorNotification';
|
||||
|
||||
import { FeatureKeys } from '../../../../../constants/features';
|
||||
import { useAppContext } from '../../../../../providers/App/App';
|
||||
import { WidgetKeys } from '../../../constant';
|
||||
import { IServiceName } from '../../types';
|
||||
import ApDexMetrics from './ApDexMetrics';
|
||||
@@ -20,17 +18,8 @@ function ApDexMetricsApplication({
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const signozLatencyBucketMetrics = dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm;
|
||||
|
||||
const { data, isLoading, error } = useGetMetricMeta(
|
||||
signozLatencyBucketMetrics,
|
||||
WidgetKeys.SignozLatencyBucket,
|
||||
servicename,
|
||||
);
|
||||
useErrorNotification(error);
|
||||
|
||||
@@ -56,10 +56,6 @@ function ServiceOverview({
|
||||
[isSpanMetricEnable, queries],
|
||||
);
|
||||
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const latencyWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
@@ -71,7 +67,6 @@ function ServiceOverview({
|
||||
tagFilterItems,
|
||||
isSpanMetricEnable,
|
||||
topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
@@ -81,13 +76,7 @@ function ServiceOverview({
|
||||
yAxisUnit: 'ns',
|
||||
id: SERVICE_CHART_ID.latency,
|
||||
}),
|
||||
[
|
||||
isSpanMetricEnable,
|
||||
servicename,
|
||||
tagFilterItems,
|
||||
topLevelOperationsRoute,
|
||||
dotMetricsEnabled,
|
||||
],
|
||||
[isSpanMetricEnable, servicename, tagFilterItems, topLevelOperationsRoute],
|
||||
);
|
||||
|
||||
const isQueryEnabled =
|
||||
|
||||
@@ -19,8 +19,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../../constants/features';
|
||||
import { useAppContext } from '../../../../providers/App/App';
|
||||
import { IServiceName } from '../types';
|
||||
import { title } from './config';
|
||||
import ColumnWithLink from './TableRenderer/ColumnWithLink';
|
||||
@@ -44,11 +42,6 @@ function TopOperationMetrics(): JSX.Element {
|
||||
convertRawQueriesToTraceSelectedTags(queries) || [],
|
||||
);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const keyOperationWidget = useMemo(
|
||||
() =>
|
||||
getWidgetQueryBuilder({
|
||||
@@ -57,14 +50,13 @@ function TopOperationMetrics(): JSX.Element {
|
||||
promql: [],
|
||||
builder: topOperationQueries({
|
||||
servicename,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
},
|
||||
panelTypes: PANEL_TYPES.TABLE,
|
||||
}),
|
||||
[servicename, dotMetricsEnabled],
|
||||
[servicename],
|
||||
);
|
||||
|
||||
const updatedQuery = updateStepInterval(keyOperationWidget.query);
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface IServiceName {
|
||||
|
||||
export interface TopOperationQueryFactoryProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
|
||||
@@ -20,7 +19,6 @@ export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
|
||||
export interface ExternalCallProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface BuilderQueriesProps {
|
||||
@@ -52,7 +50,6 @@ export interface OperationPerSecProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
topLevelOperations: string[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface LatencyProps {
|
||||
@@ -60,7 +57,6 @@ export interface LatencyProps {
|
||||
tagFilterItems: TagFilterItem[];
|
||||
isSpanMetricEnable?: boolean;
|
||||
topLevelOperationsRoute: string[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ApDexProps {
|
||||
@@ -78,5 +74,4 @@ export interface TableRendererProps {
|
||||
export interface ApDexMetricsQueryBuilderQueriesProps extends ApDexProps {
|
||||
delta: boolean;
|
||||
metricsBuckets: number[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -85,14 +85,11 @@ export enum WidgetKeys {
|
||||
HasError = 'hasError',
|
||||
Address = 'address',
|
||||
DurationNano = 'durationNano',
|
||||
StatusCodeNorm = 'status_code',
|
||||
StatusCode = 'status.code',
|
||||
Operation = 'operation',
|
||||
OperationName = 'operationName',
|
||||
Service_name_norm = 'service_name',
|
||||
Service_name = 'service.name',
|
||||
OTelServiceName = 'service.name',
|
||||
ServiceName = 'serviceName',
|
||||
SignozLatencyCountNorm = 'signoz_latency_count',
|
||||
SignozLatencyCount = 'signoz_latency.count',
|
||||
SignozDBLatencyCount = 'signoz_db_latency_count',
|
||||
DatabaseCallCount = 'signoz_database_call_count',
|
||||
@@ -101,10 +98,8 @@ export enum WidgetKeys {
|
||||
SignozCallsTotal = 'signoz_calls_total',
|
||||
SignozExternalCallLatencyCount = 'signoz_external_call_latency_count',
|
||||
SignozExternalCallLatencySum = 'signoz_external_call_latency_sum',
|
||||
Signoz_latency_bucket_norm = 'signoz_latency_bucket',
|
||||
Signoz_latency_bucket = 'signoz_latency.bucket',
|
||||
Db_system = 'db.system',
|
||||
Db_system_norm = 'db_system',
|
||||
SignozLatencyBucket = 'signoz_latency.bucket',
|
||||
DbSystem = 'db.system',
|
||||
}
|
||||
|
||||
export const topOperationMetricsDownloadOptions: DownloadOptions = {
|
||||
|
||||
@@ -32,5 +32,4 @@ export interface DatabaseCallsRPSProps extends DatabaseCallProps {
|
||||
export interface DatabaseCallProps {
|
||||
servicename: IServiceName['servicename'];
|
||||
tagFilterItems: TagFilterItem[];
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { MetricsSearchProps } from './types';
|
||||
@@ -23,12 +24,14 @@ function MetricsSearch({
|
||||
);
|
||||
|
||||
const handleStageAndRunQuery = useCallback(() => {
|
||||
saveRecentQueryByExpression(DataSource.METRICS, currentQueryFilterExpression);
|
||||
onChange(currentQueryFilterExpression);
|
||||
onRunQuery?.();
|
||||
}, [currentQueryFilterExpression, onChange, onRunQuery]);
|
||||
|
||||
const handleRunQuery = useCallback(
|
||||
(expression: string): void => {
|
||||
saveRecentQueryByExpression(DataSource.METRICS, expression);
|
||||
setCurrentQueryFilterExpression(expression);
|
||||
onChange(expression);
|
||||
},
|
||||
|
||||
@@ -387,4 +387,42 @@ describe('useOptionsMenu', () => {
|
||||
expect(remaining).toHaveLength(seedColumns.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fieldsSelector.value drops legacy columns without a name', () => {
|
||||
it('excludes entries missing name while keeping valid columns', () => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: { data: { data: { keys: {} } } },
|
||||
isFetching: false,
|
||||
});
|
||||
(usePreferenceContext as jest.Mock).mockReturnValue({
|
||||
traces: {
|
||||
preferences: {
|
||||
columns: [
|
||||
{ name: 'body', fieldContext: 'log' },
|
||||
{ key: 'legacy-key-no-name', fieldContext: 'log' },
|
||||
{ name: 'timestamp', fieldContext: 'log' },
|
||||
],
|
||||
formatting: { format: 'table', maxLines: 1, fontSize: 'small' },
|
||||
},
|
||||
updateColumns: mockUpdateColumns,
|
||||
updateFormatting: mockUpdateFormatting,
|
||||
},
|
||||
logs: {
|
||||
preferences: { columns: [], formatting: {} },
|
||||
updateColumns: mockUpdateColumns,
|
||||
updateFormatting: mockUpdateFormatting,
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useOptionsMenu({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
}),
|
||||
);
|
||||
|
||||
const fields = result.current.config.fieldsSelector?.value ?? [];
|
||||
expect(fields.map((f) => f.name)).toStrictEqual(['body', 'timestamp']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -399,7 +399,7 @@ const useOptionsMenu = ({
|
||||
onReorder: reorderSelectColumns,
|
||||
},
|
||||
fieldsSelector: {
|
||||
value: preferences?.columns ?? [],
|
||||
value: preferences?.columns?.filter((item) => has(item, 'name')) ?? [],
|
||||
onFieldsChange: updateColumns,
|
||||
},
|
||||
format: {
|
||||
|
||||
@@ -53,8 +53,6 @@ import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../../constants/features';
|
||||
import { useAppContext } from '../../../../providers/App/App';
|
||||
import { selectStyle } from './config';
|
||||
import { PLACEHOLDER } from './constant';
|
||||
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
|
||||
@@ -104,11 +102,6 @@ function QueryBuilderSearch({
|
||||
|
||||
const [isEditingTag, setIsEditingTag] = useState(false);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const {
|
||||
updateTag,
|
||||
handleClearTag,
|
||||
@@ -128,7 +121,6 @@ function QueryBuilderSearch({
|
||||
exampleQueries,
|
||||
} = useAutoComplete(
|
||||
query,
|
||||
dotMetricsEnabled,
|
||||
whereClauseConfig,
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
@@ -146,7 +138,6 @@ function QueryBuilderSearch({
|
||||
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
dotMetricsEnabled,
|
||||
searchKey,
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
|
||||
@@ -33,7 +33,7 @@ jest.mock('hooks/useNotifications', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const RESET_PASSWORD_ENDPOINT = '*/resetPassword';
|
||||
const RESET_PASSWORD_ENDPOINT = '*/api/v2/factor_password/reset';
|
||||
|
||||
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||
typeof history.push
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { Form, Input as AntdInput } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import resetPasswordApi from 'api/v1/factor_password/resetPassword';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useResetPassword } from 'api/generated/services/users';
|
||||
import AuthError from 'components/AuthError/AuthError';
|
||||
import AuthPageContainer from 'components/AuthPageContainer';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -14,7 +15,6 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { ArrowRight, CircleAlert, KeyRound } from '@signozhq/icons';
|
||||
import { Label } from 'pages/SignUp/styles';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { FormContainer } from './styles';
|
||||
|
||||
@@ -26,40 +26,41 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
const [confirmPasswordError, setConfirmPasswordError] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<APIError | null>();
|
||||
|
||||
const [isValidPassword, setIsValidPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation(['common']);
|
||||
const { search } = useLocation();
|
||||
const params = new URLSearchParams(search);
|
||||
const token = params.get('token');
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const {
|
||||
mutate: resetPassword,
|
||||
isLoading,
|
||||
error: mutationError,
|
||||
} = useResetPassword();
|
||||
|
||||
const errorMessage = useMemo(
|
||||
() => convertToApiError(mutationError),
|
||||
[mutationError],
|
||||
);
|
||||
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const handleFormSubmit: () => Promise<void> = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
const { password } = form.getFieldsValue();
|
||||
const handleFormSubmit = (): void => {
|
||||
const { password } = form.getFieldsValue();
|
||||
|
||||
await resetPasswordApi({
|
||||
password,
|
||||
token: token || '',
|
||||
});
|
||||
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
history.push(ROUTES.LOGIN);
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setErrorMessage(error as APIError);
|
||||
}
|
||||
resetPassword(
|
||||
{ data: { password, token: token || '' } },
|
||||
{
|
||||
onSuccess: (): void => {
|
||||
notifications.success({
|
||||
message: t('success', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
history.push(ROUTES.LOGIN);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const validatePassword = (): boolean => {
|
||||
@@ -222,7 +223,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-attr="reset-password"
|
||||
disabled={!isValidPassword || loading}
|
||||
disabled={!isValidPassword || isLoading}
|
||||
className="reset-password-submit-button"
|
||||
suffix={<ArrowRight size={16} />}
|
||||
>
|
||||
|
||||
@@ -14,8 +14,6 @@ import { SelectOption } from 'types/common/select';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import QueryChip from './components/QueryChip';
|
||||
import { QueryChipItem, SearchContainer } from './styles';
|
||||
|
||||
@@ -42,12 +40,7 @@ function ResourceAttributesFilter({
|
||||
SelectOption<string, string>[]
|
||||
>([]);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const resourceDeploymentKey = getResourceDeploymentKeys(dotMetricsEnabled);
|
||||
const resourceDeploymentKey = getResourceDeploymentKeys();
|
||||
|
||||
const [selectedEnvironments, setSelectedEnvironments] = useState<string[]>([]);
|
||||
|
||||
@@ -73,21 +66,20 @@ function ResourceAttributesFilter({
|
||||
}, [queries, resourceDeploymentKey]);
|
||||
|
||||
useEffect(() => {
|
||||
getEnvironmentTagKeys(dotMetricsEnabled).then((tagKeys) => {
|
||||
getEnvironmentTagKeys().then((tagKeys) => {
|
||||
if (tagKeys && Array.isArray(tagKeys) && tagKeys.length > 0) {
|
||||
getEnvironmentTagValues(dotMetricsEnabled).then((tagValues) => {
|
||||
getEnvironmentTagValues().then((tagValues) => {
|
||||
setEnvironments(tagValues);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [dotMetricsEnabled]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="resourceAttributesFilter-container">
|
||||
<div className="environment-selector">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
key={selectedEnvironments.join('')}
|
||||
showSearch
|
||||
mode="multiple"
|
||||
value={selectedEnvironments}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
|
||||
import { encode } from 'js-base64';
|
||||
|
||||
import ResourceAttributesFilter from '../ResourceAttributesFilter';
|
||||
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
push: jest.fn(),
|
||||
location: { search: '', pathname: '/' },
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('api/metrics/getResourceAttributes', () => ({
|
||||
getResourceAttributesTagKeys: jest.fn(),
|
||||
getResourceAttributesTagValues: jest.fn(),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line import/first, import/order
|
||||
import {
|
||||
getResourceAttributesTagKeys,
|
||||
getResourceAttributesTagValues,
|
||||
// eslint-disable-next-line import/newline-after-import
|
||||
} from 'api/metrics/getResourceAttributes';
|
||||
// eslint-disable-next-line import/first, import/order
|
||||
import history from 'lib/history';
|
||||
|
||||
const mockTagKeys = getResourceAttributesTagKeys as jest.MockedFunction<
|
||||
typeof getResourceAttributesTagKeys
|
||||
>;
|
||||
const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
|
||||
typeof getResourceAttributesTagValues
|
||||
>;
|
||||
|
||||
function tagKeysPayload(keys: string[]): never {
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'ok',
|
||||
payload: {
|
||||
data: {
|
||||
attributeKeys: keys.map((key) => ({
|
||||
key,
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
})),
|
||||
},
|
||||
},
|
||||
} as unknown as never;
|
||||
}
|
||||
|
||||
function tagValuesPayload(values: string[]): never {
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'ok',
|
||||
payload: { data: { stringAttributeValues: values } },
|
||||
} as unknown as never;
|
||||
}
|
||||
|
||||
function seedUrl(queries: IResourceAttribute[], pathname: string): void {
|
||||
const location = history.location as { search: string; pathname: string };
|
||||
location.search = queries.length
|
||||
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
|
||||
: '';
|
||||
location.pathname = pathname;
|
||||
}
|
||||
|
||||
function renderFilter(pathname: string): MemoryHistory {
|
||||
const routerHistory = createMemoryHistory({
|
||||
initialEntries: [`${pathname}${history.location.search}`],
|
||||
});
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router history={routerHistory}>
|
||||
<ResourceProvider>{children}</ResourceProvider>
|
||||
</Router>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ResourceAttributesFilter />
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
return routerHistory;
|
||||
}
|
||||
|
||||
describe('ResourceAttributesFilter', () => {
|
||||
beforeEach(() => {
|
||||
mockTagKeys.mockReset();
|
||||
mockTagValues.mockReset();
|
||||
mockTagKeys.mockResolvedValue(
|
||||
tagKeysPayload(['resource_deployment.environment']),
|
||||
);
|
||||
mockTagValues.mockResolvedValue(tagValuesPayload(['production', 'staging']));
|
||||
seedUrl([], '/');
|
||||
});
|
||||
|
||||
it('shows every applied filter on the service map, including ones it cannot apply', async () => {
|
||||
seedUrl(
|
||||
[
|
||||
{
|
||||
id: 'svc',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
},
|
||||
],
|
||||
ROUTES.SERVICE_MAP,
|
||||
);
|
||||
|
||||
renderFilter(ROUTES.SERVICE_MAP);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/service\.name/)).toBeInTheDocument(),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('resource-environment-filter')
|
||||
.querySelector('.ant-select-selection-item'),
|
||||
).toHaveTextContent('production'),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the environment dropdown open so more than one environment can be picked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderFilter('/services');
|
||||
|
||||
const environmentFilter = screen.getByTestId('resource-environment-filter');
|
||||
await user.click(
|
||||
environmentFilter.querySelector('input') as HTMLInputElement,
|
||||
);
|
||||
|
||||
await user.click(await screen.findByTitle('production'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByTitle('staging').closest('.ant-select-dropdown'),
|
||||
).not.toHaveClass('ant-select-dropdown-hidden'),
|
||||
);
|
||||
|
||||
await user.click(screen.getByTitle('staging'));
|
||||
|
||||
await waitFor(() => {
|
||||
const selected = Array.from(
|
||||
environmentFilter.querySelectorAll('.ant-select-selection-item-content'),
|
||||
).map((node) => node.textContent);
|
||||
expect(selected).toStrictEqual(['production', 'staging']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,6 @@ import {
|
||||
getResourceDeploymentKeys,
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
|
||||
import { FeatureKeys } from '../../../../constants/features';
|
||||
import { useAppContext } from '../../../../providers/App/App';
|
||||
import { QueryChipContainer, QueryChipItem } from '../../styles';
|
||||
import { IQueryChipProps } from './types';
|
||||
|
||||
@@ -13,13 +11,7 @@ function QueryChip({ queryData, onClose }: IQueryChipProps): JSX.Element {
|
||||
onClose(queryData.id);
|
||||
};
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const isClosable =
|
||||
queryData.tagKey !== getResourceDeploymentKeys(dotMetricsEnabled);
|
||||
const isClosable = queryData.tagKey !== getResourceDeploymentKeys();
|
||||
|
||||
return (
|
||||
<QueryChipContainer>
|
||||
|
||||
@@ -4,8 +4,6 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { ServiceMetricsProps } from '../types';
|
||||
import { getQueryRangeRequestData } from '../utils';
|
||||
import ServiceMetricTable from './ServiceMetricTable';
|
||||
@@ -18,19 +16,13 @@ function ServiceMetricsApplication({
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const queryRangeRequestData = useMemo(
|
||||
() =>
|
||||
getQueryRangeRequestData({
|
||||
topLevelOperations,
|
||||
globalSelectedInterval,
|
||||
dotMetricsEnabled,
|
||||
}),
|
||||
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
|
||||
[globalSelectedInterval, topLevelOperations],
|
||||
);
|
||||
return (
|
||||
<ServiceMetricTable
|
||||
|
||||
@@ -19,13 +19,10 @@ import {
|
||||
|
||||
export const serviceMetricsQuery = (
|
||||
topLevelOperation: [keyof ServiceDataProps, string[]],
|
||||
dotMetricsEnabled: boolean,
|
||||
): QueryBuilderData => {
|
||||
const p99AutoCompleteData: BaseAutocompleteData = {
|
||||
dataType: DataTypes.Float64,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Signoz_latency_bucket
|
||||
: WidgetKeys.Signoz_latency_bucket_norm,
|
||||
key: WidgetKeys.SignozLatencyBucket,
|
||||
type: '',
|
||||
};
|
||||
|
||||
@@ -53,9 +50,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -78,9 +73,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -90,7 +83,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.Int64,
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
key: WidgetKeys.StatusCode,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -113,9 +106,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -138,9 +129,7 @@ export const serviceMetricsQuery = (
|
||||
id: '',
|
||||
key: {
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Resource,
|
||||
},
|
||||
op: OPERATORS.IN,
|
||||
@@ -193,9 +182,7 @@ export const serviceMetricsQuery = (
|
||||
const groupBy: BaseAutocompleteData[] = [
|
||||
{
|
||||
dataType: DataTypes.String,
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.Service_name_norm,
|
||||
key: WidgetKeys.OTelServiceName,
|
||||
type: MetricsType.Tag,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -17,8 +17,6 @@ import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import SkipOnBoardingModal from '../SkipOnBoardModal';
|
||||
import ServiceTraceTable from './ServiceTracesTable';
|
||||
|
||||
@@ -40,11 +38,6 @@ function ServiceTraces(): JSX.Element {
|
||||
selectedTags,
|
||||
});
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
useErrorNotification(error);
|
||||
|
||||
const services = data || [];
|
||||
@@ -62,7 +55,7 @@ function ServiceTraces(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(val) => val.tagKey === getResourceDeploymentKeys(),
|
||||
)?.tagValue;
|
||||
|
||||
const rps = data.reduce((total, service) => total + service.callRate, 0);
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface ServiceMetricsTableProps {
|
||||
export interface GetQueryRangeRequestDataProps {
|
||||
topLevelOperations: [keyof ServiceDataProps, string[]][];
|
||||
globalSelectedInterval: Time | CustomTimeType;
|
||||
dotMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface GetServiceListFromQueryProps {
|
||||
|
||||
@@ -26,7 +26,6 @@ export function getSeriesValue(
|
||||
export const getQueryRangeRequestData = ({
|
||||
topLevelOperations,
|
||||
globalSelectedInterval,
|
||||
dotMetricsEnabled,
|
||||
}: GetQueryRangeRequestDataProps): GetQueryResultsProps[] => {
|
||||
const requestData: GetQueryResultsProps[] = [];
|
||||
topLevelOperations.forEach((operation) => {
|
||||
@@ -34,7 +33,7 @@ export const getQueryRangeRequestData = ({
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
promql: [],
|
||||
builder: serviceMetricsQuery(operation, dotMetricsEnabled),
|
||||
builder: serviceMetricsQuery(operation),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
},
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import type {
|
||||
AuthtypesGettableRoleDTO,
|
||||
AuthtypesUserRoleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
getGetRolesByUserIDQueryKey,
|
||||
useGetRolesByUserID,
|
||||
useRemoveUserRoleByUserIDAndRoleID,
|
||||
useSetRoleByUserID,
|
||||
useCreateUserRole,
|
||||
useDeleteUserRole,
|
||||
useGetUser,
|
||||
} from 'api/generated/services/users';
|
||||
import { retryOn429 } from 'utils/errorUtils';
|
||||
|
||||
const enum PromiseStatus {
|
||||
Fulfilled = 'fulfilled',
|
||||
Rejected = 'rejected',
|
||||
}
|
||||
|
||||
// Stable identity so the memos below do not recompute on every render.
|
||||
const EMPTY_USER_ROLES: AuthtypesUserRoleDTO[] = [];
|
||||
|
||||
export interface MemberRoleUpdateFailure {
|
||||
roleName: string;
|
||||
error: unknown;
|
||||
@@ -33,31 +36,31 @@ export function useMemberRoleManager(
|
||||
userId: string,
|
||||
enabled: boolean,
|
||||
): UseMemberRoleManagerResult {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useGetRolesByUserID(
|
||||
const { data, isLoading } = useGetUser(
|
||||
{ id: userId },
|
||||
{ query: { enabled: !!userId && enabled } },
|
||||
);
|
||||
|
||||
const userRoles = data?.data?.userRoles ?? EMPTY_USER_ROLES;
|
||||
|
||||
const currentRoles = useMemo<AuthtypesGettableRoleDTO[]>(
|
||||
() => data?.data ?? [],
|
||||
[data?.data],
|
||||
() => userRoles.map((userRole) => userRole.role),
|
||||
[userRoles],
|
||||
);
|
||||
|
||||
const { mutateAsync: setRole } = useSetRoleByUserID({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: removeRole } = useRemoveUserRoleByUserIDAndRoleID({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const invalidateRoles = useCallback(
|
||||
() =>
|
||||
queryClient.invalidateQueries(getGetRolesByUserIDQueryKey({ id: userId })),
|
||||
[userId, queryClient],
|
||||
// DELETE /api/v2/user_roles/{id} is keyed by the user_role join row, not the role.
|
||||
const assignmentIdByRoleId = useMemo(
|
||||
() => new Map(userRoles.map((userRole) => [userRole.roleId, userRole.id])),
|
||||
[userRoles],
|
||||
);
|
||||
|
||||
const { mutateAsync: createUserRole } = useCreateUserRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
const { mutateAsync: deleteUserRole } = useDeleteUserRole({
|
||||
mutation: { retry: retryOn429 },
|
||||
});
|
||||
|
||||
const applyDiff = useCallback(
|
||||
async (
|
||||
localRoleIds: string[],
|
||||
@@ -80,30 +83,33 @@ export function useMemberRoleManager(
|
||||
const allOperations = [
|
||||
...addedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof setRole> =>
|
||||
setRole({
|
||||
pathParams: { id: userId },
|
||||
data: { name: role.name ?? '' },
|
||||
}),
|
||||
})),
|
||||
...removedRoles.map((role) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof removeRole> =>
|
||||
removeRole({ pathParams: { id: userId, roleId: role.id ?? '' } }),
|
||||
run: (): ReturnType<typeof createUserRole> =>
|
||||
createUserRole({ data: { userId, roleId: role.id ?? '' } }),
|
||||
})),
|
||||
...removedRoles
|
||||
.map((role) => ({
|
||||
role,
|
||||
assignmentId: assignmentIdByRoleId.get(role.id ?? ''),
|
||||
}))
|
||||
.filter(
|
||||
(
|
||||
entry,
|
||||
): entry is {
|
||||
role: AuthtypesGettableRoleDTO;
|
||||
assignmentId: string;
|
||||
} => !!entry.assignmentId,
|
||||
)
|
||||
.map(({ role, assignmentId }) => ({
|
||||
role,
|
||||
run: (): ReturnType<typeof deleteUserRole> =>
|
||||
deleteUserRole({ pathParams: { id: assignmentId } }),
|
||||
})),
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
allOperations.map((op) => op.run()),
|
||||
);
|
||||
|
||||
const successCount = results.filter(
|
||||
(r) => r.status === PromiseStatus.Fulfilled,
|
||||
).length;
|
||||
if (successCount > 0) {
|
||||
await invalidateRoles();
|
||||
}
|
||||
|
||||
const failures: MemberRoleUpdateFailure[] = [];
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === PromiseStatus.Rejected) {
|
||||
@@ -113,7 +119,6 @@ export function useMemberRoleManager(
|
||||
error: result.reason,
|
||||
onRetry: async (): Promise<void> => {
|
||||
await run();
|
||||
await invalidateRoles();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -121,7 +126,7 @@ export function useMemberRoleManager(
|
||||
|
||||
return failures;
|
||||
},
|
||||
[userId, currentRoles, setRole, removeRole, invalidateRoles],
|
||||
[userId, currentRoles, assignmentIdByRoleId, createUserRole, deleteUserRole],
|
||||
);
|
||||
|
||||
return { currentRoles, isLoading, applyDiff };
|
||||
|
||||
@@ -27,7 +27,6 @@ export type WhereClauseConfig = {
|
||||
|
||||
export const useAutoComplete = (
|
||||
query: IBuilderQuery,
|
||||
dotMetricsEnabled: boolean,
|
||||
whereClauseConfig?: WhereClauseConfig,
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
@@ -40,7 +39,6 @@ export const useAutoComplete = (
|
||||
const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
dotMetricsEnabled,
|
||||
searchKey,
|
||||
shouldUseSuggestions,
|
||||
isInfraMonitoring,
|
||||
|
||||
@@ -48,7 +48,6 @@ type IuseFetchKeysAndValues = {
|
||||
export const useFetchKeysAndValues = (
|
||||
searchValue: string,
|
||||
query: IBuilderQuery,
|
||||
dotMetricsEnabled: boolean,
|
||||
searchKey: string,
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { encode } from 'js-base64';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import { whilelistedKeys } from './config';
|
||||
import { ResourceContext } from './context';
|
||||
import {
|
||||
IResourceAttribute,
|
||||
@@ -58,11 +54,6 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const dotMetricsEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const dispatchQueries = useCallback(
|
||||
(queries: IResourceAttribute[]): void => {
|
||||
urlQuery.set(
|
||||
@@ -78,7 +69,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
|
||||
const loadTagKeys = (): void => {
|
||||
handleLoading(true);
|
||||
GetTagKeys(dotMetricsEnabled)
|
||||
GetTagKeys()
|
||||
.then((tagKeys) => {
|
||||
const options = mappingWithRoutesAndKeys(pathname, tagKeys);
|
||||
setOptionsData({ options, mode: undefined });
|
||||
@@ -161,15 +152,15 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
|
||||
setSelectedQueries([...value]);
|
||||
},
|
||||
[optionsData.mode, step, staging, dotMetricsEnabled, pathname],
|
||||
[optionsData.mode, step, staging, pathname],
|
||||
);
|
||||
|
||||
const handleEnvironmentChange = useCallback(
|
||||
(environments: string[]): void => {
|
||||
const staging = [getResourceDeploymentKeys(dotMetricsEnabled), 'IN'];
|
||||
const staging = [getResourceDeploymentKeys(), 'IN'];
|
||||
|
||||
const queriesCopy = queries.filter(
|
||||
(query) => query.tagKey !== getResourceDeploymentKeys(dotMetricsEnabled),
|
||||
(query) => query.tagKey !== getResourceDeploymentKeys(),
|
||||
);
|
||||
|
||||
if (environments && Array.isArray(environments) && environments.length > 0) {
|
||||
@@ -184,7 +175,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
|
||||
setStep('Idle');
|
||||
},
|
||||
[dispatchQueries, dotMetricsEnabled, queries],
|
||||
[dispatchQueries, queries],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(
|
||||
@@ -202,16 +193,9 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
setOptionsData({ mode: undefined, options: [] });
|
||||
}, [dispatchQueries]);
|
||||
|
||||
const getVisibleQueries = useMemo(() => {
|
||||
if (pathname === ROUTES.SERVICE_MAP) {
|
||||
return queries.filter((query) => whilelistedKeys.includes(query.tagKey));
|
||||
}
|
||||
return queries;
|
||||
}, [queries, pathname]);
|
||||
|
||||
const value: IResourceAttributeProps = useMemo(
|
||||
() => ({
|
||||
queries: getVisibleQueries,
|
||||
queries,
|
||||
staging,
|
||||
handleClearAll,
|
||||
handleClose,
|
||||
@@ -234,7 +218,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
|
||||
staging,
|
||||
selectedQuery,
|
||||
optionsData,
|
||||
getVisibleQueries,
|
||||
queries,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -2,13 +2,9 @@ import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { createMemoryHistory, MemoryHistory } from 'history';
|
||||
import { encode } from 'js-base64';
|
||||
import { AppContext } from 'providers/App/App';
|
||||
import { IAppContext } from 'providers/App/types';
|
||||
import { getAppContextMock } from 'tests/test-utils';
|
||||
|
||||
import ResourceProvider from '../ResourceProvider';
|
||||
import useResourceAttribute from '../useResourceAttribute';
|
||||
@@ -55,10 +51,8 @@ const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
|
||||
|
||||
function createWrapper({
|
||||
routerHistory,
|
||||
appContextOverrides,
|
||||
}: {
|
||||
routerHistory: MemoryHistory;
|
||||
appContextOverrides?: Partial<IAppContext>;
|
||||
}): ({ children }: { children: ReactNode }) => JSX.Element {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
@@ -66,13 +60,9 @@ function createWrapper({
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext.Provider
|
||||
value={getAppContextMock('ADMIN', appContextOverrides)}
|
||||
>
|
||||
<Router history={routerHistory}>
|
||||
<ResourceProvider>{children}</ResourceProvider>
|
||||
</Router>
|
||||
</AppContext.Provider>
|
||||
<Router history={routerHistory}>
|
||||
<ResourceProvider>{children}</ResourceProvider>
|
||||
</Router>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
@@ -411,7 +401,7 @@ describe('ResourceProvider', () => {
|
||||
});
|
||||
|
||||
describe('handleEnvironmentChange', () => {
|
||||
it('adds an environment query when envs are provided', async () => {
|
||||
it('adds a dotted environment query when envs are provided', async () => {
|
||||
const routerHistory = createMemoryHistory({ initialEntries: ['/'] });
|
||||
const { result } = renderHook(() => useResourceAttribute(), {
|
||||
wrapper: createWrapper({ routerHistory }),
|
||||
@@ -424,7 +414,7 @@ describe('ResourceProvider', () => {
|
||||
await waitFor(() => {
|
||||
expect(result.current.queries).toHaveLength(1);
|
||||
expect(result.current.queries[0]).toMatchObject({
|
||||
tagKey: 'resource_deployment_environment',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
});
|
||||
@@ -435,7 +425,7 @@ describe('ResourceProvider', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment_environment',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
},
|
||||
@@ -459,7 +449,7 @@ describe('ResourceProvider', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const tagKeys = result.current.queries.map((q) => q.tagKey);
|
||||
expect(tagKeys).not.toContain('resource_deployment_environment');
|
||||
expect(tagKeys).not.toContain('resource_deployment.environment');
|
||||
expect(tagKeys).toContain('resource_service_name');
|
||||
});
|
||||
});
|
||||
@@ -468,7 +458,7 @@ describe('ResourceProvider', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment_environment',
|
||||
tagKey: 'resource_deployment.environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
},
|
||||
@@ -486,43 +476,13 @@ describe('ResourceProvider', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const envQueries = result.current.queries.filter(
|
||||
(q) => q.tagKey === 'resource_deployment_environment',
|
||||
(q) => q.tagKey === 'resource_deployment.environment',
|
||||
);
|
||||
expect(envQueries).toHaveLength(1);
|
||||
expect(envQueries[0].tagValue).toStrictEqual(['staging']);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the dotted deployment env key when DOT_METRICS_ENABLED is active', async () => {
|
||||
const routerHistory = createMemoryHistory({ initialEntries: ['/'] });
|
||||
const { result } = renderHook(() => useResourceAttribute(), {
|
||||
wrapper: createWrapper({
|
||||
routerHistory,
|
||||
appContextOverrides: {
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.DOT_METRICS_ENABLED,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleEnvironmentChange(['production']);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.queries[0].tagKey).toBe(
|
||||
'resource_deployment.environment',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves unrelated query params when dispatching', async () => {
|
||||
const routerHistory = createMemoryHistory({
|
||||
initialEntries: ['/?tab=overview'],
|
||||
@@ -544,22 +504,23 @@ describe('ResourceProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVisibleQueries (SERVICE_MAP filtering)', () => {
|
||||
it('filters queries down to whitelisted keys on SERVICE_MAP', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
describe('SERVICE_MAP', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
|
||||
it('exposes every query from the URL, including ones the map cannot apply', () => {
|
||||
mockLibHistory(
|
||||
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
|
||||
ROUTES.SERVICE_MAP,
|
||||
@@ -572,24 +533,10 @@ describe('ResourceProvider', () => {
|
||||
wrapper: createWrapper({ routerHistory }),
|
||||
});
|
||||
|
||||
expect(result.current.queries).toStrictEqual([seeded[1]]);
|
||||
expect(result.current.queries).toStrictEqual(seeded);
|
||||
});
|
||||
|
||||
it('returns all queries on non-SERVICE_MAP routes', () => {
|
||||
const seeded = [
|
||||
{
|
||||
id: 'a',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
tagKey: 'resource_k8s_cluster_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['prod'],
|
||||
},
|
||||
];
|
||||
mockLibHistory(
|
||||
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
|
||||
'/services',
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import { whilelistedKeys } from '../config';
|
||||
import { mappingWithRoutesAndKeys } from '../utils';
|
||||
import {
|
||||
filterServiceMapSupportedQueries,
|
||||
mappingWithRoutesAndKeys,
|
||||
} from '../utils';
|
||||
|
||||
describe('useResourceAttribute config', () => {
|
||||
describe('whilelistedKeys', () => {
|
||||
it('should include underscore-notation keys (DOT_METRICS_ENABLED=false)', () => {
|
||||
it('should include underscore-notation keys', () => {
|
||||
expect(whilelistedKeys).toContain('resource_deployment_environment');
|
||||
expect(whilelistedKeys).toContain('resource_k8s_cluster_name');
|
||||
expect(whilelistedKeys).toContain('resource_k8s_cluster_namespace');
|
||||
});
|
||||
|
||||
it('should include dot-notation keys (DOT_METRICS_ENABLED=true)', () => {
|
||||
it('should include dot-notation keys', () => {
|
||||
expect(whilelistedKeys).toContain('resource_deployment.environment');
|
||||
expect(whilelistedKeys).toContain('resource_k8s.cluster.name');
|
||||
expect(whilelistedKeys).toContain('resource_k8s.cluster.namespace');
|
||||
@@ -74,4 +77,29 @@ describe('useResourceAttribute config', () => {
|
||||
expect(result).toStrictEqual(allFilters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterServiceMapSupportedQueries', () => {
|
||||
const environmentQuery = {
|
||||
id: 'env',
|
||||
tagKey: 'resource_deployment_environment',
|
||||
operator: 'IN',
|
||||
tagValue: ['production'],
|
||||
};
|
||||
const serviceQuery = {
|
||||
id: 'svc',
|
||||
tagKey: 'resource_service_name',
|
||||
operator: 'IN',
|
||||
tagValue: ['frontend'],
|
||||
};
|
||||
|
||||
it('should keep only the queries the service map can filter on', () => {
|
||||
expect(
|
||||
filterServiceMapSupportedQueries([environmentQuery, serviceQuery]),
|
||||
).toStrictEqual([environmentQuery]);
|
||||
});
|
||||
|
||||
it('should return an empty list when no query is supported', () => {
|
||||
expect(filterServiceMapSupportedQueries([serviceQuery])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user