Compare commits

...

5 Commits

Author SHA1 Message Date
srikanthccv
88a522a638 feat(querybuilder): compile every signal through one storage contract
Each signal had its own copy of name resolution, the guard rule, and the
column renderer, and each copy made its own choices. The semantic
convention families made the differences visible: the same family fold
had to be written seven times, and the guards did not agree.

A storage now answers only what the compiler cannot know: the bare read
of a stored field, its presence test with its absence semantics, the
fallback for a key metadata does not report, its traits, and two
overrides for a storage with its own condition language. Resolution, the
term compiler, and the column renderer live in querybuilder and are
written one time.

- Storage replaces FieldMapper and ConditionBuilder for traces, logs,
  metrics, audit, rule state history, the resource fingerprint, and the
  related-values metadata.
- Absent says how a stored field reads when a row does not carry it,
  and every guard derives from it. A sentinel read takes the exists
  guard on positive operators and in the column stages. A NULL read and
  a table column take none. A declared keyless contract (metrics labels,
  rule state labels) takes none.
- QueryInfo carries the org, the time range, the signal, the queried
  metric, and the query-path flags, evaluated one time per builder stage.
- LogicalField carries ValueMaps, so a family member's stored vocabulary
  translates to the current one in every read.
- The not-found warning fires only when every fallback field is a guess.
  A key under a signal's own context resolves as if it had no context.

Assisted-by: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_014F37Rwd6xYrr5Abi6JJqJW
2026-09-04 18:31:59 +05:30
srikanthccv
25298585b8 fix(querybuilder): scope map attributes take the default exists guard
Logs and audit store scope attributes in a string map, like attributes, but
exempted the scope context from the default exists guard, so `scope.x = ''`
matched every row without the key. A scope attribute now takes the guard.
scope_name and scope_version are columns and take none.

Assisted-by: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_014F37Rwd6xYrr5Abi6JJqJW
2026-09-04 10:53:28 +05:30
srikanthccv
608bb9eeee fix(audit): raw select reads NULL for an absent map attribute
Logs and traces render a raw-select map attribute behind its presence test,
so a row without the key shows NULL. Audit rendered the bare map read, which
shows the empty string. Audit now follows the same contract.

Assisted-by: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_014F37Rwd6xYrr5Abi6JJqJW
2026-09-04 10:53:28 +05:30
srikanthccv
693ea77f90 fix(querybuilder): columns take no guard in the coerced stages
A table column is present in every row. The group-by, order-by, and
aggregation-argument stages wrapped it in its non-empty test, which turned 0
and the empty string into NULL: zero-duration spans left percentiles and
averages, and an empty severity became the NULL group. The stages now read
the column bare. A column among several candidates ends the candidate list,
because no candidate after it can be reached. Traces, logs, and audit.

Assisted-by: Claude Fable 5.1
Claude-Session: https://claude.ai/code/session_014F37Rwd6xYrr5Abi6JJqJW
2026-09-04 10:53:27 +05:30
Abhi kumar
618031ddaa chore(dashboard): retire the V1 panel editor and its widget route (#12648)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
> **Stacked PR — merge bottom-up.** This is part of a stack retiring the
V1 dashboard frontend.
>
> | | PR | Change |
> |---|---|---|
> | 1 | #12647 | delete dead V1 dashboard code |
> | 2 | #12648 | retire the V1 panel editor and its widget route |
> | 3 | #12649 | legacy notice for unmigrated public dashboards |
> | 4 | #12650 | retire the V1 dashboard store |
> | 5 | #12651 | move the shared chart layer to `lib/visualization` |
> | 6 | #12652 | consolidate the widget-card stack under
`container/WidgetCard` |
> | 7 | #12653 | split `types/api/dashboard/getAll` |
> | 8 | #12654 | drop the `V2` suffix |
> 
> `CODEOWNERS` for all of the above is split out into **#12707**, which
is *not* part of this stack (based on `main`) and should merge after it.
> 
> Review this one against **#12647**, not `main`.

---

#### Description

`ROUTES.DASHBOARD_WIDGET` (`/dashboard/:dashboardId/:widgetId`) was
still registered but no UI linked to it, and its page fetched `GET
/api/v1/dashboards/{id}` — which the backend answers **501**. V2 serves
panel editing at `/dashboard/:dashboardId/panel/:panelId`.

- Extract the 12 modules other features still need out of
`container/NewWidget` first, then delete the route, the page and the
container.
- Threshold/format/time types → `types/api/widgets/threshold`,
`constants/formats/*`, `constants/timePreference` (fixing the
`alertFomatCategories` spelling on the way).
- `QueryTypeTag`, `PlotTag`, `populateMultipleResults`, the ContextLinks
utils and the four externally-used `utils` exports → `components/`,
`lib/query/`, `utils/contextLinks/`.
- The two raw query editors →
`container/QueryBuilder/rawQueryEditors/{PromQL,ClickHouse}`, where the
rest of the query-builder UI lives.
- Delete the V1 write path (`useUpdateDashboard` →
`api/v1/dashboards/id/update`) and the orphaned bootstrap chain.

#### Additional Information

**Two behaviour changes worth a look:**

1. **Meter Explorer's "Add to dashboard" was already broken.** It built
a V1 editor URL (`/dashboard/:id/new`), so users landed on the 501 page.
It now uses `useGetExportToDashboardLink` like the Logs, Traces and
Metrics explorers. This is a fix, but it touches Meter Explorer.
2. `FullView`'s **Switch to Edit Mode** button is removed — it built its
link with `generateExportToDashboardLink`, which goes away here, and it
was gated on V1 state nothing populates, so it never rendered.

`WidgetHeader`'s **Edit**/**Delete**/**Clone** items are left alone. No
caller lists them in `headerMenuList`, so nothing renders them either
way, and leaving them keeps this PR scoped to the editor route.

Two tests changed assertions rather than just mocks
(`WidgetGraphComponent.test.tsx`, `ExplorerOptionWrapper.test.tsx`) —
those are the diffs to read closely; the rest is mechanical.

Also extracts `ColumnUnit` to break the `getAll` ↔ `threshold` import
cycle the type move would otherwise have created.

**Verification:** `tsgo`, `lint`, `jest` (751 suites / 7386 tests),
`build`, `knip` all clean.
2026-09-03 15:35:54 +00:00
1209 changed files with 4342 additions and 20989 deletions

View File

@@ -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.**

View File

@@ -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"

View File

@@ -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'
),
);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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],
);
}

View File

@@ -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';

View File

@@ -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;

View File

@@ -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', () => ({

View File

@@ -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 () => {

View File

@@ -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: {} } },

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,

View File

@@ -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';

View File

@@ -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',

View File

@@ -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';
/**

View File

@@ -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';

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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">

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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' },

View File

@@ -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';

View File

@@ -44,7 +44,7 @@ jest.mock(
},
);
jest.mock(
'container/NewWidget/LeftContainer/WidgetGraph/PlotTag',
'components/PlotTag/PlotTag',
() =>
function MockPlotTag(props: any): JSX.Element {
return (

View File

@@ -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();
});
});

View File

@@ -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,

View File

@@ -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';

View File

@@ -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,

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
import { DataFormats } from 'container/NewWidget/RightContainer/types';
import { DataFormats } from 'constants/formats/types';
import { covertIntoDataFormats } from './utils';

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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'];

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -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 } } => ({

View File

@@ -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';

View File

@@ -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" />,
}));

View File

@@ -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: {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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);
});
});

View File

@@ -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 = (

View File

@@ -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';

View File

@@ -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,

View File

@@ -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(

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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';

View File

@@ -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: [],

View File

@@ -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';

View File

@@ -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;

View File

@@ -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 */
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}
`;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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'};
`;

View File

@@ -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();
});
});

View File

@@ -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);

View File

@@ -1,8 +0,0 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const QueryContainer = styled(Card)`
&&& {
min-height: 23.5%;
}
`;

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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