mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-05 11:00:41 +01:00
Compare commits
5 Commits
feat/googl
...
proto/stor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88a522a638 | ||
|
|
25298585b8 | ||
|
|
608bb9eeee | ||
|
|
693ea77f90 | ||
|
|
618031ddaa |
@@ -54,50 +54,95 @@ The `fieldContexts` map includes aliases (`tag` -> `attribute`, `spanfield` -> `
|
||||
|
||||
## The Abstraction Stack
|
||||
|
||||
The query pipeline is built from four interfaces that compose vertically. Each layer has a single responsibility. Each layer depends only on the layers below it. This layering is intentional and must be preserved.
|
||||
The query pipeline has three layers. The generic layer is written one time, in `pkg/querybuilder`. A storage is written one time per signal. The statement builders compose them. Each layer depends only on the layer below it. This layering is intentional and must be preserved.
|
||||
|
||||
```
|
||||
StatementBuilder <- Orchestrates everything into executable SQL
|
||||
├── AggExprRewriter <- Rewrites aggregation expressions (maps field refs to columns)
|
||||
├── ConditionBuilder <- Builds WHERE predicates (field + operator + value -> SQL)
|
||||
└── FieldMapper <- Maps TelemetryFieldKey -> ClickHouse column expression
|
||||
StatementBuilder <- Composes one query into executable SQL
|
||||
├── AggExprRewriter <- Rewrites aggregation expressions through the generic layer
|
||||
├── filter visitor <- Parses the filter expression and compiles it one term at a time
|
||||
└── querybuilder (generic) <- Resolution, the condition builder, the column expression builder
|
||||
└── Storage <- What one signal's tables can answer about one field key
|
||||
```
|
||||
|
||||
### FieldMapper
|
||||
### Storage
|
||||
|
||||
**Contract:** Given a `TelemetryFieldKey`, return a ClickHouse column expression that yields the value for that field when used in a SELECT.
|
||||
**Contract:** `qbtypes.Storage` in `pkg/types/querybuildertypes/querybuildertypesv5/qb.go`. One implementation per signal: traces, logs, metrics, audit, rule state history, the resource fingerprint sub-query, and the related-values metadata.
|
||||
|
||||
**Principle:** This is the *only* place where field-to-column translation happens. No other layer should contain knowledge of how fields map to storage. If you need a column expression, go through the FieldMapper.
|
||||
A storage answers five questions and nothing else:
|
||||
|
||||
**Why:** The user says `http.request.method`. ClickHouse might store it as `attributes_string['http.request.method']`, or as a materialized column `` `attribute_string_http$$request$$method` ``, or via a JSON access path in a body column. This variation is entirely contained within the FieldMapper. Everything above it is storage-agnostic.
|
||||
- `Read(field)`: the bare SQL read of one field key. No alias, no guard, no cast. It honors the materialization and the evolutions the field carries.
|
||||
- `Exists(field, exists)`: the presence test of one field key, and how the field reads when a row lacks it (`Absent`, below).
|
||||
- `Fallback(key, operator, value)`: the field keys that could hold a key metadata does not report: column aliases, the type variants of a map read, body paths, and virtual keys that compile to structural predicates (a span search scope, a full-text search over a scope).
|
||||
- `Traits()`: the storage's part in the resource fingerprint split, whether it supports body functions, what it does with an unknown key, and which contexts mean "this signal's own record".
|
||||
- `Compile` and `ColumnRead`: two overrides for a storage with its own condition language (the body JSON language in logs, the index hints of the resource fingerprint, the polarity form of the related values, the String-typed labels of metrics). Every other storage returns `querybuilder.SharedCondition` and `querybuilder.DefaultRead`.
|
||||
|
||||
### ConditionBuilder
|
||||
**Principle:** A storage describes its field keys. It never decides a guard, an ambiguity, a warning, or the shape of a fold. Those decisions are derived one time, in the generic layer, from those descriptions.
|
||||
|
||||
**Contract:** Given a field key, an operator, and a value, produce a valid SQL predicate for a WHERE clause.
|
||||
### Absent
|
||||
|
||||
**Dependency:** Uses FieldMapper for the left-hand side of the condition.
|
||||
`Exists` returns the field's `Absent`: what a row without the field reads. It is a property of the read, not of the column. `resource.x::String` reads the empty string for an absent row, the multi-era fold `multiIf(..., NULL)` reads NULL, and a table column always reads a real value. Every guard derives from it:
|
||||
|
||||
**Principle:** The ConditionBuilder owns all the complexity of operator semantics, i.e type casting, array operators (`hasAny`/`hasAll` vs `=`), existence checks, and negative operator behavior. This complexity must not leak upward into the StatementBuilder.
|
||||
| WhenAbsent | Absent row reads | Positive filter | Raw select | Multi-candidate column | Field keys |
|
||||
|---|---|---|---|---|---|
|
||||
| `AlwaysPresent` | a real value | no guard | no guard | no branch, ends the candidate list | table columns |
|
||||
| `AbsentIsSentinel` | `''`, 0, false, and that is not a value | exists guard | exists guard | presence branch | map attributes, cast JSON paths, string families |
|
||||
| `AbsentIsNull` | NULL | no guard | no guard | presence branch | multi-era folds, body JSON paths, numeric families |
|
||||
| `AbsentIsValue` | `''`, and that is the keyless contract | no guard | no guard | no presence branch | metrics labels, rule state history labels |
|
||||
|
||||
### AggExprRewriter
|
||||
### The generic layer
|
||||
|
||||
**Contract:** Given a user-facing aggregation expression like `sum(duration_nano)`, resolve field references within it and produce valid ClickHouse SQL.
|
||||
`pkg/querybuilder` needs two inputs, made one time per request:
|
||||
|
||||
**Dependency:** Uses FieldMapper to resolve field names within expressions.
|
||||
- The metadata keys: `keys := metadataStore.GetKeysMulti(...)`, the field keys the metadata store reports for the query's names, as `map[name][]*TelemetryFieldKey`.
|
||||
- `q := querybuilder.NewQueryInfo(ctx, orgID, fl, signal, metric, startNs, endNs)`: the org and time range every read needs, the signal and the queried metric that family admission needs, and the query-path flags (`FamiliesOn`, `BodyJSONOn`), evaluated one time.
|
||||
|
||||
**Principle:** Aggregation expressions are user-authored strings that contain field references. The rewriter parses them, identifies field references, resolves each through the FieldMapper, and reassembles the expression.
|
||||
The functions, from the outside in:
|
||||
|
||||
### StatementBuilder
|
||||
| Function | Does |
|
||||
|---|---|
|
||||
| `PrepareWhereClause(query, opts)` | The filter visitor. Parses the filter grammar and compiles each term through `RejectsBodyFunction`, `Resolve`, and `Condition`. Returns the WHERE clause, the warnings, and the cost-guard flag. |
|
||||
| `NewAggExprRewriter(settings, fullTextColumn, storage, fl, signal)` | Parses an aggregation expression such as `sum(duration_nano)` and resolves each field reference through `ResolveColumn`. |
|
||||
| `ResolveColumn(ctx, q, storage, key, target, metadata)` | `Resolve` with `FilterOperatorUnknown`, then `Column`. The column stages (raw select, order by, group by, aggregation arguments) call it. |
|
||||
| `Resolve(ctx, q, storage, key, operator, value, metadata)` | One requested key to its meanings in this storage (see "A resolved key"). |
|
||||
| `Condition(ctx, q, storage, resolved, dropResourceFields, operator, value, sb)` | A resolved key to the conditions of one filter term: the split narrows the fields, and each field compiles through `storage.Compile`. |
|
||||
| `Conditions(...)` | `RejectsBodyFunction`, `Resolve`, and `Condition` in one call, for callers outside the visitor: the related-values metadata, the scoped traces predicate resolver, tests. |
|
||||
| `Column(ctx, q, storage, resolved, target)` | A resolved key to one bare column expression. The caller aliases. |
|
||||
| `RejectsBodyFunction(traits, operator)` | Before resolution: a storage without body functions (`has`, `hasAny`, `hasAll`, `hasToken`, `search`) errors; the fingerprint side of a split skips the term, because the main query evaluates it. |
|
||||
| `SharedCondition(...)` | The `Compile` of every storage without its own condition language: `LogicalValueExpr`, the shared data-type collision cast, `OperatorCondition`, then the guard rule. |
|
||||
| `OperatorCondition(...)` | The operator switch over an already cast read. A storage with its own cast policy composes with it. |
|
||||
| `DefaultRead(...)` | The `ColumnRead` of every storage without a target-dependent read: `LogicalValueExpr` as an uncoerced column expression. |
|
||||
| `LogicalValueExpr(...)`, `LogicalExistsExpr(...)` | The only place family expressions are built. A single-member field reads through its member. A family merges the member reads current-first (`COALESCE(NULLIF(m1, ''), NULLIF(m2, ''), '')` for strings, `multiIf` with a NULL tail for numbers) and ORs the member presence tests. A member with a value map reads through `TransformRead`. |
|
||||
|
||||
**Contract:** Given a complete `QueryBuilderQuery`, a time range, and a request type, produces an executable SQL statement.
|
||||
### A resolved key
|
||||
|
||||
**Dependency:** Uses all three abstractions above.
|
||||
`Resolve` turns one requested key into a `Resolved` value. It is the only thing the condition builder and the column expression builder receive. Compile it with the operator and value it was resolved with: the stage is the operand, and a nil value means a column or presence use.
|
||||
|
||||
**Principle:** This is the composition layer. It does not contain field mapping logic, condition building logic, or expression rewriting logic. It orchestrates the other abstractions. If you find storage-specific logic creeping into the StatementBuilder, push it down into the appropriate abstraction.
|
||||
```go
|
||||
type Resolved struct {
|
||||
Key *TelemetryFieldKey // the spelling the request used
|
||||
Fields []*LogicalField // its meanings in this storage, one per interpretation
|
||||
FromFallback bool // the fields came from the storage's Fallback, not from metadata matches
|
||||
Ambiguous bool // the matches held several interpretations
|
||||
Skipped bool // the storage contributes nothing for this key
|
||||
Warnings []string // the warnings to surface: ambiguity, not-found
|
||||
}
|
||||
```
|
||||
|
||||
A `LogicalField` is one meaning: one name, context, and data type, backed by one or more physical members. A family (one field with several spellings) is one logical field with several members, current spelling first. Ambiguity (one name, different fields) is several logical fields.
|
||||
|
||||
The resolution order is the same for every storage and every stage:
|
||||
|
||||
1. **Own context.** A key under one of the storage's own contexts (`span.x`, `log.x`) looks up as if it had no context. Strict contexts (`resource.`, `attribute.`, `scope.`, `body.`) are honored as written.
|
||||
2. **Matches.** The metadata keys under the key's spellings, grouped into families when the flag is on. Each combination of context and data type is one interpretation.
|
||||
3. **Ambiguity.** A filter settles several interpretations by resource over attribute, with a warning. A column stage keeps every interpretation in metadata order and folds them, so a select or a group by sees the value wherever it is.
|
||||
4. **Intrinsic column first**, bare keys only. A column every row has leads the list, whether metadata reports it or the storage's `Fallback` does. Sentinel-reading fields with a contradicting data type drop. A metadata gap degrades to the correct column, never to a corrupt metadata key.
|
||||
5. **Fallback.** With no match, the storage's fallback keys for the key. When the storage ignores unknown keys (a side query whose main query owns the error), the key is `Skipped`. Otherwise a key nothing can serve is an error with suggestions. The not-found warning fires only when every fallback key is a guess, that is, none of them is always present.
|
||||
|
||||
The condition builder then applies the fingerprint split (`MainOfSplit` drops the resource fields the sub-query serves and keeps fallback keys; `FingerprintOfSplit` keeps resource fields only), compiles each field, and the visitor joins the per-field conditions by the operator's polarity. The column expression builder reads each field through `ColumnRead`, casts for the coerced stages unless the read keeps its type, guards by `Absent`, and renders one candidate bare or several as `multiIf(..., NULL)`. A candidate that is not selectable (`ErrNotSelectable`) drops; the error surfaces only when none remains.
|
||||
|
||||
### Invariant: No layer skipping
|
||||
|
||||
The StatementBuilder must not call FieldMapper directly to build conditions, it goes through the ConditionBuilder. The AggExprRewriter must not hardcode column names, it goes through the FieldMapper. Skipping layers creates hidden coupling and makes the system fragile to storage changes.
|
||||
A statement builder must not spell a column or a condition. It calls `ResolveColumn` and the filter visitor. A storage must not decide a guard or an ambiguity. It declares `Absent` and answers the five questions. Skipping layers recreates the per-signal copies the contract removed.
|
||||
|
||||
---
|
||||
|
||||
@@ -119,14 +164,15 @@ Only additive/counting aggregations (`count`, `count_distinct`, `sum`, `rate`) d
|
||||
|
||||
**Enforcement:** `GetQueriesSupportingZeroDefault` determines which queries can default to zero. The `FormulaEvaluator` consumes this via `canDefaultZero`. Changes to aggregation handling must preserve this distinction.
|
||||
|
||||
### Constraint: Existence semantics differ for positive vs negative operators
|
||||
### Constraint: The exists guard derives from the operator and from the field
|
||||
|
||||
- **Positive operators** (`=`, `>`, `LIKE`, `IN`, etc.) implicitly assert field existence. `http.method = GET` means "the field exists AND equals GET".
|
||||
- **Negative operators** (`!=`, `NOT IN`, `NOT LIKE`, etc.) do **not** add an existence check. `http.method != GET` includes records where the field doesn't exist at all.
|
||||
- **Positive operators** (`=`, `>`, `LIKE`, `IN`, etc.) implicitly assert field existence for a field that reads a sentinel when absent. `http.method = GET` on a map attribute means "the field exists AND equals GET".
|
||||
- **Negative operators** (`!=`, `NOT IN`, `NOT LIKE`, etc.) never add an existence check. `http.method != GET` includes records where the field doesn't exist at all.
|
||||
- A field that reads NULL when absent, and a table column, take no guard on any operator: the comparison already excludes the absent row, or there is no absent row.
|
||||
|
||||
**Why:** The user's intent with negative operators is ambiguous. Rather than guess, we take the broader interpretation. Users can add an explicit `EXISTS` filter if they want the narrower one. This is documented in `AddDefaultExistsFilter`.
|
||||
**Why:** The user's intent with negative operators is ambiguous. Rather than guess, we take the broader interpretation. Users can add an explicit `EXISTS` filter if they want the narrower one. The operator side is declared in `AddDefaultExistsFilter`; the field side is the `Absent` a storage returns from `Exists`.
|
||||
|
||||
**Consequence:** Any new operator must declare its existence behavior in `AddDefaultExistsFilter`. Do not add operators without considering this.
|
||||
**Consequence:** Any new operator must declare its existence behavior in `AddDefaultExistsFilter`. Any new read must declare what an absent row reads. Never add a guard by hand in a storage.
|
||||
|
||||
### Constraint: Post-processing functions operate on result sets, not in SQL
|
||||
|
||||
@@ -188,11 +234,11 @@ The `MetadataStore` interface provides runtime field discovery and type resoluti
|
||||
|
||||
The same name can map to multiple `TelemetryFieldKey` variants (different contexts, different types). The metadata store returns *all* variants. Resolution to a single field happens during query building, using the query's signal and any explicit context/type hints from the user.
|
||||
|
||||
**Consequence:** Code that calls `GetKey` or `GetKeys` must handle multiple results. Do not assume a name maps to a single field.
|
||||
**Consequence:** Code that calls `GetKey` or `GetKeys` must handle multiple results. Do not assume a name maps to a single field. `querybuilder.Resolve` is where the variants settle: it returns every interpretation as a `LogicalField`, marks the result `Ambiguous`, and carries the warning.
|
||||
|
||||
### Principle: Materialized fields are a performance optimization, not a semantic distinction
|
||||
|
||||
A materialized field and its non-materialized equivalent represent the same logical field. The `Materialized` flag tells the FieldMapper to generate a simpler column expression. The user should never need to know whether a field is materialized.
|
||||
A materialized field and its non-materialized equivalent represent the same logical field. The `Materialized` flag tells the storage's `Read` to generate a simpler column expression. The user should never need to know whether a field is materialized.
|
||||
|
||||
### Principle: JSON body fields require access plans
|
||||
|
||||
@@ -203,14 +249,14 @@ Fields inside JSON body columns (`body.response.errors[].code`) need pre-compute
|
||||
## Summary of Inviolable Rules
|
||||
|
||||
1. **User-facing types never contain ClickHouse column names or SQL fragments.**
|
||||
2. **Field-to-column translation only happens in FieldMapper.**
|
||||
2. **Field-to-column translation only happens in a Storage (`Read`, `Exists`, `Fallback`).**
|
||||
3. **Normalization happens once at the API boundary, never deeper.**
|
||||
4. **Historical aliases in fieldContexts and fieldDataTypes must not be removed.**
|
||||
5. **Formula evaluation stays in Go — do not push it into ClickHouse JOINs.**
|
||||
6. **Zero-defaulting is aggregation-type-dependent — do not universally default to zero.**
|
||||
7. **Positive operators imply existence, negative operators do not.**
|
||||
7. **The exists guard derives from `AddDefaultExistsFilter` and `Absent`; positive operators guard sentinel reads, negative operators never guard.**
|
||||
8. **Post-processing functions operate on Go result sets, not in SQL.**
|
||||
9. **All user-facing types reject unknown JSON fields with suggestions.**
|
||||
10. **Validation rules are gated by request type.**
|
||||
11. **Query names must be unique within a composite query.**
|
||||
12. **The four-layer abstraction stack (FieldMapper -> ConditionBuilder -> AggExprRewriter -> StatementBuilder) must not be bypassed or flattened.**
|
||||
12. **The three-layer abstraction stack (Storage -> querybuilder generic layer -> StatementBuilder) must not be bypassed or flattened. A storage describes its field keys; the generic layer decides.**
|
||||
|
||||
@@ -565,12 +565,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
// Root V2 pages own the dashboard fetch lifecycle; useDashboardFetchRequired wraps it.
|
||||
// Root dashboard pages own the fetch lifecycle; useDashboardFetchRequired wraps it.
|
||||
// Everywhere else must use useDashboardFetchRequired().
|
||||
"files": [
|
||||
"src/pages/DashboardPageV2/DashboardPageV2.tsx",
|
||||
"src/pages/DashboardPageV2/PanelEditorPage/PanelEditorPage.tsx",
|
||||
"src/pages/DashboardPageV2/DashboardContainer/hooks/useDashboardFetchRequired.ts"
|
||||
"src/pages/DashboardPage/DashboardPage.tsx",
|
||||
"src/pages/DashboardPage/PanelEditorPage/PanelEditorPage.tsx",
|
||||
"src/pages/DashboardPage/DashboardContainer/hooks/useDashboardFetchRequired.ts"
|
||||
],
|
||||
"rules": {
|
||||
"signoz/no-dashboard-fetch-outside-root": "off"
|
||||
|
||||
@@ -94,23 +94,18 @@ export const OnboardingV2 = Loadable(
|
||||
export const DashboardsListPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPageV2'
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPage'
|
||||
),
|
||||
);
|
||||
|
||||
export const DashboardPage = Loadable(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPageV2'),
|
||||
);
|
||||
|
||||
export const DashboardWidget = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'),
|
||||
);
|
||||
|
||||
export const DashboardPanelEditorPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPageV2/PanelEditorPage/PanelEditorPage'
|
||||
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPage/PanelEditorPage/PanelEditorPage'
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
DashboardPage,
|
||||
DashboardPanelEditorPage,
|
||||
DashboardsListPage,
|
||||
DashboardWidget,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
ForgotPassword,
|
||||
@@ -183,13 +182,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: false,
|
||||
key: 'PUBLIC_DASHBOARD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.DASHBOARD_WIDGET,
|
||||
exact: true,
|
||||
component: DashboardWidget,
|
||||
isPrivate: true,
|
||||
key: 'DASHBOARD_WIDGET',
|
||||
},
|
||||
{
|
||||
path: ROUTES.DASHBOARD_PANEL_EDITOR,
|
||||
exact: true,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { GetPublicDashboardDataProps, PayloadProps,PublicDashboardDataProps } from 'types/api/dashboard/public/get';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetPublicDashboardData` hook (or `getPublicDashboardData` fetcher) from
|
||||
* `api/generated/services/dashboard` 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 getPublicDashboardData = async (props: GetPublicDashboardDataProps): Promise<SuccessResponseV2<PublicDashboardDataProps>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/public/dashboards/${props.id}`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPublicDashboardData;
|
||||
@@ -1,34 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { MetricRangePayloadV5 } from 'api/v5/v5';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { GetPublicDashboardWidgetDataProps } from 'types/api/dashboard/public/getWidgetData';
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetPublicDashboardWidgetQueryRange` hook (or `getPublicDashboardWidgetQueryRange` fetcher) from
|
||||
* `api/generated/services/dashboard` 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 getPublicDashboardWidgetData = async (props: GetPublicDashboardWidgetDataProps): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
|
||||
try {
|
||||
const response = await axios.get(`/public/dashboards/${props.id}/widgets/${props.index}/query_range`, {
|
||||
params: {
|
||||
startTime: props.startTime,
|
||||
endTime: props.endTime,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPublicDashboardWidgetData;
|
||||
@@ -1,20 +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/dashboard/get';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
|
||||
|
||||
@@ -5,15 +5,15 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Card } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||
import { CardContainer } from 'container/WidgetCard/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { ChevronDown, ChevronUp } from '@signozhq/icons';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Col, Row } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { Button } from 'container/MetricsApplication/Tabs/styles';
|
||||
import { useGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { getQueryPayloadFromWidgetsData } from 'pages/Celery/CeleryOverview/CeleryOverviewUtils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { History, Location } from 'history';
|
||||
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -4,10 +4,9 @@ import { useSelector } from 'react-redux';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
|
||||
import useUpdatedQuery from 'container/WidgetCard/hooks/useResolveQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
@@ -80,7 +79,6 @@ export function useNavigateToExplorer(): (
|
||||
);
|
||||
|
||||
const { getUpdatedQuery } = useUpdatedQuery();
|
||||
const { dashboardData } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
@@ -112,7 +110,6 @@ export function useNavigateToExplorer(): (
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
},
|
||||
dashboardData,
|
||||
})
|
||||
.then((query) => {
|
||||
preparedQuery = query;
|
||||
@@ -136,13 +133,6 @@ export function useNavigateToExplorer(): (
|
||||
|
||||
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
|
||||
},
|
||||
[
|
||||
prepareQuery,
|
||||
minTime,
|
||||
maxTime,
|
||||
getUpdatedQuery,
|
||||
dashboardData,
|
||||
notifications,
|
||||
],
|
||||
[prepareQuery, minTime, maxTime, getUpdatedQuery, notifications],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
||||
import { generateGridTitle } from 'utils/generateGridTitle';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Spline } from '@signozhq/icons';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import QueryTypeTag from '../QueryTypeTag';
|
||||
import QueryTypeTag from 'components/QueryTypeTag/QueryTypeTag';
|
||||
|
||||
interface IPlotTagProps {
|
||||
queryType: EQueryType;
|
||||
@@ -17,12 +17,6 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Shrink the suggestion-fetch debounce (300ms in prod) so these integration
|
||||
// tests aren't paced by it; coalescing semantics stay intact.
|
||||
jest.mock('../QuerySearch/constants', () => ({
|
||||
|
||||
@@ -21,12 +21,6 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
|
||||
const handleRunQuery = jest.fn();
|
||||
return {
|
||||
@@ -152,15 +146,16 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for debounced API call (300ms debounce + some buffer)
|
||||
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
const lastArgs = mockedGetKeysOnMount.mock.calls[
|
||||
mockedGetKeysOnMount.mock.calls.length - 1
|
||||
]?.[0] as { signal: unknown; searchText: string };
|
||||
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
|
||||
// Wait for the mount fetch specifically. A debounced fetch from an earlier test
|
||||
// can still land after mockClear(), so waiting on "any call" would let this
|
||||
// assert against that one instead and make the result order-dependent.
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(mockedGetKeysOnMount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signal: DataSource.LOGS, searchText: '' }),
|
||||
),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('calls provided onRun on Mod-Enter', async () => {
|
||||
|
||||
@@ -31,12 +31,6 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn().mockResolvedValue({
|
||||
data: { data: { keys: {} } },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { timeItems } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { timeItems } from 'constants/timePreference';
|
||||
|
||||
export const menuItems = timeItems.map((item) => ({
|
||||
key: item.enum,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import TimeItems, {
|
||||
timePreferance,
|
||||
timePreferenceType,
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
} from 'constants/timePreference';
|
||||
|
||||
import { menuItems } from './config';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CircleAlert } from '@signozhq/icons';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
|
||||
import { getBackgroundColorAndThresholdCheck } from './utils';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/WidgetCard/Panels/TablePanel/utils';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
|
||||
function doesValueSatisfyThreshold(
|
||||
rawValue: number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Uplot from 'components/Uplot';
|
||||
import GridTableComponent from 'container/GridTableComponent';
|
||||
import GridValueComponent from 'container/GridValueComponent';
|
||||
import GridTableComponent from 'container/WidgetCard/Panels/TablePanel';
|
||||
import GridValueComponent from 'container/WidgetCard/Panels/ValuePanel';
|
||||
import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent';
|
||||
import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -16,7 +16,6 @@ const ROUTES = {
|
||||
APPLICATION: '/services',
|
||||
ALL_DASHBOARD: '/dashboard',
|
||||
DASHBOARD: '/dashboard/:dashboardId',
|
||||
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
||||
DASHBOARD_PANEL_EDITOR: '/dashboard/:dashboardId/panel/:panelId',
|
||||
EDIT_ALERTS: '/alerts/edit',
|
||||
LIST_ALL_ALERT: '/alerts',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { MessageContext } from 'api/ai-assistant/chat';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { AlertListTabs } from 'pages/AlertList/types';
|
||||
import { NEW_PANEL_ID } from 'pages/DashboardPageV2/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
import { NEW_PANEL_ID } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getAllEndpointsWidgetData,
|
||||
getGroupByFiltersFromGroupByValues,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Card } from 'antd';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
|
||||
function MetricOverTimeGraph({
|
||||
widget,
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
getStatusCodeBarChartWidgetData,
|
||||
statusCodeWidgetInfo,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { handleGraphClick } from 'container/WidgetCard/Card/utils';
|
||||
import { useGraphClickToShowButton } from 'container/WidgetCard/hooks/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/WidgetCard/hooks/useNavigateToExplorerPages';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -23,7 +23,7 @@ import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
|
||||
@@ -17,7 +17,7 @@ jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
getGroupByFiltersFromGroupByValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/GridCard', () => ({
|
||||
jest.mock('container/WidgetCard/Card', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ customOnRowClick }) => (
|
||||
<div data-testid="grid-card-mock">
|
||||
|
||||
@@ -21,15 +21,12 @@ interface MockQueryResult {
|
||||
}
|
||||
|
||||
// Mocks
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/BarChart/BarChart',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}),
|
||||
);
|
||||
jest.mock('lib/visualization/charts/BarChart/BarChart', () => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}));
|
||||
|
||||
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
|
||||
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
|
||||
@@ -43,7 +40,7 @@ jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
|
||||
jest.mock('container/WidgetCard/hooks/useGraphClickToShowButton', () => ({
|
||||
useGraphClickToShowButton: (): {
|
||||
componentClick: boolean;
|
||||
htmlRef: HTMLElement | null;
|
||||
@@ -53,7 +50,7 @@ jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/useNavigateToExplorerPages', () => ({
|
||||
jest.mock('container/WidgetCard/hooks/useNavigateToExplorerPages', () => ({
|
||||
__esModule: true,
|
||||
default: (): { navigateToExplorerPages: jest.Mock } => ({
|
||||
navigateToExplorerPages: jest.fn(),
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'components/QuickFilters/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { GraphClickMetaData } from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import { GraphClickMetaData } from 'container/WidgetCard/hooks/useNavigateToExplorerPages';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { convertNanoToMilliseconds } from 'container/MetricsExplorer/Summary/utils';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -18,7 +18,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { ArrowUpDown, ChevronDown, ChevronRight, Info } from '@signozhq/icons';
|
||||
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Card, Flex } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import type { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import type { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
@@ -98,7 +98,7 @@ jest.mock('api/channels/getAll', () => ({
|
||||
}));
|
||||
|
||||
// Mock alert format categories
|
||||
jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
|
||||
jest.mock('constants/formats/alertFormatCategories', () => ({
|
||||
getCategoryByOptionId: jest.fn(() => ({ name: 'bytes' })),
|
||||
getCategorySelectOptionByName: jest.fn(() => [
|
||||
{ label: 'Bytes', value: 'bytes' },
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import PlotTag from 'components/PlotTag/PlotTag';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -44,7 +44,7 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
jest.mock(
|
||||
'container/NewWidget/LeftContainer/WidgetGraph/PlotTag',
|
||||
'components/PlotTag/PlotTag',
|
||||
() =>
|
||||
function MockPlotTag(props: any): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useHistory } from 'react-router-dom';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { MOCK_QUERY } from 'container/QueryTable/Drilldown/__tests__/mockTableData';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import {
|
||||
defaultFeatureFlags,
|
||||
@@ -13,15 +12,11 @@ import {
|
||||
} from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 } from 'uuid';
|
||||
import { buildExportPanelLink } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
|
||||
import ExplorerOptionWrapper from '../ExplorerOptionWrapper';
|
||||
import { getExplorerToolBarVisibility } from '../utils';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('hooks/dashboard/useUpdateDashboard');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: jest.fn(),
|
||||
@@ -40,7 +35,6 @@ const mockGetExplorerToolBarVisibility = jest.mocked(
|
||||
getExplorerToolBarVisibility,
|
||||
);
|
||||
|
||||
const mockUseUpdateDashboard = jest.mocked(useUpdateDashboard);
|
||||
const mockUseHistory = jest.mocked(useHistory);
|
||||
|
||||
// Mock data
|
||||
@@ -143,17 +137,6 @@ describe('ExplorerOptionWrapper', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetExplorerToolBarVisibility.mockReturnValue(true);
|
||||
// Mock useUpdateDashboard to return a mutation object
|
||||
mockUseUpdateDashboard.mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
data: undefined,
|
||||
error: null,
|
||||
reset: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useUpdateDashboard>);
|
||||
});
|
||||
|
||||
it('should navigate to alert creation page when "Create an Alert" is clicked in logs-explorer', async () => {
|
||||
@@ -291,34 +274,27 @@ describe('ExplorerOptionWrapper', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should test actual handleExport function with generateExportToDashboardLink and verify useUpdateDashboard is NOT called', async () => {
|
||||
it('should navigate to the panel editor via the export link without writing the dashboard', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
// Mock the safeNavigate function
|
||||
const mockSafeNavigate = jest.fn();
|
||||
|
||||
// Get the mock mutate function to track calls
|
||||
const mockMutate = mockUseUpdateDashboard().mutate as jest.MockedFunction<
|
||||
(...args: unknown[]) => void
|
||||
>;
|
||||
|
||||
const panelTypeParam = PANEL_TYPES.TIME_SERIES;
|
||||
const widgetId = v4();
|
||||
const query = mockQuery;
|
||||
|
||||
// Create a real handleExport function similar to LogsExplorerViews
|
||||
// This should NOT call useUpdateDashboard (as per PR #8029)
|
||||
// Export navigates only; it must not write the dashboard (PR #8029).
|
||||
const handleExport = (dashboard: ExportDashboard | null): void => {
|
||||
if (!dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the actual generateExportToDashboardLink function (not mocked)
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
// Call the real link builder (not mocked)
|
||||
const dashboardEditView = buildExportPanelLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
// Simulate navigation
|
||||
@@ -379,15 +355,13 @@ describe('ExplorerOptionWrapper', () => {
|
||||
// Wait for the handleExport function to be called and navigation to occur
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
|
||||
// V2 panel-editor link: compositeQuery is double-encoded (see newPanelRoute).
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/dashboard/${TEST_DASHBOARD_ID}/new?graphType=${panelTypeParam}&widgetId=${widgetId}&compositeQuery=${encodeURIComponent(
|
||||
JSON.stringify(query),
|
||||
`/dashboard/${TEST_DASHBOARD_ID}/panel/new?panelKind=signoz%2FTimeSeriesPanel&compositeQuery=${encodeURIComponent(
|
||||
encodeURIComponent(JSON.stringify(query)),
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Assert that useUpdateDashboard was NOT called (as per PR #8029)
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/query';
|
||||
import ClickHouseQueryBuilder from 'container/QueryBuilder/rawQueryEditors/ClickHouse/query';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/ClickHouse.styles.scss';
|
||||
import 'container/QueryBuilder/rawQueryEditors/ClickHouse/ClickHouse.styles.scss';
|
||||
|
||||
const ALERT_TYPE_DOC_LINK: Partial<Record<AlertTypes, string>> = {
|
||||
[AlertTypes.LOGS_BASED_ALERT]: DOCLINKS.QUERY_CLICKHOUSE_LOGS,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import TimeSeries from 'lib/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
MiscellaneousFormats,
|
||||
ThroughputFormats,
|
||||
TimeFormats,
|
||||
} from 'container/NewWidget/RightContainer/types';
|
||||
} from 'constants/formats/types';
|
||||
|
||||
export const dataFormatConfig: Record<DataFormats, number> = {
|
||||
[DataFormats.BytesIEC]: 1,
|
||||
|
||||
@@ -15,9 +15,9 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { populateMultipleResults } from 'lib/query/populateMultipleResults';
|
||||
import { getFormatNameByOptionId } from 'constants/formats/alertFormatCategories';
|
||||
import { timePreferenceType } from 'constants/timePreference';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataFormats } from 'container/NewWidget/RightContainer/types';
|
||||
import { DataFormats } from 'constants/formats/types';
|
||||
|
||||
import { covertIntoDataFormats } from './utils';
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import {
|
||||
BooleanFormats,
|
||||
DataFormats,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
MiscellaneousFormats,
|
||||
ThroughputFormats,
|
||||
TimeFormats,
|
||||
} from 'container/NewWidget/RightContainer/types';
|
||||
} from 'constants/formats/types';
|
||||
import { TFunction } from 'i18next';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import PromQLQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query';
|
||||
import PromQLQueryBuilder from 'container/QueryBuilder/rawQueryEditors/PromQL/query';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
function PromqlSection(): JSX.Element {
|
||||
|
||||
@@ -26,8 +26,8 @@ import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import QueryTypeTag from 'components/QueryTypeTag/QueryTypeTag';
|
||||
import PlotTag from 'components/PlotTag/PlotTag';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
const THRESHOLD_COLORS_SORTING_ORDER = ['Red', 'Orange', 'Green', 'Blue'];
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { FC, forwardRef, memo, useMemo } from 'react';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { getComponentForPanelType } from 'constants/panelTypes';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||
|
||||
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
|
||||
|
||||
const GridPanelSwitch = forwardRef<
|
||||
ToggleGraphProps | undefined,
|
||||
GridPanelSwitchProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
panelType,
|
||||
data,
|
||||
yAxisUnit,
|
||||
panelData,
|
||||
query,
|
||||
options,
|
||||
thresholds,
|
||||
dataSource,
|
||||
},
|
||||
ref,
|
||||
): JSX.Element | null => {
|
||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||
const result: PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: {
|
||||
data,
|
||||
options,
|
||||
ref,
|
||||
},
|
||||
[PANEL_TYPES.VALUE]: {
|
||||
data,
|
||||
yAxisUnit,
|
||||
thresholds,
|
||||
},
|
||||
[PANEL_TYPES.TABLE]: {
|
||||
...GRID_TABLE_CONFIG,
|
||||
data: panelData,
|
||||
query,
|
||||
thresholds,
|
||||
sticky: true,
|
||||
},
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.PIE]: null,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.BAR]: {
|
||||
data,
|
||||
options,
|
||||
ref,
|
||||
},
|
||||
[PANEL_TYPES.HISTOGRAM]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
};
|
||||
|
||||
return result;
|
||||
}, [data, options, ref, yAxisUnit, thresholds, panelData, query]);
|
||||
|
||||
const Component = getComponentForPanelType(panelType, dataSource) as FC<
|
||||
PropsTypePropsMap[typeof panelType]
|
||||
>;
|
||||
const componentProps = useMemo(
|
||||
() => currentProps[panelType],
|
||||
[panelType, currentProps],
|
||||
);
|
||||
|
||||
if (!Component || !componentProps) {
|
||||
return null;
|
||||
}
|
||||
return <Component {...componentProps} />;
|
||||
},
|
||||
);
|
||||
|
||||
GridPanelSwitch.displayName = 'GridPanelSwitch';
|
||||
|
||||
export default memo(GridPanelSwitch);
|
||||
@@ -1,48 +0,0 @@
|
||||
import { ForwardedRef } from 'react';
|
||||
import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { UplotProps } from 'components/Uplot/Uplot';
|
||||
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
||||
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
||||
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { PANEL_TYPES } from '../../constants/queryBuilder';
|
||||
|
||||
export type GridPanelSwitchProps = {
|
||||
panelType: PANEL_TYPES;
|
||||
data: uPlot.AlignedData;
|
||||
options: uPlot.Options;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
staticLine?: StaticLineProps;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
panelData: QueryDataV3[];
|
||||
query: Query;
|
||||
thresholds?: Widgets['thresholds'];
|
||||
dataSource?: DataSource;
|
||||
selectedLogFields?: Widgets['selectedLogFields'];
|
||||
selectedTracesFields?: Widgets['selectedTracesFields'];
|
||||
selectedTime?: timePreferance;
|
||||
};
|
||||
|
||||
export type PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: UplotProps & {
|
||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||
};
|
||||
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
||||
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
||||
[PANEL_TYPES.TRACE]: null;
|
||||
[PANEL_TYPES.PIE]: null;
|
||||
[PANEL_TYPES.LIST]: null;
|
||||
[PANEL_TYPES.BAR]: UplotProps & {
|
||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||
};
|
||||
[PANEL_TYPES.HISTOGRAM]: null;
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import TimeSeries from 'lib/visualization/charts/TimeSeries/TimeSeries';
|
||||
import {
|
||||
IRenderTooltipFooterArgs,
|
||||
LegendPosition,
|
||||
|
||||
@@ -50,15 +50,12 @@ jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="uplot-chart">TimeSeries Chart</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
jest.mock('lib/visualization/charts/TimeSeries/TimeSeries', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="uplot-chart">TimeSeries Chart</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Timezone', () => ({
|
||||
useTimezone: (): { timezone: { value: string } } => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DashboardContainer from 'pages/DashboardPageV2/DashboardContainer';
|
||||
import DashboardContainer from 'pages/DashboardPage/DashboardContainer';
|
||||
|
||||
import { useSeededDashboardV2 } from './hooks/useSeededDashboardV2';
|
||||
import styles from './Overview.module.scss';
|
||||
|
||||
@@ -13,7 +13,7 @@ import LLMObservability from '../LLMObservability';
|
||||
// The Overview tab renders the full V2 DashboardContainer (toolbar + date picker
|
||||
// call useNavigationType, which needs a data router this integration test doesn't
|
||||
// set up). These cases assert tab routing, not dashboard rendering, so stub it.
|
||||
jest.mock('pages/DashboardPageV2/DashboardContainer', () => ({
|
||||
jest.mock('pages/DashboardPage/DashboardContainer', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="llm-overview-dashboard" />,
|
||||
}));
|
||||
|
||||
@@ -92,12 +92,6 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
|
||||
@@ -22,7 +22,7 @@ import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { getLogPanelColumnsList } from './utils';
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import NewWidget from 'container/NewWidget';
|
||||
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import i18n from 'ReactI18';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayload } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
// Constants
|
||||
const QUERY_RANGE_URL = `${ENVIRONMENT.baseURL}/api/v3/query_range`;
|
||||
const MOCK_SEARCH_PARAMS =
|
||||
'?graphType=list&widgetId=36a7b342-c642-4b92-abe4-cb833a244786&compositeQuery=%7B%22id%22%3A%22b325ac88-5e75-4117-a38c-1a2a7caf8115%22%2C%22builder%22%3A%7B%22queryData%22%3A%5B%7B%22dataSource%22%3A%22logs%22%2C%22queryName%22%3A%22A%22%2C%22aggregateOperator%22%3A%22noop%22%2C%22aggregateAttribute%22%3A%7B%22id%22%3A%22------%22%2C%22dataType%22%3A%22%22%2C%22key%22%3A%22%22%2C%22isColumn%22%3Afalse%2C%22type%22%3A%22%22%2C%22isJSON%22%3Afalse%7D%2C%22timeAggregation%22%3A%22rate%22%2C%22spaceAggregation%22%3A%22sum%22%2C%22functions%22%3A%5B%5D%2C%22filters%22%3A%7B%22items%22%3A%5B%5D%2C%22op%22%3A%22AND%22%7D%2C%22expression%22%3A%22A%22%2C%22disabled%22%3Afalse%2C%22stepInterval%22%3A60%2C%22having%22%3A%5B%5D%2C%22limit%22%3Anull%2C%22orderBy%22%3A%5B%7B%22columnName%22%3A%22timestamp%22%2C%22order%22%3A%22desc%22%7D%5D%2C%22groupBy%22%3A%5B%5D%2C%22legend%22%3A%22%22%2C%22reduceTo%22%3A%22avg%22%2C%22offset%22%3A0%2C%22pageSize%22%3A100%7D%5D%2C%22queryFormulas%22%3A%5B%5D%7D%2C%22clickhouse_sql%22%3A%5B%7B%22name%22%3A%22A%22%2C%22legend%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22query%22%3A%22%22%7D%5D%2C%22promql%22%3A%5B%7B%22name%22%3A%22A%22%2C%22query%22%3A%22%22%2C%22legend%22%3A%22%22%2C%22disabled%22%3Afalse%7D%5D%2C%22queryType%22%3A%22builder%22%7D&relativeTime=30m&options=%7B%22selectColumns%22%3A%5B%5D%2C%22maxLines%22%3A2%2C%22format%22%3A%22list%22%2C%22fontSize%22%3A%22small%22%7D';
|
||||
|
||||
// Mocks
|
||||
|
||||
jest.mock('components/OverlayScrollbar/OverlayScrollbar', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children }: { children: React.ReactNode }): JSX.Element => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string; search: string } => ({
|
||||
pathname: '',
|
||||
search: MOCK_SEARCH_PARAMS,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div>MockDateTimeSelection</div>,
|
||||
}));
|
||||
|
||||
// Helpers
|
||||
const getBuilderQuery = (payload: QueryRangePayload): IBuilderQuery =>
|
||||
payload.compositeQuery.builderQueries?.A as IBuilderQuery;
|
||||
|
||||
const assertTimeRangeConsistency = (
|
||||
payload: QueryRangePayload,
|
||||
initialTimeRange: { start: number; end: number },
|
||||
): void => {
|
||||
expect(payload.start).toBe(initialTimeRange.start);
|
||||
expect(payload.end).toBe(initialTimeRange.end);
|
||||
};
|
||||
|
||||
jest.setTimeout(20000);
|
||||
|
||||
Object.defineProperty(globalThis, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: true,
|
||||
media: query,
|
||||
addListener: (listener: (params: { matches: boolean }) => void): void => {
|
||||
listener({ matches: true });
|
||||
},
|
||||
removeListener: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
describe('LogsPanelComponent', () => {
|
||||
let capturedQueryRangePayloads: QueryRangePayload[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedQueryRangePayloads = [];
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
capturedQueryRangePayloads.push(payload);
|
||||
|
||||
const queryData = getBuilderQuery(payload);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
logsPaginationQueryRangeSuccessResponse({
|
||||
offset: queryData?.offset ?? 0,
|
||||
pageSize: 10,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const renderComponent = async (): Promise<void> => {
|
||||
render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
dashboardId=""
|
||||
dashboardData={undefined}
|
||||
selectedGraph={PANEL_TYPES.LIST}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</I18nextProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('No data')).not.toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
it.skip('should handle pagination flows correctly', async () => {
|
||||
await renderComponent();
|
||||
const initialTimeRange = {
|
||||
start: capturedQueryRangePayloads[0].start,
|
||||
end: capturedQueryRangePayloads[0].end,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /next/i }));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedQueryRangePayloads).toHaveLength(2);
|
||||
});
|
||||
|
||||
const firstPayload = capturedQueryRangePayloads[0];
|
||||
const secondPayload = capturedQueryRangePayloads[1];
|
||||
|
||||
const firstQueryData = getBuilderQuery(firstPayload);
|
||||
const secondQueryData = getBuilderQuery(secondPayload);
|
||||
|
||||
expect(firstQueryData.offset).toBe(0);
|
||||
expect(secondQueryData.offset).toBe(10);
|
||||
assertTimeRangeConsistency(secondPayload, initialTimeRange);
|
||||
const idFilter = secondQueryData.filters?.items?.find(
|
||||
(item) => item?.key?.key === 'id',
|
||||
);
|
||||
expect(idFilter).toBeUndefined();
|
||||
|
||||
const secondOrderByTimestamp = secondQueryData.orderBy?.find(
|
||||
(item) => item.columnName === 'timestamp',
|
||||
);
|
||||
const secondOrderById = secondQueryData.orderBy?.find(
|
||||
(item) => item.columnName === 'id',
|
||||
);
|
||||
expect(secondOrderByTimestamp).toBeDefined();
|
||||
expect(secondOrderById).toBeDefined();
|
||||
expect(secondOrderById?.order).toBe(secondOrderByTimestamp?.order);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /previous/i }));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedQueryRangePayloads).toHaveLength(3);
|
||||
});
|
||||
|
||||
const thirdPayload = capturedQueryRangePayloads[2];
|
||||
const thirdQueryData = getBuilderQuery(thirdPayload);
|
||||
expect(thirdQueryData.offset).toBe(0);
|
||||
assertTimeRangeConsistency(thirdPayload, initialTimeRange);
|
||||
const thirdIdFilter = thirdQueryData.filters?.items?.find(
|
||||
(item) => item?.key?.key === 'id',
|
||||
);
|
||||
expect(thirdIdFilter).toBeUndefined();
|
||||
|
||||
const thirdOrderByTimestamp = thirdQueryData.orderBy?.find(
|
||||
(item) => item.columnName === 'timestamp',
|
||||
);
|
||||
const thirdOrderById = thirdQueryData.orderBy?.find(
|
||||
(item) => item.columnName === 'id',
|
||||
);
|
||||
expect(thirdOrderByTimestamp).toBeDefined();
|
||||
expect(thirdOrderById).toBeDefined();
|
||||
expect(thirdOrderById?.order).toBe(thirdOrderByTimestamp?.order);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { TableColumnsType as ColumnsType } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
|
||||
export const getLogPanelColumnsList = (
|
||||
|
||||
@@ -9,8 +9,8 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card, CardContainer } from 'container/GridCardLayout/styles';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card, CardContainer } from 'container/WidgetCard/styles';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -18,7 +18,7 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
|
||||
@@ -14,6 +14,7 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -21,7 +22,6 @@ import { Filter } from '@signozhq/icons';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
|
||||
@@ -38,6 +38,7 @@ function Explorer(): JSX.Element {
|
||||
currentQuery,
|
||||
} = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const getExportToDashboardLink = useGetExportToDashboardLink();
|
||||
const queryClient = useQueryClient();
|
||||
const [isLoadingQueries, setIsLoadingQueries] = useState(false);
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
@@ -91,16 +92,18 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const widgetId = uuid();
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
const dashboardEditView = getExportToDashboardLink({
|
||||
query: queryToExport || exportDefaultQuery,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
if (dashboardEditView) {
|
||||
safeNavigate(dashboardEditView);
|
||||
}
|
||||
},
|
||||
[exportDefaultQuery, safeNavigate],
|
||||
[exportDefaultQuery, safeNavigate, getExportToDashboardLink],
|
||||
);
|
||||
|
||||
const splitedQueries = useMemo(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useRef } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { GetWidgetQueryBuilderProps } from './types';
|
||||
|
||||
@@ -7,7 +7,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import {
|
||||
databaseCallsAvgDuration,
|
||||
databaseCallsRPS,
|
||||
|
||||
@@ -7,7 +7,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import {
|
||||
externalCallDuration,
|
||||
externalCallDurationByAddress,
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
} from 'constants/apDex';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import DisplayThreshold from 'container/WidgetCard/Header/DisplayThreshold';
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import { SERVICE_DETAIL_DRILLDOWN_ENABLED } from 'container/MetricsApplication/constant';
|
||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
|
||||
function TopLevelOperation({
|
||||
name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownloadOptions } from 'container/Download/Download.types';
|
||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||
import { MenuItemKeys } from 'container/WidgetCard/Header/contants';
|
||||
import {
|
||||
MetricAggregateOperator,
|
||||
TracesAggregatorOperator,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { IServiceName } from './Tabs/types';
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import * as useOptionsMenuHooks from 'container/OptionsMenu';
|
||||
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as useHandleExplorerTabChangeHooks from 'hooks/useHandleExplorerTabChange';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
@@ -95,10 +94,6 @@ jest.mock('react-redux', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.spyOn(useUpdateDashboardHooks, 'useUpdateDashboard').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
jest.spyOn(useOptionsMenuHooks, 'useOptionsMenu').mockReturnValue({
|
||||
options: {
|
||||
selectColumns: [],
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import { Spin } from 'antd';
|
||||
import { useGetMetricReductionRuleTimeseries } from 'api/generated/services/metrics';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Empty } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { EXCLUDED_COLUMNS } from 'container/OptionsMenu/constants';
|
||||
import { QueryKeySuggestionsResponseProps } from 'types/api/querySuggestions/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
type ExplorerAttributeColumnsProps = {
|
||||
isLoading: boolean;
|
||||
data: AxiosResponse<QueryKeySuggestionsResponseProps> | undefined;
|
||||
searchText: string;
|
||||
isAttributeKeySelected: (key: string) => boolean;
|
||||
handleCheckboxChange: (key: string) => void;
|
||||
dataSource: DataSource;
|
||||
};
|
||||
|
||||
function ExplorerAttributeColumns({
|
||||
isLoading,
|
||||
data,
|
||||
searchText,
|
||||
isAttributeKeySelected,
|
||||
handleCheckboxChange,
|
||||
dataSource,
|
||||
}: ExplorerAttributeColumnsProps): JSX.Element {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
<Spinner size="large" tip="Loading..." height="2vh" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const filteredAttributeKeys =
|
||||
Object.values(data?.data?.data?.keys || {})
|
||||
?.flat()
|
||||
?.filter(
|
||||
(attributeKey) =>
|
||||
attributeKey.name.toLowerCase().includes(searchText.toLowerCase()) &&
|
||||
!EXCLUDED_COLUMNS[dataSource].includes(attributeKey.name),
|
||||
) || [];
|
||||
if (filteredAttributeKeys.length === 0) {
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
<Empty description="No columns found" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
{filteredAttributeKeys.map((attributeKey: any) => (
|
||||
<Checkbox
|
||||
value={isAttributeKeySelected(attributeKey.name)}
|
||||
onChange={(): void => handleCheckboxChange(attributeKey.name)}
|
||||
key={attributeKey.name}
|
||||
>
|
||||
{attributeKey.name}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerAttributeColumns;
|
||||
@@ -1,136 +0,0 @@
|
||||
.explorer-columns-renderer {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.ant-typography {
|
||||
color: var(rgba(255, 255, 255, 0.85));
|
||||
font-family: 'Inter';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
--divider-color: var(--l1-border);
|
||||
--divider-margin: 8px 0;
|
||||
}
|
||||
|
||||
.explorer-columns-contents {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 8px;
|
||||
|
||||
.explorer-columns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
overflow-x: scroll;
|
||||
min-width: 90%;
|
||||
|
||||
.explorer-columns-list {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.explorer-column-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px;
|
||||
min-width: 200px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid
|
||||
var(
|
||||
--colorBorder,
|
||||
color-mix(in srgb, var(--bg-robin-300) 12%, transparent)
|
||||
);
|
||||
background: var(--l1-border);
|
||||
cursor: unset;
|
||||
|
||||
.explorer-column-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.lucide-trash2 {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-columns::-webkit-scrollbar {
|
||||
height: 0px; /* Height of the scrollbar */
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0px 16px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-columns-search {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-robin-300) 12%, transparent);
|
||||
border-radius: 6px;
|
||||
padding: 0px;
|
||||
background: var(--l1-background);
|
||||
> input {
|
||||
height: 32px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-columns-dropdown {
|
||||
height: 200px;
|
||||
background-color: var(--l1-border);
|
||||
overflow: hidden !important;
|
||||
|
||||
padding: 4px;
|
||||
.ant-checkbox-wrapper {
|
||||
padding: 2px 8px !important;
|
||||
}
|
||||
|
||||
.attribute-columns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 160px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar {
|
||||
width: 3px; /* Width of the scrollbar */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-track {
|
||||
background: var(--l1-border); /* Color of the track */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-thumb {
|
||||
background: var(--l2-foreground); /* Color of the thumb */
|
||||
border-radius: 4px; /* Roundness of the thumb */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--l1-border); /* Color of the thumb on hover */
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
DropResult,
|
||||
} from 'react-beautiful-dnd';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@signozhq/ui/dropdown-menu';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { FieldDataType } from 'api/v5/v5';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import {
|
||||
CircleAlert,
|
||||
CirclePlus,
|
||||
GripVertical,
|
||||
Search,
|
||||
Trash2,
|
||||
} from '@signozhq/icons';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { WidgetGraphProps } from '../types';
|
||||
import ExplorerAttributeColumns from './ExplorerAttributeColumns';
|
||||
|
||||
import './ExplorerColumnsRenderer.styles.scss';
|
||||
|
||||
type LogColumnsRendererProps = {
|
||||
setSelectedLogFields: WidgetGraphProps['setSelectedLogFields'];
|
||||
selectedLogFields: WidgetGraphProps['selectedLogFields'];
|
||||
selectedTracesFields: WidgetGraphProps['selectedTracesFields'];
|
||||
setSelectedTracesFields: WidgetGraphProps['setSelectedTracesFields'];
|
||||
};
|
||||
|
||||
function ExplorerColumnsRenderer({
|
||||
selectedLogFields,
|
||||
setSelectedLogFields,
|
||||
selectedTracesFields,
|
||||
setSelectedTracesFields,
|
||||
}: LogColumnsRendererProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [querySearchText, setQuerySearchText] = useState<string>('');
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
||||
|
||||
// const { data, isLoading, isError } = useGetAggregateKeys(
|
||||
// {
|
||||
// aggregateAttribute: '',
|
||||
// dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
// aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator,
|
||||
// searchText: querySearchText,
|
||||
// tagType: '',
|
||||
// },
|
||||
// {
|
||||
// queryKey: [
|
||||
// currentQuery.builder.queryData[0].dataSource,
|
||||
// currentQuery.builder.queryData[0].aggregateOperator,
|
||||
// querySearchText,
|
||||
// ],
|
||||
// },
|
||||
// );
|
||||
|
||||
const { data, isLoading, isError } = useGetQueryKeySuggestions(
|
||||
{
|
||||
searchText: querySearchText,
|
||||
signal: currentQuery.builder.queryData[0].dataSource,
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
currentQuery.builder.queryData[0].dataSource,
|
||||
currentQuery.builder.queryData[0].aggregateOperator,
|
||||
querySearchText,
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const isAttributeKeySelected = (key: string): boolean => {
|
||||
if (initialDataSource === DataSource.LOGS && selectedLogFields) {
|
||||
return selectedLogFields.some((field) => field.name === key);
|
||||
}
|
||||
if (initialDataSource === DataSource.TRACES && selectedTracesFields) {
|
||||
return selectedTracesFields.some((field) => field.name === key);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (key: string): void => {
|
||||
if (
|
||||
initialDataSource === DataSource.LOGS &&
|
||||
setSelectedLogFields !== undefined
|
||||
) {
|
||||
if (selectedLogFields) {
|
||||
if (isAttributeKeySelected(key)) {
|
||||
setSelectedLogFields(
|
||||
selectedLogFields.filter((field) => field.name !== key),
|
||||
);
|
||||
} else {
|
||||
setSelectedLogFields([
|
||||
...selectedLogFields,
|
||||
{ dataType: 'string', name: key, type: '' },
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
setSelectedLogFields([{ dataType: 'string', name: key, type: '' }]);
|
||||
}
|
||||
} else if (
|
||||
initialDataSource === DataSource.TRACES &&
|
||||
setSelectedTracesFields !== undefined
|
||||
) {
|
||||
const selectedField = Object.values(data?.data?.data?.keys || {})
|
||||
?.flat()
|
||||
?.find((attributeKey) => attributeKey.name === key);
|
||||
|
||||
if (selectedTracesFields) {
|
||||
if (isAttributeKeySelected(key)) {
|
||||
setSelectedTracesFields(
|
||||
selectedTracesFields.filter((field) => field.name !== key),
|
||||
);
|
||||
} else if (selectedField) {
|
||||
setSelectedTracesFields([
|
||||
...selectedTracesFields,
|
||||
{
|
||||
...selectedField,
|
||||
fieldDataType: selectedField.fieldDataType as FieldDataType,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else if (selectedField) {
|
||||
setSelectedTracesFields([
|
||||
{
|
||||
...selectedField,
|
||||
fieldDataType: selectedField.fieldDataType as FieldDataType,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const debouncedSetQuerySearchText = useDebouncedFn((value) => {
|
||||
setQuerySearchText(value as string);
|
||||
}, 400);
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
debouncedSetQuerySearchText.cancel();
|
||||
},
|
||||
[debouncedSetQuerySearchText],
|
||||
);
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setSearchText(e.target.value);
|
||||
debouncedSetQuerySearchText(e.target.value);
|
||||
};
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean): void => {
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
setSearchText('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelectedLogField = (name: string): void => {
|
||||
if (
|
||||
initialDataSource === DataSource.LOGS &&
|
||||
setSelectedLogFields &&
|
||||
selectedLogFields
|
||||
) {
|
||||
setSelectedLogFields(
|
||||
selectedLogFields.filter((field) => field.name !== name),
|
||||
);
|
||||
}
|
||||
if (
|
||||
initialDataSource === DataSource.TRACES &&
|
||||
setSelectedTracesFields &&
|
||||
selectedTracesFields
|
||||
) {
|
||||
setSelectedTracesFields(
|
||||
selectedTracesFields.filter((field) => field.name !== name),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult): void => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
initialDataSource === DataSource.LOGS &&
|
||||
selectedLogFields &&
|
||||
setSelectedLogFields
|
||||
) {
|
||||
const items = [...selectedLogFields];
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
setSelectedLogFields(items);
|
||||
}
|
||||
if (
|
||||
initialDataSource === DataSource.TRACES &&
|
||||
selectedTracesFields &&
|
||||
setSelectedTracesFields
|
||||
) {
|
||||
const items = [...selectedTracesFields];
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
setSelectedTracesFields(items);
|
||||
}
|
||||
};
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return (
|
||||
<div className="explorer-columns-renderer">
|
||||
<div className="title">
|
||||
<Typography.Text>Columns</Typography.Text>
|
||||
{isError && (
|
||||
<Tooltip title={SOMETHING_WENT_WRONG}>
|
||||
<CircleAlert size={16} data-testid="alert-circle-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<Divider className="explorer-columns-renderer__divider" />
|
||||
{!isError && (
|
||||
<div className="explorer-columns-contents">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="drag-drop-list" direction="horizontal">
|
||||
{(provided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-columns"
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{initialDataSource === DataSource.LOGS &&
|
||||
selectedLogFields &&
|
||||
selectedLogFields.map((field, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Draggable key={index} draggableId={index.toString()} index={index}>
|
||||
{(dragProvided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-column-card"
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
<div className="explorer-column-title">
|
||||
<GripVertical size={12} color="#5A5A5A" />
|
||||
{field.name}
|
||||
</div>
|
||||
<Trash2
|
||||
size={12}
|
||||
color="red"
|
||||
onClick={(): void => removeSelectedLogField(field.name)}
|
||||
data-testid="trash-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{initialDataSource === DataSource.TRACES &&
|
||||
selectedTracesFields &&
|
||||
selectedTracesFields.map((field, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Draggable key={index} draggableId={index.toString()} index={index}>
|
||||
{(dragProvided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-column-card"
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
<div className="explorer-column-title">
|
||||
<GripVertical size={12} color="#5A5A5A" />
|
||||
{field?.name || (field as any)?.key}
|
||||
</div>
|
||||
<Trash2
|
||||
size={12}
|
||||
color="red"
|
||||
onClick={(): void =>
|
||||
removeSelectedLogField(field?.name || (field as any)?.key)
|
||||
}
|
||||
data-testid="trash-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<div>
|
||||
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="action-btn"
|
||||
data-testid="add-columns-button"
|
||||
icon={
|
||||
<CirclePlus
|
||||
size={16}
|
||||
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" className="explorer-columns-dropdown">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="explorer-columns-search"
|
||||
value={searchText}
|
||||
onChange={handleSearchChange}
|
||||
prefix={<Search size={16} style={{ padding: '6px' }} />}
|
||||
/>
|
||||
<ExplorerAttributeColumns
|
||||
isLoading={isLoading}
|
||||
data={data}
|
||||
searchText={searchText}
|
||||
isAttributeKeySelected={isAttributeKeySelected}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
dataSource={initialDataSource}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerColumnsRenderer;
|
||||
@@ -1,9 +0,0 @@
|
||||
.query-section-left-container {
|
||||
border: none;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
.dashboard-navigation {
|
||||
// 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);
|
||||
|
||||
.run-query-dashboard-btn {
|
||||
min-width: 180px;
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
border: none !important;
|
||||
margin-left: 0px !important;
|
||||
padding: 0px !important;
|
||||
|
||||
.nav-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
padding: 7px 23px;
|
||||
|
||||
.prom-ql-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
.ant-btn-default {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.ant-tabs-tab-active {
|
||||
.nav-btns {
|
||||
background: var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin: 0px;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-extra-content {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
.ant-tabs-nav::before {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
.ant-tabs-nav-list {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.ant-tabs-tab + .ant-tabs-tab {
|
||||
border-left: 1px solid var(--l1-border) !important;
|
||||
}
|
||||
.stage-run-query {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
import { PANEL_TYPE_TO_QUERY_TYPES } from 'container/NewWidget/utils';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Atom, Terminal } from '@signozhq/icons';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import ClickHouseQueryContainer from './QueryBuilder/ClickHouse';
|
||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||
|
||||
import './QuerySection.styles.scss';
|
||||
function QuerySection({
|
||||
selectedGraph,
|
||||
isLoadingQueries,
|
||||
handleCancelQuery,
|
||||
selectedWidget,
|
||||
dashboardVersion,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
isNewPanel,
|
||||
}: QueryProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
handleRunQuery: handleRunQueryFromQueryBuilder,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { query } = selectedWidget;
|
||||
|
||||
useShareBuilderUrl({ defaultValue: query });
|
||||
|
||||
const handleQueryCategoryChange = useCallback(
|
||||
(qCategory: string): void => {
|
||||
const currentQueryType = qCategory as EQueryType;
|
||||
redirectWithQueryBuilderData({
|
||||
...currentQuery,
|
||||
queryType: currentQueryType,
|
||||
});
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
const handleRunQuery = (): void => {
|
||||
logEvent('Panel Edit: Stage and run query', {
|
||||
dataSource: currentQuery.builder?.queryData?.[0]?.dataSource,
|
||||
panelType: selectedWidget.panelTypes,
|
||||
queryType: currentQuery.queryType,
|
||||
widgetId: selectedWidget.id,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
isNewPanel,
|
||||
});
|
||||
handleRunQueryFromQueryBuilder();
|
||||
};
|
||||
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
stepInterval: { isHidden: false, isDisabled: false },
|
||||
};
|
||||
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
const queryComponents = useMemo(
|
||||
(): QueryBuilderProps['queryComponents'] => ({}),
|
||||
[],
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const supportedQueryTypes = PANEL_TYPE_TO_QUERY_TYPES[selectedGraph] || [];
|
||||
|
||||
const queryTypeComponents = {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
icon: <Atom size={14} />,
|
||||
label: 'Query Builder',
|
||||
component: (
|
||||
<div className="query-builder-v2-container">
|
||||
<QueryBuilderV2
|
||||
panelType={selectedGraph}
|
||||
filterConfigs={filterConfigs}
|
||||
showTraceOperator={selectedGraph !== PANEL_TYPES.LIST}
|
||||
version={dashboardVersion || 'v3'}
|
||||
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
|
||||
queryComponents={queryComponents}
|
||||
signalSourceChangeEnabled
|
||||
savePreviousQuery
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EQueryType.CLICKHOUSE]: {
|
||||
icon: <Terminal size={14} />,
|
||||
label: 'ClickHouse Query',
|
||||
component: <ClickHouseQueryContainer />,
|
||||
},
|
||||
[EQueryType.PROM]: {
|
||||
icon: (
|
||||
<PromQLIcon
|
||||
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||
/>
|
||||
),
|
||||
label: 'PromQL',
|
||||
component: <PromQLQueryContainer />,
|
||||
},
|
||||
};
|
||||
|
||||
return supportedQueryTypes.map((queryType) => ({
|
||||
key: queryType,
|
||||
label: (
|
||||
<Button className="nav-btns">
|
||||
{queryTypeComponents[queryType].icon}
|
||||
<Typography>{queryTypeComponents[queryType].label}</Typography>
|
||||
</Button>
|
||||
),
|
||||
tab: <Typography>{queryTypeComponents[queryType].label}</Typography>,
|
||||
children: queryTypeComponents[queryType].component,
|
||||
}));
|
||||
}, [
|
||||
queryComponents,
|
||||
selectedGraph,
|
||||
filterConfigs,
|
||||
dashboardVersion,
|
||||
isDarkMode,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(QBShortcuts.StageAndRunQuery, handleRunQuery);
|
||||
|
||||
return (): void => {
|
||||
deregisterShortcut(QBShortcuts.StageAndRunQuery);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handleRunQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
// switch to query builder if query type is not supported
|
||||
if (
|
||||
(selectedGraph === PANEL_TYPES.TABLE || selectedGraph === PANEL_TYPES.PIE) &&
|
||||
currentQuery.queryType === EQueryType.PROM
|
||||
) {
|
||||
handleQueryCategoryChange(EQueryType.QUERY_BUILDER);
|
||||
}
|
||||
}, [currentQuery, handleQueryCategoryChange, selectedGraph]);
|
||||
|
||||
return (
|
||||
<div className="dashboard-navigation">
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={
|
||||
selectedGraph !== PANEL_TYPES.EMPTY_WIDGET
|
||||
? currentQuery.queryType
|
||||
: currentQuery.builder.queryData[0].dataSource
|
||||
}
|
||||
activeKey={currentQuery.queryType}
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||
<RunQueryBtn
|
||||
className="run-query-dashboard-btn"
|
||||
label="Stage & Run Query"
|
||||
onStageRunQuery={handleRunQuery}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryProps {
|
||||
selectedGraph: PANEL_TYPES;
|
||||
isLoadingQueries: boolean;
|
||||
handleCancelQuery: () => void;
|
||||
selectedWidget: Widgets;
|
||||
dashboardVersion?: string;
|
||||
dashboardId?: string;
|
||||
dashboardName?: string;
|
||||
isNewPanel?: boolean;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const InputContainer = styled.div`
|
||||
width: 50%;
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const QueryButton = styled(Button)`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryWrapper = styled.div`
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const QueryBuilderWrapper = styled.div<{ isDarkMode: boolean }>`
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#efefef')};
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
> button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,34 +0,0 @@
|
||||
.widget-graph {
|
||||
border: none;
|
||||
background-color: unset;
|
||||
background-image: radial-gradient(var(--l1-border) 1px, transparent 0);
|
||||
background-size: 20px 20px;
|
||||
padding: 16px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.plot-tag {
|
||||
display: inline-flex;
|
||||
padding: 4px 4px 4px 6px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--l3-background);
|
||||
backdrop-filter: blur(6px);
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.header:has(.date-time-selector:only-child) {
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { WidgetGraphContainerProps } from 'container/NewWidget/types';
|
||||
import APIError from 'types/api/error';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
|
||||
import { NotFoundContainer } from './styles';
|
||||
import { populateMultipleResults } from './util';
|
||||
import WidgetGraph from './WidgetGraphs';
|
||||
|
||||
function WidgetGraphContainer({
|
||||
selectedGraph,
|
||||
queryResponse,
|
||||
setRequestData,
|
||||
selectedWidget,
|
||||
isLoadingPanelData,
|
||||
enableDrillDown = false,
|
||||
}: WidgetGraphContainerProps): JSX.Element {
|
||||
if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
queryResponse.data?.payload.data.result,
|
||||
);
|
||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||
}
|
||||
|
||||
if (queryResponse.data && selectedGraph === PANEL_TYPES.PIE) {
|
||||
const transformedData = populateMultipleResults(queryResponse?.data);
|
||||
queryResponse.data = transformedData;
|
||||
}
|
||||
|
||||
if (selectedWidget === undefined) {
|
||||
return <Card>Invalid widget</Card>;
|
||||
}
|
||||
|
||||
if (queryResponse?.error) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<ErrorInPlace error={queryResponse.error as APIError} />
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
if (queryResponse.isLoading && selectedGraph !== PANEL_TYPES.LIST) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (isLoadingPanelData) {
|
||||
return <Spinner size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (queryResponse.isIdle) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WidgetGraph
|
||||
selectedWidget={selectedWidget}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
selectedGraph={selectedGraph}
|
||||
enableDrillDown={enableDrillDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default WidgetGraphContainer;
|
||||
@@ -1,214 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function WidgetGraph({
|
||||
selectedWidget,
|
||||
queryResponse,
|
||||
setRequestData,
|
||||
selectedGraph,
|
||||
enableDrillDown = false,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const dispatch = useDispatch();
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
// Add legend state management similar to dashboard components
|
||||
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
|
||||
Array((queryResponse.data?.payload?.data?.result?.length || 0) + 1).fill(
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
// Initialize graph visibility when data changes
|
||||
useEffect(() => {
|
||||
if (queryResponse.data?.payload?.data?.result) {
|
||||
setGraphVisibility(
|
||||
Array(queryResponse.data.payload.data.result.length + 1).fill(true),
|
||||
);
|
||||
}
|
||||
}, [queryResponse.data?.payload?.data?.result]);
|
||||
|
||||
// Apply graph visibility when lineChartRef is available
|
||||
useEffect(() => {
|
||||
if (!lineChartRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
graphVisibility.forEach((state, index) => {
|
||||
lineChartRef.current?.toggleGraph(index, state);
|
||||
});
|
||||
}, [graphVisibility]);
|
||||
|
||||
const handleBackNavigation = (): void => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const startTime = searchParams.get(QueryParams.startTime);
|
||||
const endTime = searchParams.get(QueryParams.endTime);
|
||||
const relativeTime = searchParams.get(
|
||||
QueryParams.relativeTime,
|
||||
) as CustomTimeType;
|
||||
|
||||
if (relativeTime) {
|
||||
dispatch(UpdateTimeInterval(relativeTime));
|
||||
} else if (startTime && endTime && startTime !== endTime) {
|
||||
dispatch(
|
||||
UpdateTimeInterval('custom', [
|
||||
parseInt(getTimeString(startTime), 10),
|
||||
parseInt(getTimeString(endTime), 10),
|
||||
]),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
|
||||
const { maxTime, minTime } = GetMinMax('custom', [
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
]);
|
||||
|
||||
urlQuery.set(QueryParams.startTime, minTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, maxTime.toString());
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
},
|
||||
[dispatch, location.pathname, safeNavigate, urlQuery],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('popstate', handleBackNavigation);
|
||||
|
||||
return (): void => {
|
||||
window.removeEventListener('popstate', handleBackNavigation);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
// context redirection to explorer pages
|
||||
const graphClick = useGraphClickToShowButton({
|
||||
graphRef,
|
||||
isButtonEnabled: (selectedWidget?.query?.builder?.queryData &&
|
||||
Array.isArray(selectedWidget.query.builder.queryData)
|
||||
? selectedWidget.query.builder.queryData
|
||||
: []
|
||||
).some(
|
||||
(q) =>
|
||||
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
|
||||
),
|
||||
buttonClassName: 'view-onclick-show-button',
|
||||
});
|
||||
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const graphClickHandler = (
|
||||
xValue: number,
|
||||
yValue: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
metric?: { [key: string]: string },
|
||||
queryData?: { queryName: string; inFocusOrNot: boolean },
|
||||
): void => {
|
||||
handleGraphClick({
|
||||
xValue,
|
||||
yValue,
|
||||
mouseX,
|
||||
mouseY,
|
||||
metric,
|
||||
queryData,
|
||||
widget: selectedWidget,
|
||||
navigateToExplorerPages,
|
||||
navigateToExplorer,
|
||||
notifications,
|
||||
graphClick,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={graphRef}
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
marginTop: '16px',
|
||||
borderRadius: '3px',
|
||||
border: isDarkMode
|
||||
? '1px solid var(--bg-slate-500)'
|
||||
: '1px solid var(--bg-vanilla-300)',
|
||||
background: isDarkMode
|
||||
? 'linear-gradient(0deg, rgba(171, 189, 255, 0.00) 0%, rgba(171, 189, 255, 0.00) 100%), #0B0C0E'
|
||||
: 'var(--bg-vanilla-100)',
|
||||
}}
|
||||
>
|
||||
<PanelWrapper
|
||||
panelMode={PanelMode.DASHBOARD_EDIT}
|
||||
widget={selectedWidget}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
onDragSelect={onDragSelect}
|
||||
selectedGraph={selectedGraph}
|
||||
onClickHandler={graphClickHandler}
|
||||
graphVisibility={graphVisibility}
|
||||
setGraphVisibility={setGraphVisibility}
|
||||
enableDrillDown={enableDrillDown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface WidgetGraphProps {
|
||||
selectedWidget: Widgets;
|
||||
queryResponse: UseQueryResult<MetricQueryRangeSuccessResponse, Error>;
|
||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
enableDrillDown?: boolean;
|
||||
}
|
||||
|
||||
export default WidgetGraph;
|
||||
|
||||
WidgetGraph.defaultProps = {
|
||||
enableDrillDown: false,
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import { SolidInfoCircle } from '@signozhq/icons';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { Warning } from 'types/api';
|
||||
|
||||
import { WidgetGraphContainerProps } from '../../types';
|
||||
import PlotTag from './PlotTag';
|
||||
import { AlertIconContainer, Container } from './styles';
|
||||
import WidgetGraphComponent from './WidgetGraphContainer';
|
||||
|
||||
import './WidgetGraph.styles.scss';
|
||||
|
||||
function WidgetGraph({
|
||||
selectedGraph,
|
||||
queryResponse,
|
||||
setRequestData,
|
||||
selectedWidget,
|
||||
isLoadingPanelData,
|
||||
enableDrillDown = false,
|
||||
isCancelled = false,
|
||||
}: WidgetGraphContainerProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
if (selectedWidget === undefined) {
|
||||
return (
|
||||
<Card $panelType={selectedGraph} isDarkMode={isDarkMode}>
|
||||
Invalid widget
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container $panelType={selectedGraph} className="widget-graph">
|
||||
<div className="header">
|
||||
<div className="header-left">
|
||||
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
|
||||
{!isEmpty(queryResponse.data?.warning) && (
|
||||
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
|
||||
)}
|
||||
</div>
|
||||
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
|
||||
</div>
|
||||
{!isCancelled && queryResponse.error && (
|
||||
<AlertIconContainer color="red" title={queryResponse.error.message}>
|
||||
<SolidInfoCircle size="md" />
|
||||
</AlertIconContainer>
|
||||
)}
|
||||
|
||||
{isCancelled ? (
|
||||
<QueryCancelledPlaceholder subText='Click "Run Query" to reload the chart.' />
|
||||
) : (
|
||||
<WidgetGraphComponent
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
selectedGraph={selectedGraph}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
selectedWidget={selectedWidget}
|
||||
enableDrillDown={enableDrillDown}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(WidgetGraph);
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Card, Tooltip } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
$panelType: PANEL_TYPES;
|
||||
}
|
||||
|
||||
export const Container = styled(Card)<Props>`
|
||||
&&& {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AlertIconContainer = styled(Tooltip)`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
`;
|
||||
|
||||
export const NotFoundContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 47vh;
|
||||
`;
|
||||
|
||||
export const PlotTagWrapperStyled = styled.div<Props>`
|
||||
margin-left: 2rem;
|
||||
margin-top: ${({ $panelType }): string =>
|
||||
$panelType === PANEL_TYPES.TABLE ? '1rem' : '0'};
|
||||
|
||||
margin-bottom: ${({ $panelType }): string =>
|
||||
$panelType === PANEL_TYPES.TABLE ? '1rem' : '0'};
|
||||
`;
|
||||
@@ -1,505 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DropResult } from 'react-beautiful-dnd';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import ExplorerColumnsRenderer from '../ExplorerColumnsRenderer';
|
||||
|
||||
// Mock hooks
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
|
||||
|
||||
// Mock react-beautiful-dnd
|
||||
let onDragEndMock: ((result: DropResult) => void) | undefined;
|
||||
|
||||
jest.mock('react-beautiful-dnd', () => ({
|
||||
DragDropContext: jest.fn(
|
||||
({
|
||||
children,
|
||||
onDragEnd,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDragEnd: (result: any) => void;
|
||||
}) => {
|
||||
onDragEndMock = onDragEnd;
|
||||
return children;
|
||||
},
|
||||
),
|
||||
Droppable: jest.fn(
|
||||
({ children }: { children: (provided: any) => React.ReactNode }) =>
|
||||
children({
|
||||
draggableProps: { style: {} },
|
||||
innerRef: jest.fn(),
|
||||
placeholder: null,
|
||||
}),
|
||||
),
|
||||
Draggable: jest.fn(
|
||||
({ children }: { children: (provided: any) => React.ReactNode }) =>
|
||||
children({
|
||||
draggableProps: { style: {} },
|
||||
innerRef: jest.fn(),
|
||||
dragHandleProps: {},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
// Create a wrapper component with QueryClient
|
||||
const createWrapper = (): React.FC<{ children: React.ReactNode }> => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function Wrapper({ children }: { children: React.ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
|
||||
describe('ExplorerColumnsRenderer', () => {
|
||||
const mockSetSelectedLogFields = jest.fn();
|
||||
const mockSetSelectedTracesFields = jest.fn();
|
||||
const Wrapper = createWrapper();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset mock implementations for useQueryBuilder and useGetQueryKeySuggestions before each test
|
||||
// to ensure a clean state for each test case unless explicitly overridden.
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: 'count',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: [
|
||||
{ name: 'attribute1', dataType: 'string', type: '' },
|
||||
{ name: 'attribute2', dataType: 'string', type: '' },
|
||||
{ name: 'another_attribute', dataType: 'string', type: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correctly with default props and displays "Columns" title', () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Columns')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('add-columns-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays error message when data fetching fails', () => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValueOnce({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('alert-circle-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens and closes the dropdown', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const addButton = screen.getByTestId('add-columns-button');
|
||||
await user.click(addButton);
|
||||
|
||||
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
|
||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||
|
||||
await user.click(addButton);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('filters attribute keys based on search text', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
|
||||
const searchInput = screen.getByPlaceholderText('Search');
|
||||
await userEvent.type(searchInput, 'another');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('attribute1')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('another_attribute')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.clear(searchInput);
|
||||
await userEvent.type(searchInput, 'attribute');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||
expect(screen.getByText('attribute2')).toBeInTheDocument();
|
||||
expect(screen.getByText('another_attribute')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Log Data Source', () => {
|
||||
it('adds a log field when checkbox is checked', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
const checkbox = screen.getByLabelText('attribute1');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(mockSetSelectedLogFields).toHaveBeenCalledWith([
|
||||
{ dataType: 'string', name: 'attribute1', type: '' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes a log field when checkbox is unchecked', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[{ dataType: 'string', name: 'attribute1', type: '' }]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
const checkbox = screen.getByLabelText('attribute1');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(mockSetSelectedLogFields).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('removes a log field using the trash icon', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[{ dataType: 'string', name: 'attribute1', type: '' }]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||
const trashIcon = screen.getByTestId('trash-icon');
|
||||
await userEvent.click(trashIcon);
|
||||
|
||||
expect(mockSetSelectedLogFields).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('reorders log fields on drag and drop', () => {
|
||||
const initialSelectedFields = [
|
||||
{ dataType: 'string', name: 'field1', type: '' },
|
||||
{ dataType: 'string', name: 'field2', type: '' },
|
||||
];
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={initialSelectedFields}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const field1Element = screen.getByText('field1');
|
||||
const dragDropContext = field1Element.closest('.explorer-columns');
|
||||
|
||||
if (dragDropContext && onDragEndMock) {
|
||||
// Simulate onDragEnd directly
|
||||
onDragEndMock({
|
||||
source: { index: 0, droppableId: 'drag-drop-list' },
|
||||
destination: { index: 1, droppableId: 'drag-drop-list' },
|
||||
draggableId: '0',
|
||||
type: 'DEFAULT',
|
||||
reason: 'DROP',
|
||||
combine: undefined,
|
||||
mode: 'FLUID',
|
||||
});
|
||||
|
||||
expect(mockSetSelectedLogFields).toHaveBeenCalledWith([
|
||||
{ dataType: 'string', name: 'field2', type: '' },
|
||||
{ dataType: 'string', name: 'field1', type: '' },
|
||||
]);
|
||||
} else {
|
||||
fail('DragDropContext or onDragEndMock not found');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trace Data Source', () => {
|
||||
beforeEach(() => {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: [
|
||||
{ name: 'trace_attribute1', dataType: 'string', type: 'tag' },
|
||||
{ name: 'trace_attribute2', dataType: 'string', type: 'tag' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a trace field when checkbox is checked', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
const checkbox = screen.getByLabelText('trace_attribute1');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([
|
||||
{ name: 'trace_attribute1', dataType: 'string', type: 'tag' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('removes a trace field when checkbox is unchecked', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[
|
||||
{
|
||||
name: 'trace_attribute1',
|
||||
fieldDataType: DataTypes.String,
|
||||
fieldContext: '',
|
||||
},
|
||||
]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
const checkbox = screen.getByLabelText('trace_attribute1');
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('removes a trace field using the trash icon', async () => {
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[
|
||||
{
|
||||
name: 'trace_attribute1',
|
||||
fieldDataType: DataTypes.String,
|
||||
fieldContext: '',
|
||||
},
|
||||
]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('trace_attribute1')).toBeInTheDocument();
|
||||
const trashIcon = screen.getByTestId('trash-icon');
|
||||
await userEvent.click(trashIcon);
|
||||
|
||||
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('reorders trace fields on drag and drop', () => {
|
||||
const initialSelectedFields = [
|
||||
{ name: 'trace_field1', fieldDataType: 'string', fieldContext: 'tag' },
|
||||
{ name: 'trace_field2', fieldDataType: 'string', fieldContext: 'tag' },
|
||||
];
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={initialSelectedFields as TelemetryFieldKey[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
const traceField1Element = screen.getByText('trace_field1');
|
||||
const dragDropContext = traceField1Element.closest('.explorer-columns');
|
||||
if (dragDropContext && onDragEndMock) {
|
||||
// Simulate onDragEnd directly
|
||||
onDragEndMock({
|
||||
source: { index: 0, droppableId: 'drag-drop-list' },
|
||||
destination: { index: 1, droppableId: 'drag-drop-list' },
|
||||
draggableId: '0',
|
||||
type: 'DEFAULT',
|
||||
reason: 'DROP',
|
||||
combine: undefined,
|
||||
mode: 'FLUID',
|
||||
});
|
||||
|
||||
expect(mockSetSelectedTracesFields).toHaveBeenCalledWith([
|
||||
{ name: 'trace_field2', fieldDataType: 'string', fieldContext: 'tag' },
|
||||
{ name: 'trace_field1', fieldDataType: 'string', fieldContext: 'tag' },
|
||||
]);
|
||||
} else {
|
||||
fail('DragDropContext or onDragEndMock not found');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show isRoot or isEntryPoint in add column dropdown (traces, dashboard table panel)', async () => {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: [
|
||||
{ name: 'isRoot', dataType: 'bool', type: '' },
|
||||
{ name: 'isEntryPoint', dataType: 'bool', type: '' },
|
||||
{ name: 'duration', dataType: 'number', type: '' },
|
||||
{ name: 'serviceName', dataType: 'string', type: '' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={[]}
|
||||
setSelectedLogFields={mockSetSelectedLogFields}
|
||||
selectedTracesFields={[]}
|
||||
setSelectedTracesFields={mockSetSelectedTracesFields}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('add-columns-button'));
|
||||
|
||||
// Visible columns should appear
|
||||
expect(screen.getByText('duration')).toBeInTheDocument();
|
||||
expect(screen.getByText('serviceName')).toBeInTheDocument();
|
||||
|
||||
// Hidden columns should NOT appear
|
||||
expect(screen.queryByText('isRoot')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('isEntryPoint')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { WidgetGraphProps } from '../types';
|
||||
import ExplorerColumnsRenderer from './ExplorerColumnsRenderer';
|
||||
import QuerySection from './QuerySection';
|
||||
import { QueryContainer } from './styles';
|
||||
import WidgetGraph from './WidgetGraph';
|
||||
|
||||
import './LeftContainer.styles.scss';
|
||||
|
||||
function LeftContainer({
|
||||
selectedGraph,
|
||||
selectedLogFields,
|
||||
setSelectedLogFields,
|
||||
selectedTracesFields,
|
||||
setSelectedTracesFields,
|
||||
selectedWidget,
|
||||
requestData,
|
||||
isLoadingPanelData,
|
||||
setRequestData,
|
||||
setQueryResponse,
|
||||
enableDrillDown = false,
|
||||
dashboardData,
|
||||
isNewPanel = false,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedInterval,
|
||||
minTime,
|
||||
maxTime,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
const queryRangeKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedInterval,
|
||||
requestData,
|
||||
minTime,
|
||||
maxTime,
|
||||
],
|
||||
[globalSelectedInterval, requestData, minTime, maxTime],
|
||||
);
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
queryClient.cancelQueries(queryRangeKey);
|
||||
setIsCancelled(true);
|
||||
}, [queryClient, queryRangeKey]);
|
||||
|
||||
const queryResponse = useGetQueryRange(requestData, ENTITY_VERSION_V5, {
|
||||
enabled: !!stagedQuery,
|
||||
queryKey: queryRangeKey,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (queryResponse.isFetching) {
|
||||
setIsCancelled(false);
|
||||
}
|
||||
}, [queryResponse.isFetching]);
|
||||
|
||||
// Update parent component with query response for legend colors
|
||||
useEffect(() => {
|
||||
if (setQueryResponse) {
|
||||
setQueryResponse(queryResponse);
|
||||
}
|
||||
}, [queryResponse, setQueryResponse]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetGraph
|
||||
selectedGraph={selectedGraph}
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
selectedWidget={selectedWidget}
|
||||
isLoadingPanelData={isLoadingPanelData}
|
||||
enableDrillDown={enableDrillDown}
|
||||
isCancelled={isCancelled}
|
||||
/>
|
||||
<QueryContainer className="query-section-left-container">
|
||||
<QuerySection
|
||||
selectedGraph={selectedGraph}
|
||||
isLoadingQueries={queryResponse.isFetching}
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
selectedWidget={selectedWidget}
|
||||
dashboardVersion={ENTITY_VERSION_V5}
|
||||
dashboardId={dashboardData?.id}
|
||||
dashboardName={dashboardData?.data.title}
|
||||
isNewPanel={isNewPanel}
|
||||
/>
|
||||
{selectedGraph === PANEL_TYPES.LIST && (
|
||||
<ExplorerColumnsRenderer
|
||||
selectedLogFields={selectedLogFields}
|
||||
setSelectedLogFields={setSelectedLogFields}
|
||||
selectedTracesFields={selectedTracesFields}
|
||||
setSelectedTracesFields={setSelectedTracesFields}
|
||||
/>
|
||||
)}
|
||||
</QueryContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LeftContainer);
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const QueryContainer = styled(Card)`
|
||||
&&& {
|
||||
min-height: 23.5%;
|
||||
}
|
||||
`;
|
||||
@@ -1,74 +0,0 @@
|
||||
.facing-issue-btn-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0px 12px 0px 16px;
|
||||
|
||||
.left-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.discard-icon {
|
||||
color: var(--l1-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.configure-panel {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.new-widget-container {
|
||||
.resizable-panel-left-container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.resizable-panel-right-container {
|
||||
overflow-y: auto !important;
|
||||
min-width: 350px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgb(136, 136, 136);
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-resizable-panel-group {
|
||||
.widget-resizable-handle {
|
||||
height: 100vh;
|
||||
background: color-mix(in srgb, var(--l3-background) 20%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
.column-unit-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.heading {
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Space Mono';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 138.462% */
|
||||
letter-spacing: 0.52px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.y-axis-unit-selector {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
|
||||
.heading {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.y-axis-unit-selector-v2 {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
gap: 24px !important;
|
||||
|
||||
.y-axis-unit-selector-component {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect } from 'react';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ColumnUnit } from 'types/api/dashboard/getAll';
|
||||
|
||||
import YAxisUnitSelectorV2 from '../DashboardYAxisUnitSelectorWrapper';
|
||||
|
||||
import './ColumnUnitSelector.styles.scss';
|
||||
|
||||
interface ColumnUnitSelectorProps {
|
||||
columnUnits: ColumnUnit;
|
||||
setColumnUnits: Dispatch<SetStateAction<ColumnUnit>>;
|
||||
isNewDashboard: boolean;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export function ColumnUnitSelector(
|
||||
props: ColumnUnitSelectorProps,
|
||||
): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { columnUnits, setColumnUnits, isNewDashboard } = props;
|
||||
|
||||
const aggregationQueries = useGetQueryLabels(currentQuery);
|
||||
|
||||
const handleColumnUnitSelect = useCallback(
|
||||
(queryName: string, value: string): void => {
|
||||
setColumnUnits((prev) => ({
|
||||
...prev,
|
||||
[queryName]: value,
|
||||
}));
|
||||
},
|
||||
[setColumnUnits],
|
||||
);
|
||||
|
||||
const getValues = (value: string): string => {
|
||||
const currentValue = columnUnits[value];
|
||||
if (currentValue) {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
// if base query has value, return it
|
||||
const baseQuery = value.split('.')[0];
|
||||
|
||||
if (columnUnits[baseQuery]) {
|
||||
return columnUnits[baseQuery];
|
||||
}
|
||||
|
||||
// if we have value as base query i.e. value = B, but the columnUnit have let say B.count(): 'h' then we need to return B.count()
|
||||
// get the queryName B.count() from the columnUnits keys based on the B that we have (first match - 0th aggregationIndex)
|
||||
const newQueryWithExpression = Object.keys(columnUnits).find(
|
||||
(key) =>
|
||||
key.startsWith(baseQuery) &&
|
||||
!isEmpty(aggregationQueries.find((query) => query.value === key)),
|
||||
);
|
||||
if (newQueryWithExpression) {
|
||||
return columnUnits[newQueryWithExpression];
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const newColumnUnits = aggregationQueries.reduce(
|
||||
(acc, query) => {
|
||||
acc[query.value] = getValues(query.value);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
setColumnUnits(newColumnUnits);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [aggregationQueries]);
|
||||
|
||||
return (
|
||||
<section className="column-unit-selector">
|
||||
<Typography.Text className="heading">Column Units</Typography.Text>
|
||||
<div className="column-unit-selector-content">
|
||||
{aggregationQueries.map(({ value, label }) => {
|
||||
const baseQueryName = value.split('.')[0];
|
||||
return (
|
||||
<YAxisUnitSelectorV2
|
||||
value={columnUnits[value] || ''}
|
||||
onSelect={(unitValue: string): void =>
|
||||
handleColumnUnitSelect(value, unitValue)
|
||||
}
|
||||
fieldLabel={label}
|
||||
key={value}
|
||||
data-testid={props['data-testid']}
|
||||
selectedQueryName={baseQueryName}
|
||||
// Update the column unit value automatically only in create mode
|
||||
shouldUpdateYAxisUnit={isNewDashboard}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user