Compare commits

...

9 Commits

Author SHA1 Message Date
Gaurav Tewari
cf211fdae4 fix: test json being send 2026-09-21 23:01:27 +05:30
Aditya Singh
2ff7e7d3af test(traces-explorer): flaky "select a view" test on CI (#12931)
#### Description

- `select a view options` in the traces explorer test flakes on CI..
`findByRole` defaults to a 1s timeout and the saved views call has to
land and re-render the dropdown inside that window. locally the option
only shows up at ~550-680ms, so there is barely any headroom and a
loaded runner tips it over.
- bumped just that one query to 5s. the wait sits on the assertion that
actually times out, so nothing else in the test moves and it still fails
loudly if the option stops rendering.
- costs nothing when things are fast.. findByRole resolves the moment
the option appears, the timeout is only a cap. test stays ~1.2s before
and after.

#### Additional Information

- verified with a repro.. delaying the views handler by 1500ms fails
without the change with the same error CI gives, and passes with it.
- opening the dropdown before the views land is not the problem, antd
re-renders the options once the data arrives. so no reordering needed
here.
- the real driver is the `test / js` job at 7-10 min on a single
unsharded runner. a repo-wide `asyncUtilTimeout` default plus sharding
that job is the proper fix for this class of flake, not doing it here.
2026-09-21 16:43:41 +00:00
Aditya Singh
3ecc21377d refactor(traces): delete the old trace explorer code (#12919)
#### Description

- deleted the old trace explorer at `/trace`. its apis
(`/getSpanFilters`, `/getFilteredSpans`, `/getTagFilters`,
`/getTagValues`) were removed from the backend in #6464 so the page has
been 404ing on every request since then. takes `container/Trace`,
`store/actions/trace`, the `traces` redux slice and the dead `api/trace`
clients with it.
- deleted the dead trees that came along with it.. the pre quick filters
`Filter` panel in traces explorer, `GantChart`, `TraceFlameGraph`,
`TraceDetail` and the `/trace-old` redirect. none of these had a real
consumer, they were only compiling because of a stray type import here
and there.
- moved the shared bits out before deleting anything around them..
`Tags`/`OperatorValues`, `getMs`, `StyledCSS` and the live trace detail
helpers. `DurationSection` now sits next to the quick filters duration
renderer since that is its only consumer. `filterUtils` is split, key
catalogue to constants since three unrelated places read it, rest kept
local to the renderer.
- dropped `ROUTES.TRACE`. four places were using it as a string prefix
to build trace detail links, which only worked because `/trace/:id`
shares the prefix. moved them to `generatePath(ROUTES.TRACE_DETAIL, { id
})` like `FieldCell` already does.
- removed `NewExplorerCTA`. with old logs explorer (#12914) and this one
gone it has no route left to render on.

#### Issues closed by this PR

Part of https://github.com/SigNoz/engineering-pod/issues/6103

#### Additional Information

- `/trace` and `/trace-old/:id` now fall through to home like any
unknown route. did not add a redirect since that means keeping
`ROUTES.TRACE` around along with its permission and authz test entries.
can add it back if we think bookmarks matter.
- `NEW_ROUTES_MENU_ITEM_KEY_MAP` still has a `'/trace'` key on purpose,
sidebar strips the url to its first segment and that is what highlights
Traces on detail pages.
- could not test four paths on staging.. llm explorer list view, kafka
drop rate links, linked spans and the apm view traces popup. all one
line `generatePath` changes with unit coverage.
- both this and #12914 touch `NewExplorerCTA`, whichever merges second
needs a rebase.
2026-09-21 16:34:29 +00:00
Gaurav Tewari
b071610a27 chore: use endpoint for fetching data (#12926)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
Previously we had hardcoded the api data in the Frontend End. now we
have removed it. it's a api from where we fetch data in frontend.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

<!--If applicable, include screenshots or screen recordings that clearly
show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings


https://github.com/user-attachments/assets/1b348d4c-c2c1-47c3-b338-2e5b352ea73c



https://github.com/user-attachments/assets/71009d56-0dc8-44b2-9946-0850086ac397





<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information

<!--Please delete paragraphs that you did not use before submitting.-->

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-21 14:49:41 +00:00
Swapnil Nakade
41d4818030 feat(authz): enable FGA for cloud integration (#12895)
#### Description
- enabling FGA support for cloud integration end points

#### Issues closed by this PR
Closes: https://github.com/SigNoz/platform-pod/issues/2946

---------

Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2026-09-21 14:20:11 +00:00
Abhi kumar
a2e42df790 fix(charts): stop tooltip labels breaking mid-word (#12894)
#### Description

- Chart tooltip labels were breaking mid-word (`Metric` / `s`) whenever
a row's value was wide. `overflow-wrap: anywhere` drops a flex item's
min-content width to one character, so the label was the only thing in
the row that could give way — the value and the dashed leader kept their
width and squeezed it to nothing.
- The label now uses `overflow-wrap: break-word`, so it breaks a token
only when a line can't hold it whole, and wraps at 3 lines before
ellipsizing. `title` carries the full text.
- Tooltip width floor goes 300 → 360. The width comes from the *legend*
label length, so charts with short series names (billing) got the
narrowest box even though their values are the longest. 360 is already
the upper bound that formula produces, so no tooltip is wider than the
widest one today, and the width still never changes between hovers.

#### Screenshots / Screen Recordings

| Before | After |
| --- | --- |
| Label crushed to ~40px, broken mid-word | Label intact on one line,
value pinned at the row end |

#### Additional Information

Verified in a headless browser at the new width: short labels stay on
one line even with a longer value than the reported case, long series
names wrap at token boundaries with the value still at the end of the
row, and no row overflows horizontally.

Closes https://github.com/SigNoz/pulse-pod/issues/365
2026-09-21 11:57:13 +00:00
Naman Verma
f9928aa4db feat: add plugin schema for area chart panel (#12635)
#### Description

Add a new plugin schema for area chart. This also adds a new enum for
fill mode specific to area charts cuz the default is different wrt time
series chart.

Frontend changes to be built on top of this

#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/294
Closes https://github.com/SigNoz/pulse-pod/issues/214

---------

Co-authored-by: Abhi kumar <ahrefabhi@gmail.com>
2026-09-21 11:14:29 +00:00
Aditya Singh
1c7811c414 refactor(log-details): move infra metrics charts to v5 query_range (#12916)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- moves the metrics tab in log details + span details (same InfraMetrics
component) from v4 to v5 query_range.. pod, node and host charts
- no payload rewrite, the v5 preparer already handles the legacy builder
shape so constants stay as is
- having helper now always emits the v5 expression form and the
`useV5HavingFormat` flag is gone. host payload is shared with Hosts V2
which was already on v5, so both callers line up now
- fixed legends while here.. on v5 the response has per-query metadata
and getLegend was resolving against the explorer's currentQuery, so
CPU/Memory usage showed `count()` instead of the pod name. charts now
pass their own payload query, same as K8s V2 EntityMetrics
- node/pod toggle: re-clicking the pressed pill deselects (radix single
toggle-group emits '') and blanked the tab. ignoring empty values now.
pre-existing, component fix tracked in
https://github.com/SigNoz/components/issues/402

#### Issues closed by this PR

Closes https://github.com/SigNoz/engineering-pod/issues/5807




https://github.com/user-attachments/assets/62826754-e1eb-44b3-af60-50a2b614048d



#### Additional Information

- staging has no bare-vm logs (host.name without k8s.node.name), so the
host chart path from the drawer wasn't hit directly.. verified via Hosts
details which uses the same getHostQueryPayload
- cc. @H4ad
2026-09-21 09:01:08 +00:00
Abhi kumar
1839648e75 fix(legend): isolate legend stacking context (#12924)
#### Description

- Legend row markers carry `z-index: 4`, and nothing between them and
the page root established a stacking context, so they competed with (and
beat) the logs explorer quick filter panel at `z-index: 2` — the colored
markers painted on top of the panel.
- `isolation: isolate` on the legend container keeps the row-level
z-indexes local. The legend is a flex sibling of the plot and its
tooltips portal to body, so nothing relied on them escaping.

Before
<img width="2032" height="1048" alt="image"
src="https://github.com/user-attachments/assets/e2c792b7-930c-477f-aeb5-4867434b46cd"
/>


After
<img width="1336" height="493" alt="image"
src="https://github.com/user-attachments/assets/35a67f89-ea97-4773-b050-2f39321e9468"
/>
2026-09-21 08:07:50 +00:00
227 changed files with 2856 additions and 10049 deletions

View File

@@ -3321,6 +3321,53 @@ components:
repeatVariable:
type: string
type: object
DashboardtypesAreaChartAppearance:
properties:
fillMode:
$ref: '#/components/schemas/DashboardtypesAreaFillMode'
fillOpacity:
$ref: '#/components/schemas/DashboardtypesFillOpacity'
lineInterpolation:
$ref: '#/components/schemas/DashboardtypesLineInterpolation'
lineStyle:
$ref: '#/components/schemas/DashboardtypesLineStyle'
showPoints:
type: boolean
spanGaps:
$ref: '#/components/schemas/DashboardtypesSpanGaps'
type: object
DashboardtypesAreaChartPanelSpec:
properties:
axes:
$ref: '#/components/schemas/DashboardtypesAxes'
chartAppearance:
$ref: '#/components/schemas/DashboardtypesAreaChartAppearance'
formatting:
$ref: '#/components/schemas/DashboardtypesPanelFormatting'
legend:
$ref: '#/components/schemas/DashboardtypesLegend'
thresholds:
items:
$ref: '#/components/schemas/DashboardtypesThresholdWithLabel'
nullable: true
type: array
visualization:
$ref: '#/components/schemas/DashboardtypesAreaChartVisualization'
type: object
DashboardtypesAreaChartVisualization:
properties:
fillSpans:
type: boolean
stack:
$ref: '#/components/schemas/DashboardtypesStackMode'
timePreference:
$ref: '#/components/schemas/DashboardtypesTimePreference'
type: object
DashboardtypesAreaFillMode:
enum:
- solid
- gradient
type: string
DashboardtypesAxes:
properties:
isLogScale:
@@ -3552,6 +3599,11 @@ components:
- gradient
- none
type: string
DashboardtypesFillOpacity:
maximum: 1
minimum: 0
nullable: true
type: number
DashboardtypesGettableDashboardV2:
properties:
createdAt:
@@ -4014,6 +4066,7 @@ components:
DashboardtypesPanelPlugin:
discriminator:
mapping:
signoz/AreaChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
signoz/BarChartPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
signoz/HistogramPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesHistogramPanelSpec'
signoz/ListPanel: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesListPanelSpec'
@@ -4026,6 +4079,7 @@ components:
oneOf:
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpec'
- $ref: '#/components/schemas/DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpec'
@@ -4037,6 +4091,7 @@ components:
enum:
- signoz/TimeSeriesPanel
- signoz/BarChartPanel
- signoz/AreaChartPanel
- signoz/NumberPanel
- signoz/PieChartPanel
- signoz/TablePanel
@@ -4044,6 +4099,18 @@ components:
- signoz/ListPanel
- signoz/TextPanel
type: string
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpec:
properties:
kind:
enum:
- signoz/AreaChartPanel
type: string
spec:
$ref: '#/components/schemas/DashboardtypesAreaChartPanelSpec'
required:
- kind
- spec
type: object
DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpec:
properties:
kind:
@@ -4374,6 +4441,12 @@ components:
are connected.
type: boolean
type: object
DashboardtypesStackMode:
enum:
- none
- normal
- percent
type: string
DashboardtypesStorableDashboardData:
additionalProperties: {}
type: object
@@ -11102,9 +11175,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- cloud-integration:read
- tokenizer:
- VIEWER
- cloud-integration:read
summary: Agent check-in
tags:
- cloudintegration
@@ -11154,9 +11227,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:list
- tokenizer:
- ADMIN
- cloud-integration:list
summary: List accounts
tags:
- cloudintegration
@@ -11211,9 +11284,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:create
- tokenizer:
- ADMIN
- cloud-integration:create
summary: Create account
tags:
- cloudintegration
@@ -11256,9 +11329,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:delete
- tokenizer:
- ADMIN
- cloud-integration:delete
summary: Disconnect account
tags:
- cloudintegration
@@ -11324,9 +11397,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:read
- tokenizer:
- ADMIN
- cloud-integration:read
summary: Get account
tags:
- cloudintegration
@@ -11373,9 +11446,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:update
- tokenizer:
- ADMIN
- cloud-integration:update
summary: Update account
tags:
- cloudintegration
@@ -11431,9 +11504,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:list
- tokenizer:
- ADMIN
- cloud-integration:list
summary: List account services metadata
tags:
- cloudintegration
@@ -11506,9 +11579,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:read
- tokenizer:
- ADMIN
- cloud-integration:read
summary: Get service for account
tags:
- cloudintegration
@@ -11560,9 +11633,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:update
- tokenizer:
- ADMIN
- cloud-integration:update
summary: Update service
tags:
- cloudintegration
@@ -11617,9 +11690,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- cloud-integration:read
- tokenizer:
- VIEWER
- cloud-integration:read
summary: Agent check-in
tags:
- cloudintegration
@@ -11670,9 +11743,17 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- ingestion-key:create
- serviceaccount:create
- factor-api-key:create
- serviceaccount:attach
- role:attach
- tokenizer:
- ADMIN
- ingestion-key:create
- serviceaccount:create
- factor-api-key:create
- serviceaccount:attach
- role:attach
summary: Get connection credentials
tags:
- cloudintegration
@@ -11722,10 +11803,8 @@ paths:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
- api_key: []
- tokenizer: []
summary: List services metadata
tags:
- cloudintegration
@@ -11780,10 +11859,8 @@ paths:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
- api_key: []
- tokenizer: []
summary: Get service
tags:
- cloudintegration

View File

@@ -4,7 +4,6 @@
"GET_STARTED": "SigNoz | Get Started",
"SERVICE_METRICS": "SigNoz | Service Metrics",
"SERVICE_MAP": "SigNoz | Service Map",
"TRACE": "SigNoz | Trace",
"HOME": "SigNoz | Home",
"TRACE_DETAIL": "SigNoz | Trace Detail",
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
@@ -56,7 +55,6 @@
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
"MCP_SERVER": "SigNoz | MCP Server",
"AI_ASSISTANT": "SigNoz | AI Assistant",
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
"ROLE_DETAILS": "SigNoz | Role Details",
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",

View File

@@ -14,7 +14,6 @@
"GET_STARTED_AZURE_MONITORING": "SigNoz | Get Started | AZURE",
"GET_STARTED": "SigNoz | Get Started with SigNoz Cloud",
"GET_STARTED_WITH_CLOUD": "SigNoz | Get Started with SigNoz Cloud",
"TRACE": "SigNoz | Trace",
"TRACE_DETAIL": "SigNoz | Trace Detail",
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
"SETTINGS": "SigNoz | Settings",
@@ -79,7 +78,6 @@
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
"MCP_SERVER": "SigNoz | MCP Server",
"AI_ASSISTANT": "SigNoz | AI Assistant",
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
"ROLE_DETAILS": "SigNoz | Role Details",
"ROLE_CREATE": "SigNoz | Create Role",

View File

@@ -1588,15 +1588,10 @@ describe('PrivateRoute', () => {
deniedRoles: DENIED_ROLES,
},
TRACES_EXPLORER: { path: ROUTES.TRACES_EXPLORER, deniedRoles: DENIED_ROLES },
TRACE: { path: ROUTES.TRACE, deniedRoles: DENIED_ROLES },
TRACE_DETAIL: {
path: ROUTES.TRACE_DETAIL.replace(':id', 'trace-id-1'),
deniedRoles: DENIED_ROLES,
},
TRACE_DETAIL_OLD: {
path: ROUTES.TRACE_DETAIL_OLD.replace(':id', 'trace-id-1'),
deniedRoles: DENIED_ROLES,
},
// LOGS and LOGS_EXPLORER share a path - matchPath resolves it to whichever
// route definition comes last, and both keys are authz-aware either way.
LOGS: { path: ROUTES.LOGS, deniedRoles: DENIED_ROLES },

View File

@@ -53,17 +53,6 @@ export const TracesFunnelDetails = Loadable(
),
);
export const TraceFilter = Loadable(
() => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'),
);
export const TraceDetailOldRedirect = Loadable(
() =>
import(
/* webpackChunkName: "TraceDetailOldRedirect" */ 'pages/TraceDetailOldRedirect/index'
),
);
export const TraceDetailV3 = Loadable(
() =>
import(

View File

@@ -47,9 +47,7 @@ import {
SomethingWentWrong,
StatusPage,
SupportPage,
TraceDetailOldRedirect,
TraceDetailV3,
TraceFilter,
TracesExplorer,
TracesFunnelDetails,
TracesFunnels,
@@ -132,14 +130,6 @@ const routes: AppRoutes[] = [
exact: true,
key: 'LOGS_SAVE_VIEWS',
},
// Legacy /trace-old/:id redirects to the current /trace/:id view.
{
path: ROUTES.TRACE_DETAIL_OLD,
exact: true,
component: TraceDetailOldRedirect,
isPrivate: true,
key: 'TRACE_DETAIL_OLD',
},
{
path: ROUTES.TRACE_DETAIL,
exact: true,
@@ -224,13 +214,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'ALERT_OVERVIEW',
},
{
path: ROUTES.TRACE,
exact: true,
component: TraceFilter,
isPrivate: true,
key: 'TRACE',
},
{
path: ROUTES.TRACES_EXPLORER,
exact: true,

View File

@@ -4144,6 +4144,52 @@ export interface DashboardGridLayoutSpecDTO {
repeatVariable?: string;
}
export enum DashboardtypesAreaFillModeDTO {
solid = 'solid',
gradient = 'gradient',
}
/**
* @minimum 0
* @maximum 1
* @nullable
*/
export type DashboardtypesFillOpacityDTO = number | null;
export enum DashboardtypesLineInterpolationDTO {
linear = 'linear',
spline = 'spline',
step_after = 'step_after',
step_before = 'step_before',
}
export enum DashboardtypesLineStyleDTO {
solid = 'solid',
dashed = 'dashed',
}
export interface DashboardtypesSpanGapsDTO {
/**
* @type string
* @description The maximum gap size to connect when fillOnlyBelow is true. Gaps larger than this duration are left disconnected.
*/
fillLessThan?: string;
/**
* @type boolean
* @description Controls whether lines connect across null values. When false (default), all gaps are connected. When true, only gaps smaller than fillLessThan are connected.
*/
fillOnlyBelow?: boolean;
}
export interface DashboardtypesAreaChartAppearanceDTO {
fillMode?: DashboardtypesAreaFillModeDTO;
fillOpacity?: DashboardtypesFillOpacityDTO | null;
lineInterpolation?: DashboardtypesLineInterpolationDTO;
lineStyle?: DashboardtypesLineStyleDTO;
/**
* @type boolean
*/
showPoints?: boolean;
spanGaps?: DashboardtypesSpanGapsDTO;
}
export interface DashboardtypesAxesDTO {
/**
* @type boolean
@@ -4221,6 +4267,11 @@ export interface DashboardtypesThresholdWithLabelDTO {
value: number;
}
export enum DashboardtypesStackModeDTO {
none = 'none',
normal = 'normal',
percent = 'percent',
}
export enum DashboardtypesTimePreferenceDTO {
global_time = 'global_time',
last_5_min = 'last_5_min',
@@ -4233,6 +4284,27 @@ export enum DashboardtypesTimePreferenceDTO {
last_1_week = 'last_1_week',
last_1_month = 'last_1_month',
}
export interface DashboardtypesAreaChartVisualizationDTO {
/**
* @type boolean
*/
fillSpans?: boolean;
stack?: DashboardtypesStackModeDTO;
timePreference?: DashboardtypesTimePreferenceDTO;
}
export interface DashboardtypesAreaChartPanelSpecDTO {
axes?: DashboardtypesAxesDTO;
chartAppearance?: DashboardtypesAreaChartAppearanceDTO;
formatting?: DashboardtypesPanelFormattingDTO;
legend?: DashboardtypesLegendDTO;
/**
* @type array,null
*/
thresholds?: DashboardtypesThresholdWithLabelDTO[] | null;
visualization?: DashboardtypesAreaChartVisualizationDTO;
}
export interface DashboardtypesBarChartVisualizationDTO {
/**
* @type boolean
@@ -4930,29 +5002,6 @@ export enum DashboardtypesFillModeDTO {
gradient = 'gradient',
none = 'none',
}
export enum DashboardtypesLineInterpolationDTO {
linear = 'linear',
spline = 'spline',
step_after = 'step_after',
step_before = 'step_before',
}
export enum DashboardtypesLineStyleDTO {
solid = 'solid',
dashed = 'dashed',
}
export interface DashboardtypesSpanGapsDTO {
/**
* @type string
* @description The maximum gap size to connect when fillOnlyBelow is true. Gaps larger than this duration are left disconnected.
*/
fillLessThan?: string;
/**
* @type boolean
* @description Controls whether lines connect across null values. When false (default), all gaps are connected. When true, only gaps smaller than fillLessThan are connected.
*/
fillOnlyBelow?: boolean;
}
export interface DashboardtypesTimeSeriesChartAppearanceDTO {
fillMode?: DashboardtypesFillModeDTO;
lineInterpolation?: DashboardtypesLineInterpolationDTO;
@@ -5005,6 +5054,18 @@ export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDa
spec: DashboardtypesBarChartPanelSpecDTO;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTOKind {
'signoz/AreaChartPanel' = 'signoz/AreaChartPanel',
}
export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTO {
/**
* @enum signoz/AreaChartPanel
* @type string
*/
kind: DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTOKind;
spec: DashboardtypesAreaChartPanelSpecDTO;
}
export enum DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpecDTOKind {
'signoz/NumberPanel' = 'signoz/NumberPanel',
}
@@ -5210,6 +5271,7 @@ export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDa
export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpecDTO
@@ -6134,6 +6196,7 @@ export interface DashboardtypesListableDashboardViewDTO {
export enum DashboardtypesPanelPluginKindDTO {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
'signoz/BarChartPanel' = 'signoz/BarChartPanel',
'signoz/AreaChartPanel' = 'signoz/AreaChartPanel',
'signoz/NumberPanel' = 'signoz/NumberPanel',
'signoz/PieChartPanel' = 'signoz/PieChartPanel',
'signoz/TablePanel' = 'signoz/TablePanel',

View File

@@ -1,49 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import omitBy from 'lodash-es/omitBy';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getFilters';
const getFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const duration =
omitBy(props.other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(props.other, (_, key) =>
key.startsWith('duration'),
);
const exclude: string[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const response = await axios.post<PayloadProps>(`/getSpanFilters`, {
start: props.start,
end: props.end,
getFilters: props.getFilters,
...nonDuration,
maxDuration: String((duration.duration || [])[0] || ''),
minDuration: String((duration.duration || [])[1] || ''),
exclude,
spanKind: props.spanKind,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getFilters;

View File

@@ -1,62 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import omitBy from 'lodash-es/omitBy';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getSpans';
const getSpans = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: `${e.Key}.(string)`,
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
}));
const exclude: string[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const other = Object.fromEntries(props.selectedFilter);
const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(other, (_, key) => key.startsWith('duration'));
const response = await axios.post<PayloadProps>(
`/getFilteredSpans/aggregates`,
{
start: String(props.start),
end: String(props.end),
function: props.function,
groupBy: props.groupBy === 'none' ? '' : props.groupBy,
step: props.step,
tags: updatedSelectedTags,
...nonDuration,
maxDuration: String((duration.duration || [])[0] || ''),
minDuration: String((duration.duration || [])[1] || ''),
exclude,
spanKind: props.spanKind,
},
);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getSpans;

View File

@@ -1,65 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import omitBy from 'lodash-es/omitBy';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getSpanAggregate';
import { TraceFilterEnum } from 'types/reducer/trace';
const getSpanAggregate = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const preProps = {
start: String(props.start),
end: String(props.end),
limit: props.limit,
offset: props.offset,
order: props.order,
orderParam: props.orderParam,
};
const exclude: TraceFilterEnum[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: `${e.Key}.(string)`,
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
}));
const other = Object.fromEntries(props.selectedFilter);
const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(other, (_, key) => key.startsWith('duration'));
const response = await axios.post<PayloadProps>(`/getFilteredSpans`, {
...preProps,
tags: updatedSelectedTags,
...nonDuration,
maxDuration: String((duration.duration || [])[0] || ''),
minDuration: String((duration.duration || [])[1] || ''),
exclude,
spanKind: props.spanKind,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getSpanAggregate;

View File

@@ -1,49 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { omitBy } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getTagFilters';
import { TraceFilterEnum } from 'types/reducer/trace';
const getTagFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const duration =
omitBy(props.other, (_, key) => !key.startsWith('duration')) || [];
const exclude: TraceFilterEnum[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const nonDuration = omitBy(props.other, (_, key) =>
key.startsWith('duration'),
);
const response = await axios.post<PayloadProps>(`/getTagFilters`, {
start: String(props.start),
end: String(props.end),
...nonDuration,
maxDuration: String((duration.duration || [])[0] || ''),
minDuration: String((duration.duration || [])[1] || ''),
exclude,
spanKind: props.spanKind,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTagFilters;

View File

@@ -1,31 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getTagValue';
const getTagValue = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadProps>(`/getTagValues`, {
start: props.start.toString(),
end: props.end.toString(),
tagKey: {
Key: props.tagKey.Key,
Type: props.tagKey.Type,
},
spanKind: props.spanKind,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTagValue;

View File

@@ -6,31 +6,27 @@ import {
IQuickFiltersConfig,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { getMs } from 'utils/timeUtils';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { DurationSection } from 'pages/TracesExplorer/Filter/DurationSection';
import {
AllTraceFilterKeys,
AllTraceFilterKeyValue,
HandleRunProps,
traceFilterKeys,
unionTagFilterItems,
} from 'pages/TracesExplorer/Filter/filterUtils';
} from 'constants/traceFilterKeys';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { clearFilterFromQuery } from '../shared/filterQuery';
import { SectionActionButton } from '../shared/SectionActionButton/SectionActionButton';
import { DurationSection } from './DurationSection';
import { FilterType, HandleRunProps, unionTagFilterItems } from './utils';
import './Duration.styles.scss';
export type FilterType = Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>;
export type { FilterType };
function Duration({
filter,

View File

@@ -9,10 +9,12 @@ import {
} from 'react';
import { Input } from 'antd';
import { Slider } from '@signozhq/ui/slider';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { getMs } from 'utils/timeUtils';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { addFilter, FilterType, traceFilterKeys } from './filterUtils';
import { traceFilterKeys } from 'constants/traceFilterKeys';
import { addFilter, FilterType } from './utils';
interface DurationProps {
selectedFilters: FilterType | undefined;

View File

@@ -0,0 +1,101 @@
import { Dispatch, SetStateAction } from 'react';
import { AllTraceFilterKeys } from 'constants/traceFilterKeys';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
export type FilterType = Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>;
export interface HandleRunProps {
resetAll?: boolean;
clearByType?: AllTraceFilterKeys;
}
function convertToStringArr(value: string | string[] | undefined): string[] {
if (value) {
if (typeof value === 'string') {
return [value];
}
return value;
}
return [];
}
export const addFilter = (
filterType: AllTraceFilterKeys,
value: string,
setSelectedFilters: Dispatch<
SetStateAction<
| Record<
AllTraceFilterKeys,
{ values: string[] | string; keys: BaseAutocompleteData }
>
| undefined
>
>,
keys: BaseAutocompleteData,
): void => {
setSelectedFilters((prevFilters) => {
const isDuration = [
'durationNanoMax',
'durationNanoMin',
'durationNano',
].includes(filterType);
// Convert value to string array
const valueArray = convertToStringArr(value);
// If previous filters are undefined, initialize them
if (!prevFilters) {
return {
[filterType]: { values: isDuration ? value : valueArray, keys },
} as unknown as FilterType;
}
// If the filter type doesn't exist, initialize it
if (!prevFilters[filterType]?.values.length) {
return {
...prevFilters,
[filterType]: { values: isDuration ? value : valueArray, keys },
};
}
// If the value already exists, don't add it again
if (convertToStringArr(prevFilters[filterType].values).includes(value)) {
return prevFilters;
}
// Otherwise, add the value to the existing array
return {
...prevFilters,
[filterType]: {
values: isDuration
? value
: [...convertToStringArr(prevFilters[filterType].values), value],
keys,
},
};
});
};
/** Merges two filter lists; later items win on the same key + operator. */
export function unionTagFilterItems(
items1: TagFilterItem[],
items2: TagFilterItem[],
): TagFilterItem[] {
const unionMap = new Map<string, TagFilterItem>();
items1?.forEach((item) => {
const keyOp = `${item?.key?.key}_${item?.op}`;
unionMap.set(keyOp, item);
});
items2?.forEach((item) => {
const keyOp = `${item?.key?.key}_${item?.op}`;
unionMap.set(keyOp, item);
});
return Array.from(unionMap?.values());
}

View File

@@ -41,7 +41,6 @@ export enum LOCALSTORAGE {
DISMISSED_API_KEYS_DEPRECATION_BANNER = 'DISMISSED_API_KEYS_DEPRECATION_BANNER',
TRACE_DETAILS_SPAN_DETAILS_POSITION = 'TRACE_DETAILS_SPAN_DETAILS_POSITION',
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
TRACE_DETAILS_PREFER_OLD_VIEW = 'TRACE_DETAILS_PREFER_OLD_VIEW',
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',

View File

@@ -29,6 +29,7 @@ export const getComponentForPanelType = (
[PANEL_TYPES.LIST]:
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
[PANEL_TYPES.BAR]: Uplot,
[PANEL_TYPES.AREA]: Uplot,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.HISTOGRAM]: Uplot,
// Dashboards v2 renders this kind; nothing reaches the V1 chart map for it.

View File

@@ -336,6 +336,7 @@ export enum PANEL_TYPES {
LIST = 'list',
TRACE = 'trace',
BAR = 'bar',
AREA = 'area',
PIE = 'pie',
HISTOGRAM = 'histogram',
TEXT = 'text',

View File

@@ -1,4 +1,4 @@
import { OperatorValues } from 'types/reducer/trace';
import { OperatorValues } from 'hooks/useResourceAttribute/types';
export const OperatorConversions: Array<{
label: string;

View File

@@ -6,9 +6,8 @@ const ROUTES = {
SERVICE_METRICS: '/services/:servicename',
SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations',
SERVICE_MAP: '/service-map',
TRACE: '/trace',
TRACE_BASE: '/trace',
TRACE_DETAIL: '/trace/:id',
TRACE_DETAIL_OLD: '/trace-old/:id',
TRACES_EXPLORER: '/traces-explorer',
ONBOARDING: '/onboarding',
GET_STARTED_WITH_CLOUD: '/get-started-with-signoz-cloud',

View File

@@ -0,0 +1,124 @@
import { SPAN_ATTRIBUTES } from 'container/ApiMonitoring/Explorer/Domains/DomainDetails/constants';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
/** Trace attribute key -> label shown in quick filters, the options menu and APM "view traces" links. */
export const AllTraceFilterKeyValue: Record<string, string> = {
durationNanoMin: 'Duration',
durationNano: 'Duration',
duration_nano: 'Duration',
durationNanoMax: 'Duration',
'deployment.environment': 'Environment',
hasError: 'Status',
has_error: 'Status',
serviceName: 'Service Name',
'service.name': 'service.name',
name: 'Operation / Name',
rpcMethod: 'RPC Method',
'rpc.method': 'RPC Method',
responseStatusCode: 'Status Code',
response_status_code: 'Status Code',
httpHost: 'HTTP Host',
http_host: 'HTTP Host',
httpMethod: 'HTTP Method',
http_method: 'HTTP Method',
httpRoute: 'HTTP Route',
'http.route': 'HTTP Route',
httpUrl: 'HTTP URL',
[SPAN_ATTRIBUTES.HTTP_URL]: 'HTTP URL',
traceID: 'Trace ID',
trace_id: 'Trace ID',
} as const;
export type AllTraceFilterKeys = keyof typeof AllTraceFilterKeyValue;
export const traceFilterKeys: Record<AllTraceFilterKeys, BaseAutocompleteData> =
{
durationNano: {
key: 'durationNano',
dataType: DataTypes.Float64,
type: 'tag',
id: 'durationNano--float64--tag--true',
},
hasError: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
id: 'hasError--bool--tag--true',
},
serviceName: {
key: 'serviceName',
dataType: DataTypes.String,
type: 'tag',
id: 'serviceName--string--tag--true',
},
'deployment.environment': {
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'resource',
id: 'deployment.environment--string--resource--false',
},
name: {
key: 'name',
dataType: DataTypes.String,
type: 'tag',
id: 'name--string--tag--true',
},
rpcMethod: {
key: 'rpcMethod',
dataType: DataTypes.String,
type: 'tag',
id: 'rpcMethod--string--tag--true',
},
responseStatusCode: {
key: 'responseStatusCode',
dataType: DataTypes.String,
type: 'tag',
id: 'responseStatusCode--string--tag--true',
},
httpHost: {
key: 'httpHost',
dataType: DataTypes.String,
type: 'tag',
id: 'httpHost--string--tag--true',
},
httpMethod: {
key: 'httpMethod',
dataType: DataTypes.String,
type: 'tag',
id: 'httpMethod--string--tag--true',
},
httpRoute: {
key: 'httpRoute',
dataType: DataTypes.String,
type: 'tag',
id: 'httpRoute--string--tag--true',
},
httpUrl: {
key: 'httpUrl',
dataType: DataTypes.String,
type: 'tag',
id: 'httpUrl--string--tag--true',
},
traceID: {
key: 'traceID',
dataType: DataTypes.String,
type: 'tag',
id: 'traceID--string--tag--true',
},
durationNanoMin: {
key: 'durationNanoMin',
dataType: DataTypes.Float64,
type: 'tag',
id: 'durationNanoMin--float64--tag--true',
},
durationNanoMax: {
key: 'durationNanoMax',
dataType: DataTypes.Float64,
type: 'tag',
id: 'durationNanoMax--float64--tag--true',
},
} as const;

View File

@@ -1,16 +0,0 @@
.span-container {
.spanDetails {
position: absolute;
height: 50px;
padding: 8px;
min-width: 150px;
background: lightcyan;
color: black;
bottom: 24px;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -1,96 +0,0 @@
import { useEffect } from 'react';
import { Popover } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTimezone } from 'providers/Timezone';
import { toFixed } from 'utils/toFixed';
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
import '../GantChart.styles.scss';
interface SpanLengthProps {
globalStart: number;
startTime: number;
name: string;
width: string;
leftOffset: string;
bgColor: string;
inMsCount: number;
}
function Span(props: SpanLengthProps): JSX.Element {
const { width, leftOffset, bgColor, inMsCount, startTime, name, globalStart } =
props;
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
const { timezone } = useTimezone();
useEffect(() => {
document.documentElement.scrollTop = document.documentElement.clientHeight;
document.documentElement.scrollLeft = document.documentElement.clientWidth;
}, []);
const getContent = (): JSX.Element => {
const timeStamp = dayjs(startTime)
.tz(timezone.value)
.format(DATE_TIME_FORMATS.TIME_UTC_MS);
const startTimeInMs = startTime - globalStart;
return (
<div>
<Typography.Text style={{ marginBottom: '8px' }}>
{' '}
Duration : {inMsCount}
</Typography.Text>
<br />
<Typography.Text style={{ marginBottom: '8px' }}>
Start Time: {startTimeInMs}ms [{timeStamp}]{' '}
</Typography.Text>
</div>
);
};
return (
<SpanWrapper className="span-container">
<SpanLine
className="spanLine"
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<div>
<Popover
style={{
left: `${leftOffset}%`,
}}
title={name}
content={getContent()}
trigger="hover"
placement="left"
autoAdjustOverflow
>
<SpanBorder
className="spanTrack"
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
</Popover>
</div>
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
time,
2,
)} ${timeUnitName}`}</SpanText>
</SpanWrapper>
);
}
export default Span;

View File

@@ -1,52 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
interface Props {
width: string;
leftOffset: string;
bgColor: string;
isDarkMode: boolean;
}
export const SpanLine = styled.div<Props>`
width: ${({ leftOffset }): string => `${leftOffset}%`};
height: 0px;
border-bottom: 0.1px solid
${({ isDarkMode }): string => (isDarkMode ? '#303030' : '#c0c0c0')};
top: 50%;
position: absolute;
`;
export const SpanBorder = styled.div<Props>`
background: ${({ bgColor }): string => bgColor};
border-radius: 5px;
height: 0.625rem;
width: ${({ width }): string => `${width}%`};
left: ${({ leftOffset }): string => `${leftOffset}%`};
top: 35%;
position: absolute;
`;
export const SpanWrapper = styled.div`
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
position: relative;
z-index: 2;
min-height: 2rem;
`;
interface SpanTextProps extends Pick<Props, 'leftOffset'> {
isDarkMode: boolean;
}
export const SpanText = styled(Typography.Text)<SpanTextProps>`
&&& {
left: ${({ leftOffset }): string => `${leftOffset}%`};
top: 65%;
position: absolute;
width: max-content;
color: ${({ isDarkMode }): string => (isDarkMode ? '#ACACAC' : '#666')};
font-size: 0.75rem;
}
`;

View File

@@ -1,22 +0,0 @@
import { Container, Service, Span, SpanWrapper } from './styles';
function SpanNameComponent({
name,
serviceName,
}: SpanNameComponentProps): JSX.Element {
return (
<Container title={`${name} ${serviceName}`}>
<SpanWrapper>
<Span truncate={1}>{name}</Span>
<Service truncate={1}>{serviceName}</Service>
</SpanWrapper>
</Container>
);
}
interface SpanNameComponentProps {
name: string;
serviceName: string;
}
export default SpanNameComponent;

View File

@@ -1,41 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const Span = styled(Typography.Text)`
&&& {
font-size: 0.75rem;
margin: 0;
/* border-bottom: 1px solid grey; */
}
`;
export const Service = styled(Typography.Text)`
&&& {
color: #acacac;
font-size: 0.75rem;
}
`;
export const SpanWrapper = styled.div`
display: flex;
flex-direction: column;
margin-left: 0.625rem;
width: 10rem;
`;
export const SpanConnector = styled.div`
width: 37px;
border: 1px solid #303030;
height: 0;
`;
export const Container = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
`;
export const SpanName = styled.div`
width: fit-content;
border-bottom: 1px solid black;
`;

View File

@@ -1,233 +0,0 @@
import {
Dispatch,
MouseEventHandler,
SetStateAction,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import { Col } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { StyledCol, StyledRow } from 'components/Styled';
import {
IIntervalUnit,
SPAN_DETAILS_LEFT_COL_WIDTH,
} from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { ITraceMetaData } from '..';
import Span from '../Span';
import SpanName from '../SpanName';
import { getMetaDataFromSpanTree, getTopLeftFromBody } from '../utils';
import {
CardComponent,
CardContainer,
CaretContainer,
HoverCard,
styles,
Wrapper,
} from './styles';
import { getIconStyles } from './utils';
function Trace(props: TraceProps): JSX.Element {
const {
name,
activeHoverId,
setActiveHoverId,
globalSpread,
globalStart,
serviceName,
startTime,
value,
serviceColour,
id,
setActiveSelectedId,
activeSelectedId,
level,
activeSpanPath,
isExpandAll,
intervalUnit,
children,
isMissing,
} = props;
const isDarkMode = useIsDarkMode();
const [isOpen, setOpen] = useState<boolean>(activeSpanPath[level] === id);
const localTreeExpandInteraction = useRef<boolean | 0>(0); // Boolean is for the state of the expansion whereas the number i.e. 0 is for skipping the user interaction.
useEffect(() => {
if (localTreeExpandInteraction.current !== 0) {
setOpen(localTreeExpandInteraction.current);
localTreeExpandInteraction.current = 0;
} else if (!isOpen) {
setOpen(activeSpanPath[level] === id);
}
}, [activeSpanPath, isOpen, id, level]);
useEffect(() => {
if (isExpandAll) {
setOpen(isExpandAll);
} else {
setOpen(activeSpanPath[level] === id);
}
}, [isExpandAll, activeSpanPath, id, level]);
const isOnlyChild = children.length === 1;
const [top, setTop] = useState<number>(0);
const ref = useRef<HTMLUListElement>(null);
useEffect(() => {
if (activeSelectedId === id) {
ref.current?.scrollIntoView({
block: 'nearest',
behavior: 'auto',
inline: 'nearest',
});
}
}, [activeSelectedId, id]);
const onMouseEnterHandler = (): void => {
setActiveHoverId(id);
if (ref.current) {
const { top } = getTopLeftFromBody(ref.current);
setTop(top);
}
};
const onMouseLeaveHandler = (): void => {
setActiveHoverId('');
};
const onClick = (): void => {
setActiveSelectedId(id);
};
const onClickTreeExpansion: MouseEventHandler<HTMLDivElement> = (
event,
): void => {
event.stopPropagation();
setOpen((state) => {
localTreeExpandInteraction.current = !isOpen;
return !state;
});
};
const { totalSpans } = getMetaDataFromSpanTree(props);
const inMsCount = value;
const nodeLeftOffset = ((startTime - globalStart) * 1e2) / globalSpread;
const width = (value * 1e2) / (globalSpread * 1e6);
const panelWidth = SPAN_DETAILS_LEFT_COL_WIDTH - level * (16 + 1) - 48;
const iconStyles = useMemo(() => getIconStyles(), []);
const icon = useMemo(
() =>
isOpen ? (
<ChevronDown size="md" style={iconStyles} />
) : (
<ChevronRight size="md" style={iconStyles} />
),
[isOpen, iconStyles],
);
return (
<Wrapper
onMouseEnter={onMouseEnterHandler}
onMouseLeave={onMouseLeaveHandler}
isOnlyChild={isOnlyChild}
ref={ref}
isDarkMode={isDarkMode}
>
<HoverCard
top={top}
isHovered={activeHoverId === id}
isSelected={activeSelectedId === id}
isDarkMode={isDarkMode}
/>
<CardContainer isMissing={isMissing} onClick={onClick}>
<StyledCol flex={`${panelWidth}px`} styledclass={[styles.overFlowHidden]}>
<StyledRow styledclass={[styles.flexNoWrap]}>
<Col>
{totalSpans !== 1 && (
<CardComponent
isOnlyChild={isOnlyChild}
isDarkMode={isDarkMode}
onClick={onClickTreeExpansion}
>
<Typography style={{ wordBreak: 'normal' }}>{totalSpans}</Typography>
<CaretContainer>{icon}</CaretContainer>
</CardComponent>
)}
</Col>
<Col>
<SpanName name={name} serviceName={serviceName} />
</Col>
</StyledRow>
</StyledCol>
<Col flex="1">
<Span
globalStart={globalStart}
startTime={startTime}
name={name}
leftOffset={nodeLeftOffset.toString()}
width={width.toString()}
bgColor={serviceColour}
inMsCount={inMsCount / 1e6}
/>
</Col>
</CardContainer>
{isOpen && (
<>
{children.map((child) => (
<Trace
key={child.id}
activeHoverId={activeHoverId}
setActiveHoverId={setActiveHoverId}
{...child}
globalSpread={globalSpread}
globalStart={globalStart}
setActiveSelectedId={setActiveSelectedId}
activeSelectedId={activeSelectedId}
level={level + 1}
activeSpanPath={activeSpanPath}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
isMissing={child.isMissing}
/>
))}
</>
)}
</Wrapper>
);
}
Trace.defaultProps = {
isMissing: false,
};
interface ITraceGlobal {
globalSpread: ITraceMetaData['spread'];
globalStart: ITraceMetaData['globalStart'];
}
interface TraceProps extends ITraceTree, ITraceGlobal {
activeHoverId: string;
setActiveHoverId: Dispatch<SetStateAction<string>>;
setActiveSelectedId: Dispatch<SetStateAction<string>>;
activeSelectedId: string;
level: number;
activeSpanPath: string[];
isExpandAll: boolean;
intervalUnit: IIntervalUnit;
isMissing?: boolean;
}
export default Trace;

View File

@@ -1,113 +0,0 @@
import { volcano } from '@ant-design/colors';
import styled, {
css,
DefaultTheme,
ThemedCssFunction,
} from 'styled-components';
interface Props {
isOnlyChild: boolean;
}
export const Wrapper = styled.ul<Props>`
display: flex;
flex-direction: column;
padding-bottom: 0.5rem;
padding-top: 0.5rem;
position: relative;
z-index: 1;
ul {
border-left: ${({ isOnlyChild }): StyledCSS =>
isOnlyChild && 'none'} !important;
${({ isOnlyChild }): StyledCSS =>
isOnlyChild &&
css`
&:before {
border-left: 1px solid #434343;
display: inline-block;
content: '';
height: 54px;
position: absolute;
left: 0;
top: -35px;
}
`}
}
`;
export const CardContainer = styled.li<{ isMissing?: boolean }>`
display: flex;
width: 100%;
cursor: pointer;
border-radius: 0.25rem;
z-index: 2;
${({ isMissing }): string =>
isMissing ? `border: 1px dashed ${volcano[6]} !important;` : ''}
`;
interface Props {
isDarkMode: boolean;
}
export type StyledCSS =
| ReturnType<ThemedCssFunction<DefaultTheme>>
| string
| false
| undefined;
export const CardComponent = styled.div<Props>`
border: 1px solid
${({ isDarkMode }): StyledCSS => (isDarkMode ? '#434343' : '#333')};
box-sizing: border-box;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
padding: 1px 8px;
background: ${({ isDarkMode }): StyledCSS =>
isDarkMode ? '#1d1d1d' : '#ddd'};
height: 22px;
`;
export const CaretContainer = styled.span`
margin-left: 0.304rem;
`;
interface HoverCardProps {
isHovered: boolean;
isSelected: boolean;
top: number;
isDarkMode: boolean;
}
export const HoverCard = styled.div<HoverCardProps>`
display: ${({ isSelected, isHovered }): string =>
isSelected || isHovered ? 'block' : 'none'};
width: 200%;
background-color: ${({ isHovered, isDarkMode }): string => {
if (isHovered) {
return isDarkMode ? '#262626' : '#ddd';
}
return isDarkMode ? '#4f4f4f' : '#bbb';
}};
position: absolute;
top: 0;
left: -100%;
right: 0;
height: 3rem;
opacity: 0.5;
`;
const flexNoWrap = css`
flex-wrap: nowrap;
`;
const overFlowHidden = css`
overflow: hidden;
`;
export const styles = {
flexNoWrap,
overFlowHidden,
};

View File

@@ -1,3 +0,0 @@
export const getIconStyles = (): Record<string, string> => ({
color: 'var(--l1-foreground)',
});

View File

@@ -1,91 +0,0 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { SquareMinus, SquarePlus } from '@signozhq/icons';
import { IIntervalUnit } from 'container/TraceDetail/utils';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { CardContainer, CardWrapper, CollapseButton } from './styles';
import Trace from './Trace';
import { getSpanPath } from './utils';
function GanttChart(props: GanttChartProps): JSX.Element {
const {
data,
traceMetaData,
activeHoverId,
setActiveHoverId,
activeSelectedId,
setActiveSelectedId,
spanId,
intervalUnit,
} = props;
const { globalStart, spread: globalSpread } = traceMetaData;
const [isExpandAll, setIsExpandAll] = useState<boolean>(false);
const [activeSpanPath, setActiveSpanPath] = useState<string[]>([]);
useEffect(() => {
setActiveSpanPath(getSpanPath(data, spanId));
}, [spanId, data]);
useEffect(() => {
setActiveSpanPath(getSpanPath(data, activeSelectedId));
}, [activeSelectedId, data]);
const handleCollapse = (): void => {
setIsExpandAll((prev) => !prev);
};
return (
<CardContainer>
<CollapseButton
onClick={handleCollapse}
title={isExpandAll ? 'Collapse All' : 'Expand All'}
>
{isExpandAll ? (
<SquareMinus size={16} style={{ color: 'var(--accent-primary)' }} />
) : (
<SquarePlus size={16} style={{ color: 'var(--accent-primary)' }} />
)}
</CollapseButton>
<CardWrapper>
<Trace
activeHoverId={activeHoverId}
activeSpanPath={activeSpanPath}
setActiveHoverId={setActiveHoverId}
key={data.id}
{...{
...data,
globalSpread,
globalStart,
setActiveSelectedId,
activeSelectedId,
}}
level={0}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
/>
</CardWrapper>
</CardContainer>
);
}
export interface ITraceMetaData {
globalEnd: number;
globalStart: number;
levels: number;
spread: number;
totalSpans: number;
}
export interface GanttChartProps {
data: ITraceTree;
traceMetaData: ITraceMetaData;
activeSelectedId: string;
activeHoverId: string;
setActiveHoverId: Dispatch<SetStateAction<string>>;
setActiveSelectedId: Dispatch<SetStateAction<string>>;
spanId: string;
intervalUnit: IIntervalUnit;
}
export default GanttChart;

View File

@@ -1,48 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.ul`
padding-left: 0;
position: absolute;
width: 100%;
height: 100%;
ul {
list-style: none;
border-left: 1px solid #434343;
padding-left: 1rem;
width: 100%;
margin: 0px;
}
ul li {
position: relative;
&:before {
position: absolute;
left: -1rem;
top: 10px;
content: '';
height: 1px;
width: 1rem;
background-color: #434343;
}
}
`;
export const CardWrapper = styled.div`
display: flex;
width: 100%;
margin-left: 1rem;
margin-top: 1.5rem;
`;
export const CardContainer = styled.li`
display: flex;
width: 100%;
position: relative;
`;
export const CollapseButton = styled.div`
position: absolute;
top: 0;
`;

View File

@@ -1,203 +0,0 @@
import { set } from 'lodash-es';
import { ITraceForest, ITraceTree } from 'types/api/trace/getTraceItem';
interface GetTraceMetaData {
globalStart: number;
globalEnd: number;
spread: number;
totalSpans: number;
levels: number;
}
export const getMetaDataFromSpanTree = (
treeData: ITraceTree,
): GetTraceMetaData => {
let globalStart = Number.POSITIVE_INFINITY;
let globalEnd = Number.NEGATIVE_INFINITY;
let totalSpans = 0;
let levels = 1;
const traverse = (treeNode: ITraceTree, level = 0): void => {
if (!treeNode) {
return;
}
totalSpans += 1;
levels = Math.max(levels, level);
const { startTime } = treeNode;
const endTime = startTime + treeNode.value;
globalStart = Math.min(globalStart, startTime);
globalEnd = Math.max(globalEnd, endTime);
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1);
});
};
traverse(treeData, 1);
globalStart *= 1e6;
globalEnd *= 1e6;
return {
globalStart,
globalEnd,
spread: globalEnd - globalStart,
totalSpans,
levels,
};
};
export function getTopLeftFromBody(elem: HTMLElement): {
top: number;
left: number;
} {
const box = elem.getBoundingClientRect();
const { body } = document;
const docEl = document.documentElement;
const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
const clientTop = docEl.clientTop || body.clientTop || 0;
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
const top = box.top + scrollTop - clientTop;
const left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}
export const getNodeById = (
searchingId: string,
treesData: ITraceForest | undefined,
): ITraceForest => {
const newtreeData: ITraceForest = {} as ITraceForest;
const traverse = (
treeNode: ITraceTree,
setCallBack: (arg0: ITraceTree) => void,
level = 0,
): void => {
if (!treeNode) {
return;
}
if (searchingId === treeNode.id) {
setCallBack(treeNode);
}
treeNode.children.forEach((childNode) => {
traverse(childNode, setCallBack, level + 1);
});
};
const spanTreeSetCallback = (
path: (keyof ITraceForest)[],
value: ITraceTree,
): ITraceForest => set(newtreeData, path, [value]);
if (treesData?.spanTree) {
treesData.spanTree.forEach((tree) => {
traverse(tree, (value) => spanTreeSetCallback(['spanTree'], value), 1);
});
}
if (treesData?.missingSpanTree) {
treesData.missingSpanTree.forEach((tree) => {
traverse(
tree,
(value) => spanTreeSetCallback(['missingSpanTree'], value),
1,
);
});
}
return newtreeData;
};
const getSpanWithoutChildren = (
span: ITraceTree,
): Omit<ITraceTree, 'children'> => ({
id: span.id,
name: span.name,
parent: span.parent,
serviceColour: span.serviceColour,
serviceName: span.serviceName,
startTime: span.startTime,
tags: span.tags,
time: span.time,
value: span.value,
event: span.event,
hasError: span.hasError,
spanKind: span.spanKind,
statusCodeString: span.statusCodeString,
statusMessage: span.statusMessage,
});
export const isSpanPresentInSearchString = (
searchedString: string,
tree: ITraceTree,
): boolean => {
const parsedTree = getSpanWithoutChildren(tree);
const stringifyTree = JSON.stringify(parsedTree);
return stringifyTree.includes(searchedString);
};
export const isSpanPresent = (
tree: ITraceTree,
searchedKey: string,
): ITraceTree[] => {
const foundNode: ITraceTree[] = [];
const traverse = (
treeNode: ITraceTree,
level = 0,
foundNode: ITraceTree[],
): void => {
if (!treeNode) {
return;
}
const isPresent = isSpanPresentInSearchString(searchedKey, treeNode);
if (isPresent) {
foundNode.push(treeNode);
}
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1, foundNode);
});
};
traverse(tree, 1, foundNode);
return foundNode;
};
export const getSpanPath = (tree: ITraceTree, spanId: string): string[] => {
const spanPath: string[] = [];
const traverse = (treeNode: ITraceTree): boolean => {
if (!treeNode) {
return false;
}
spanPath.push(treeNode.id);
if (spanId === treeNode.id) {
return true;
}
let foundInChild = false;
treeNode.children.forEach((childNode) => {
if (traverse(childNode)) {
foundInChild = true;
}
});
if (!foundInChild) {
spanPath.pop();
}
return foundInChild;
};
traverse(tree);
return spanPath;
};

View File

@@ -28,7 +28,7 @@ import {
} from 'types/api/licensesV3/getActive';
import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';

View File

@@ -82,7 +82,7 @@ export function getHostMetricsQueryPayload(
start: number,
end: number,
): ReturnType<typeof getHostQueryPayload> {
return getHostQueryPayload(host.hostName, start, end, true);
return getHostQueryPayload(host.hostName, start, end);
}
export { hostWidgetInfo };

View File

@@ -3,7 +3,7 @@ import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import { TextNoData } from '../../components/TextNoData';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { getMs } from 'utils/timeUtils';
import {
BlockLink,
getTraceLink,

View File

@@ -63,6 +63,26 @@ const EDITED_SPAN_JSON = `{
}
}`;
const SPAN_WITH_EXTRA_KEY_JSON = `{
"attributes": {
"input.value": "What is quantum computing?"
},
"resource": {
"service.name": "llm-gateway"
},
"demo": {
"name": "demo"
}
}`;
const EXTRA_KEY_RESULT_SPAN = {
attributes: {
'input.value': 'What is quantum computing?',
[MAPPED_ATTRIBUTE_KEY]: 'What is quantum computing?',
},
resource: { 'service.name': 'llm-gateway' },
};
const SPAN_INPUT_KEY = LOCALSTORAGE.LLM_ATTRIBUTE_MAPPING_TEST_SPAN;
describe('TestTab — sample-span flow', () => {
@@ -104,6 +124,47 @@ describe('TestTab — sample-span flow', () => {
expect(screen.queryByTestId('test-error')).not.toBeInTheDocument();
});
it('trims extra top-level keys and sends only the envelope', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
let body: { spans?: { attributes?: Record<string, unknown> }[] } | undefined;
server.use(
rest.post(TEST_ENDPOINT, async (req, res, ctx) => {
body = await req.json();
return res(
ctx.status(200),
ctx.json(makeTestResponse([EXTRA_KEY_RESULT_SPAN])),
);
}),
);
render(<LLMObservabilityAttributeMapping />);
await user.click(screen.getByRole('tab', { name: 'Test' }));
const runBtn = await screen.findByTestId('run-test-button');
await user.clear(screen.getByTestId('monaco'));
await user.paste(SPAN_WITH_EXTRA_KEY_JSON);
await waitFor(() =>
expect(screen.getByTestId('monaco')).toHaveValue(SPAN_WITH_EXTRA_KEY_JSON),
);
expect(screen.queryByTestId('test-input-error')).not.toBeInTheDocument();
await user.click(runBtn);
await expect(
screen.findByTestId('test-results'),
).resolves.toBeInTheDocument();
expect(body?.spans?.[0]?.attributes).toStrictEqual({
'input.value': 'What is quantum computing?',
});
expect(screen.getByTestId('test-result-0-attributes')).toHaveTextContent(
MAPPED_ATTRIBUTE_KEY,
);
expect(screen.getByTestId('test-result-0-resource')).toBeInTheDocument();
expect(screen.getByText('populated')).toBeInTheDocument();
});
it('surfaces a backend error and renders no results', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(

View File

@@ -0,0 +1,51 @@
import { parseSpanInput } from '../testPayload';
describe('parseSpanInput', () => {
it('reads the envelope and trims extra top-level keys', () => {
const span = parseSpanInput(`{
"attributes": { "llm.model_name": "gpt-4o" },
"resource": { "service.name": "llm-gateway" },
"demo": { "name": "demo" }
}`);
expect(span.attributes).toStrictEqual({ 'llm.model_name': 'gpt-4o' });
expect(span.resource).toStrictEqual({ 'service.name': 'llm-gateway' });
});
it('reads a clean envelope', () => {
const span = parseSpanInput(`{
"attributes": { "llm.model_name": "gpt-4o" },
"resource": { "service.name": "llm-gateway" }
}`);
expect(span.attributes).toStrictEqual({ 'llm.model_name': 'gpt-4o' });
expect(span.resource).toStrictEqual({ 'service.name': 'llm-gateway' });
});
it('treats an envelope-less object as a bare attribute map', () => {
const span = parseSpanInput('{ "llm.model_name": "gpt-4o", "demo": "x" }');
expect(span.attributes).toStrictEqual({
'llm.model_name': 'gpt-4o',
demo: 'x',
});
expect(span.resource).toStrictEqual({});
});
it('drops an envelope key that is not an object', () => {
const span = parseSpanInput(
'{ "attributes": { "llm.provider": "openai" }, "resource": "oops" }',
);
expect(span.attributes).toStrictEqual({ 'llm.provider': 'openai' });
expect(span.resource).toStrictEqual({});
});
it.each([
[' ', 'Paste a JSON span object to run the test.'],
['{ "a": }', 'Invalid JSON — check for trailing commas or missing quotes.'],
['[1, 2]', 'Span must be a JSON object of attribute key-value pairs.'],
])('rejects %p', (input, message) => {
expect(() => parseSpanInput(input)).toThrow(message);
});
});

View File

@@ -51,13 +51,9 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
// Any other top-level key (a real span carries name, spanId, kind...) is trimmed.
function isSpanEnvelope(parsed: Record<string, unknown>): boolean {
const keys = Object.keys(parsed);
return (
keys.length > 0 &&
keys.every((key) => key === 'attributes' || key === 'resource') &&
(isPlainObject(parsed.attributes) || isPlainObject(parsed.resource))
);
return isPlainObject(parsed.attributes) || isPlainObject(parsed.resource);
}
export function parseSpanInput(input: string): SpantypesSpanMapperTestSpanDTO {

View File

@@ -1,6 +1,7 @@
import type { TracesTableRow } from '../TracesTable/getFieldColumn';
import { generatePath } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { formUrlParams } from 'container/TraceDetail/utils';
import { formUrlParams } from 'utils/traceUtils';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
export const getTraceLink = (record: Record<string, unknown>): string => {
@@ -14,7 +15,11 @@ export const getTraceLink = (record: Record<string, unknown>): string => {
const traceId = readId(record.traceID) || readId(record.trace_id);
const spanId = readId(record.spanID) || readId(record.span_id);
return `${ROUTES.TRACE}/${traceId}${formUrlParams({
if (!traceId) {
return '';
}
return `${generatePath(ROUTES.TRACE_DETAIL, { id: traceId })}${formUrlParams({
spanId,
levelUp: 0,
levelDown: 0,

View File

@@ -3,7 +3,7 @@ import { Badge } from '@signozhq/ui/badge';
import TanStackTable from 'components/TanStackTableView';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { getMs } from 'utils/timeUtils';
import { useTimezone } from 'providers/Timezone';
import {

View File

@@ -10,3 +10,11 @@
display: none;
}
}
.errorState {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-2);
padding: var(--spacing-8);
}

View File

@@ -1,19 +1,41 @@
import { Typography } from '@signozhq/ui/typography';
import Spinner from 'components/Spinner';
import DashboardContainer from 'pages/DashboardPage/DashboardContainer';
import { useSeededDashboardV2 } from './hooks/useSeededDashboardV2';
import { useSystemDashboard } from './hooks/useSystemDashboard';
import styles from './Overview.module.scss';
function Overview(): JSX.Element {
//TODO: this is a temporary solution to get the seeded dashboard. We should fetch this json from the backend.
const { dashboard, refetch } = useSeededDashboardV2();
const { dashboard, isLoading, isError, error, refetch } = useSystemDashboard();
return (
<div className={styles.overview} data-testid="llm-observability-overview">
const renderContent = (): JSX.Element => {
if (isLoading) {
return <Spinner tip="Loading dashboard..." />;
}
if (isError || !dashboard) {
return (
<div className={styles.errorState}>
<Typography.Title>Failed to load dashboard</Typography.Title>
<Typography.Text>
{error?.response?.data?.error?.message ?? error?.message}
</Typography.Text>
</div>
);
}
return (
<DashboardContainer
dashboard={dashboard}
refetch={refetch}
canEditDashboardOverride={false}
/>
);
};
return (
<div className={styles.overview} data-testid="llm-observability-overview">
{renderContent()}
</div>
);
}

View File

@@ -1,33 +0,0 @@
import { useQueryClient } from 'react-query';
import { getGetDashboardV2QueryKey } from 'api/generated/services/dashboard';
import type {
DashboardtypesGettableDashboardV2DTO,
GetDashboardV2200,
} from 'api/generated/services/sigNoz.schemas';
import dashboardV2Json from '../json/dashboard.json';
const dashboard =
dashboardV2Json as unknown as DashboardtypesGettableDashboardV2DTO;
export interface UseSeededDashboardV2Result {
dashboard: DashboardtypesGettableDashboardV2DTO;
refetch: () => void;
}
const noop = (): void => {};
export function useSeededDashboardV2(): UseSeededDashboardV2Result {
const queryClient = useQueryClient();
const key = getGetDashboardV2QueryKey({ id: dashboard.id });
if (queryClient.getQueryData<GetDashboardV2200>(key) === undefined) {
queryClient.setQueryData<GetDashboardV2200>(key, {
data: dashboard,
status: 'success',
});
}
return { dashboard, refetch: noop };
}

View File

@@ -0,0 +1,56 @@
import { useCallback, useMemo } from 'react';
import { useQueryClient } from 'react-query';
import {
getGetDashboardV2QueryKey,
useGetSystemDashboard,
} from 'api/generated/services/dashboard';
import type { GetSystemDashboardQueryError } from 'api/generated/services/dashboard';
import type {
DashboardtypesGettableDashboardV2DTO,
GetDashboardV2200,
} from 'api/generated/services/sigNoz.schemas';
const SYSTEM_DASHBOARD_NAME = 'ai-o11y-overview';
const DASHBOARD_ID = 'llm-observability-overview';
export interface UseSystemDashboardResult {
dashboard: DashboardtypesGettableDashboardV2DTO | undefined;
isLoading: boolean;
isError: boolean;
error: GetSystemDashboardQueryError | null;
refetch: () => void;
}
export function useSystemDashboard(): UseSystemDashboardResult {
const queryClient = useQueryClient();
const { data, isLoading, isError, error, refetch } = useGetSystemDashboard(
{ name: SYSTEM_DASHBOARD_NAME },
{ query: { staleTime: Infinity, refetchOnMount: false } },
);
const dashboard = useMemo(
() =>
data
? ({
...data.data,
id: DASHBOARD_ID,
} as DashboardtypesGettableDashboardV2DTO)
: undefined,
[data],
);
if (dashboard) {
queryClient.setQueryData<GetDashboardV2200>(
getGetDashboardV2QueryKey({ id: DASHBOARD_ID }),
{ data: dashboard, status: 'success' },
);
}
const refetchDashboard = useCallback((): void => {
void refetch();
}, [refetch]);
return { dashboard, isLoading, isError, error, refetch: refetchDashboard };
}

View File

@@ -24,6 +24,35 @@ jest.mock('container/LLMObservability/Explorer/Explorer', () => ({
default: (): JSX.Element => <div data-testid="llm-observability-explorer" />,
}));
const SYSTEM_DASHBOARD_ENDPOINT = '*/api/v2/dashboards/system/ai-o11y-overview';
function setupSystemDashboard(): void {
server.use(
rest.get(SYSTEM_DASHBOARD_ENDPOINT, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
orgId: 'org',
locked: true,
name: 'signoz---ai-o11y-overview',
schemaVersion: 'v6',
source: 'system',
tags: null,
spec: {
display: { name: 'AI Observability Overview' },
variables: [],
panels: {},
layouts: [],
},
},
}),
),
),
);
}
function setupList(items = mockRules): void {
server.use(
rest.get(LLM_PRICING_ENDPOINT, (_req, res, ctx) =>
@@ -41,14 +70,17 @@ describe('LLMObservability (integration)', () => {
server.resetHandlers();
});
it('renders the overview panel and the tab bar on the overview route', () => {
it('renders the overview panel and the tab bar on the overview route', async () => {
setupSystemDashboard();
render(<LLMObservability />, undefined, {
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
});
expect(screen.getByTestId('llm-observability-tabs')).toBeInTheDocument();
expect(screen.getByTestId('llm-observability-overview')).toBeInTheDocument();
expect(screen.getByTestId('llm-overview-dashboard')).toBeInTheDocument();
await waitFor(() =>
expect(screen.getByTestId('llm-overview-dashboard')).toBeInTheDocument(),
);
expect(screen.getByRole('tab', { name: 'Overview' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Explorer' })).toBeInTheDocument();
expect(

View File

@@ -28,7 +28,7 @@ function InfraMetrics({
dataSource = DataSource.LOGS,
}: MetricsDataProps): JSX.Element {
const [selectedView, setSelectedView] = useState<string>(() =>
podName ? VIEW_TYPES.POD : VIEW_TYPES.NODE,
nodeName || hostName ? VIEW_TYPES.NODE : VIEW_TYPES.POD,
);
const viewOptions = useMemo(() => {
@@ -60,6 +60,10 @@ function InfraMetrics({
}, [podName]);
const handleModeChange = (value: string): void => {
// single toggle-group emits '' on re-click of the pressed item
if (!value) {
return;
}
setSelectedView(value);
};

View File

@@ -4,9 +4,8 @@ import { Card, Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import dayjs from 'dayjs';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
@@ -61,9 +60,9 @@ function NodeMetrics({
const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'NODE'],
queryKey: ['metrics', payload, ENTITY_VERSION_V5, 'NODE'],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
GetMetricQueryRange(payload, ENTITY_VERSION_V5),
enabled: !!payload,
})),
);
@@ -85,7 +84,6 @@ function NodeMetrics({
);
const { timezone } = useTimezone();
const { currentQuery } = useQueryBuilder();
const options = useMemo(
() =>
@@ -103,7 +101,7 @@ function NodeMetrics({
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
query: currentQuery,
query: queryPayloads[idx].query,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
@@ -122,7 +120,7 @@ function NodeMetrics({
verticalLineTimestamp,
end,
timezone.value,
currentQuery,
queryPayloads,
],
);

View File

@@ -4,9 +4,8 @@ import { Card, Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import Uplot from 'components/Uplot';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ENTITY_VERSION_V5 } from 'constants/app';
import dayjs from 'dayjs';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
@@ -58,9 +57,9 @@ function PodMetrics({
);
const queries = useQueries(
queryPayloads.map((payload) => ({
queryKey: ['metrics', payload, ENTITY_VERSION_V4, 'POD'],
queryKey: ['metrics', payload, ENTITY_VERSION_V5, 'POD'],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
GetMetricQueryRange(payload, ENTITY_VERSION_V5),
enabled: !!payload,
})),
);
@@ -74,7 +73,6 @@ function PodMetrics({
[queries],
);
const { timezone } = useTimezone();
const { currentQuery } = useQueryBuilder();
const options = useMemo(
() =>
@@ -92,7 +90,7 @@ function PodMetrics({
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
query: currentQuery,
query: queryPayloads[idx].query,
legendScrollPosition: legendScrollPositionRef.current,
setLegendScrollPosition: (position: {
scrollTop: number;
@@ -110,7 +108,7 @@ function PodMetrics({
end,
verticalLineTimestamp,
timezone.value,
currentQuery,
queryPayloads,
],
);

View File

@@ -1,18 +1,13 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import type { Having } from 'types/api/queryBuilder/queryBuilderData';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { Having as HavingV5 } from 'types/api/v5/queryRange';
import type { Having } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
const buildSumGreaterThanZeroHaving = (
metricKey: string,
useV5HavingFormat: boolean,
): Having[] | HavingV5 =>
useV5HavingFormat
? { expression: `sum(${metricKey}) > 0` }
: [{ columnName: `SUM(${metricKey})`, op: '>', value: 0 }];
const buildSumGreaterThanZeroHaving = (metricKey: string): Having => ({
expression: `sum(${metricKey}) > 0`,
});
export const getPodQueryPayload = (
clusterName: string,
@@ -1550,7 +1545,6 @@ export const getHostQueryPayload = (
hostName: string,
start: number,
end: number,
useV5HavingFormat = false,
): GetQueryResultsProps[] => {
const hostNameKey = 'host.name';
const cpuTimeKey = 'system.cpu.time';
@@ -1813,7 +1807,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
having: buildSumGreaterThanZeroHaving(fsUsageKey),
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
@@ -1862,7 +1856,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
having: buildSumGreaterThanZeroHaving(fsUsageKey),
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
@@ -2088,7 +2082,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(netIoKey, useV5HavingFormat),
having: buildSumGreaterThanZeroHaving(netIoKey),
legend: '{{device}}::{{direction}}',
limit: 30,
orderBy: [],
@@ -2544,7 +2538,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskOpsKey, useV5HavingFormat),
having: buildSumGreaterThanZeroHaving(diskOpsKey),
legend: '{{device}}::{{direction}}',
limit: null,
orderBy: [],
@@ -2613,7 +2607,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskPendingKey, useV5HavingFormat),
having: buildSumGreaterThanZeroHaving(diskPendingKey),
legend: '{{device}}',
limit: null,
orderBy: [],
@@ -2689,7 +2683,7 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskOpTimeKey, useV5HavingFormat),
having: buildSumGreaterThanZeroHaving(diskOpTimeKey),
legend: '{{device}}::{{direction}}',
limit: null,
orderBy: [],

View File

@@ -242,7 +242,7 @@ function Application(): JSX.Element {
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const avialableParams = routeConfig[ROUTES.TRACES_EXPLORER];
const queryString = getQueryString(avialableParams, urlParams);
const JSONCompositeQuery = encodeURIComponent(

View File

@@ -10,7 +10,7 @@ import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute
import { AppState } from 'store/reducers';
import { PayloadProps } from 'types/api/metrics/getTopOperations';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
function TopOperation(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(

View File

@@ -10,7 +10,7 @@ import useResourceAttribute from 'hooks/useResourceAttribute';
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
import createQueryParams from 'lib/createQueryParams';
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
import { traceFilterKeys } from 'constants/traceFilterKeys';
import {
BaseAutocompleteData,
DataTypes,
@@ -21,7 +21,7 @@ import {
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
import { isModifierKeyPressed } from 'utils/app';
import { secondsToMilliseconds } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -102,7 +102,7 @@ export function onViewTracePopupClick({
urlParams.set(QueryParams.startTime, startTime.toString());
urlParams.set(QueryParams.endTime, endTime.toString());
urlParams.delete(QueryParams.relativeTime);
const avialableParams = routeConfig[ROUTES.TRACE];
const avialableParams = routeConfig[ROUTES.TRACES_EXPLORER];
const queryString = getQueryString(avialableParams, urlParams);
const JSONCompositeQuery = encodeURIComponent(

View File

@@ -56,10 +56,6 @@ jest.mock('../Tabs/util', () => ({
}));
// Mock the resourceAttributesToTracesFilterItems function
jest.mock('container/TraceDetail/utils', () => ({
resourceAttributesToTracesFilterItems: (): any[] => [],
}));
const mockedUseResourceAttribute = useResourceAttribute as jest.MockedFunction<
typeof useResourceAttribute
>;

View File

@@ -1,5 +0,0 @@
.new-explorer-cta-with-badge {
display: inline-flex;
align-items: center;
gap: 6px;
}

View File

@@ -1,8 +0,0 @@
import ROUTES from 'constants/routes';
export const buttonText: Record<string, string> = {
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
[ROUTES.TRACE]: 'New Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'New Explorer',
[ROUTES.TRACES_EXPLORER]: 'Old Explorer',
};

View File

@@ -1,83 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Undo } from '@signozhq/icons';
import { isModifierKeyPressed } from 'utils/app';
import { buttonText } from './config';
import './NewExplorerCTA.styles.scss';
function NewExplorerCTA(): JSX.Element | null {
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const isTraceOrLogsExplorerPage = useMemo(
() =>
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACE ||
location.pathname === ROUTES.OLD_LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER,
[location.pathname],
);
const onClickHandler = useCallback(
(e?: React.MouseEvent): void => {
let targetPath: string;
if (location.pathname === ROUTES.LOGS_EXPLORER) {
targetPath = ROUTES.OLD_LOGS_EXPLORER;
} else if (location.pathname === ROUTES.TRACE) {
targetPath = ROUTES.TRACES_EXPLORER;
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
targetPath = ROUTES.LOGS_EXPLORER;
} else if (location.pathname === ROUTES.TRACES_EXPLORER) {
targetPath = ROUTES.TRACE;
} else {
return;
}
safeNavigate(targetPath, { newTab: !!e && isModifierKeyPressed(e) });
},
[location.pathname],
);
const button = useMemo(
() => (
<Button
icon={<Undo size={16} />}
onClick={(e): void => onClickHandler(e)}
data-testid="newExplorerCTA"
type="text"
className="periscope-btn link"
>
{buttonText[location.pathname]}
</Button>
),
[location.pathname, onClickHandler],
);
if (!isTraceOrLogsExplorerPage) {
return null;
}
if (location.pathname === ROUTES.TRACES_EXPLORER) {
return button;
}
if (location.pathname === ROUTES.LOGS_EXPLORER) {
return button;
}
return (
<span className="new-explorer-cta-with-badge">
{button}
<Badge color="robin" variant="default">
New
</Badge>
</span>
);
}
export default NewExplorerCTA;

View File

@@ -10,7 +10,7 @@ import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { has } from 'lodash-es';
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
import { AllTraceFilterKeyValue } from 'constants/traceFilterKeys';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import {
QueryKeyRequestProps,

View File

@@ -3,7 +3,7 @@ import { Badge } from '@signozhq/ui/badge';
import { Button, Table, TableProps } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { themeColors } from 'constants/theme';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import { StyledCSS } from 'types/styledComponents';
import styled from 'styled-components';
export const FooterButton = styled(Button)`

View File

@@ -11,7 +11,7 @@ import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceMetricsApplication from './ServiceMetricsApplication';

View File

@@ -15,7 +15,7 @@ import {
import { isUndefined } from 'lodash-es';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { Tags } from 'hooks/useResourceAttribute/types';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceTraceTable from './ServiceTracesTable';

View File

@@ -42,9 +42,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
[ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes],
[ROUTES.TRACES_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.TRACE]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_DETAIL_OLD]: [QueryParams.resourceAttributes],
[ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],
[ROUTES.USAGE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.VERSION]: [QueryParams.resourceAttributes],

View File

@@ -568,7 +568,7 @@ export const getUserSettingsDropdownMenuItems = ({
This is used to highlight the correct menu item when the user navigates to a new route
**/
export const NEW_ROUTES_MENU_ITEM_KEY_MAP: Record<string, string> = {
[ROUTES.TRACE]: ROUTES.TRACES_EXPLORER,
[ROUTES.TRACE_BASE]: ROUTES.TRACES_EXPLORER,
[ROUTES.TRACE_EXPLORER]: ROUTES.TRACES_EXPLORER,
[ROUTES.LOGS_BASE]: ROUTES.LOGS_EXPLORER,
[ROUTES.METRICS_EXPLORER_BASE]: ROUTES.METRICS_EXPLORER,
@@ -580,7 +580,7 @@ export const NEW_ROUTES_MENU_ITEM_KEY_MAP: Record<string, string> = {
// `getActiveMenuKeyFromPath` strips the URL down to its first segment;
// `/ai-assistant/<id>` reduces to `/ai-assistant`, which we point back
// to the AI Assistant menu item's concrete key.
'/ai-assistant': AI_ASSISTANT_NAV_KEY,
[ROUTES.AI_ASSISTANT_BASE]: AI_ASSISTANT_NAV_KEY,
};
export default menuItems;

View File

@@ -2,7 +2,6 @@ import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import ROUTES from 'constants/routes';
import LiveLogsPauseResume from 'container/LiveLogs/LiveLogsPauseResume/LiveLogsPauseResume';
import NewExplorerCTA from 'container/NewExplorerCTA';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { noop } from 'lodash-es';
@@ -12,7 +11,6 @@ interface ToolbarProps {
showAutoRefresh: boolean;
leftActions?: JSX.Element;
rightActions?: JSX.Element;
showOldCTA?: boolean;
warningElement?: JSX.Element;
onGoLive?: () => void;
onExitLiveLogs?: () => void;
@@ -23,7 +21,6 @@ export default function Toolbar({
showAutoRefresh,
leftActions,
rightActions,
showOldCTA,
warningElement,
showLiveLogs,
onGoLive,
@@ -48,7 +45,6 @@ export default function Toolbar({
<div className="rightActions">
<div className="timeRange">
{warningElement}
{showOldCTA && <NewExplorerCTA />}
{showLiveLogs && <LiveLogsPauseResume />}
<DateTimeSelectionV2
showLiveLogs={showLiveLogs}
@@ -69,7 +65,6 @@ export default function Toolbar({
Toolbar.defaultProps = {
leftActions: <div />,
rightActions: <div />,
showOldCTA: false,
warningElement: <div />,
showLiveLogs: false,
onGoLive: (): void => noop(),

View File

@@ -20,11 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
__esModule: true,
default: ({

View File

@@ -20,11 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
null;
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =

View File

@@ -61,11 +61,6 @@ jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
function NuqsParamSetter({ paramValue }: { paramValue: string }): JSX.Element {
const [, setYAxisUnit] = useQueryState(
'yAxisUnit',

View File

@@ -15,11 +15,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="custom-time-picker" />,

View File

@@ -20,11 +20,6 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
jest.mock('container/NewExplorerCTA', () => ({
__esModule: true,
default: (): null => null,
}));
let mockOnCustomDateHandler: ((range: [unknown, unknown]) => void) | null =
null;
let mockOnValidCustomDateChange: ((data: { timeStr: string }) => void) | null =

View File

@@ -143,7 +143,6 @@ export const routesToSkip = [
ROUTES.SETTINGS,
ROUTES.LIST_ALL_ALERT,
ROUTES.TRACE_DETAIL,
ROUTES.TRACE_DETAIL_OLD,
ROUTES.ALL_CHANNELS,
ROUTES.USAGE_EXPLORER,
ROUTES.GET_STARTED_WITH_CLOUD,

View File

@@ -12,7 +12,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import NewExplorerCTA from 'container/NewExplorerCTA';
import dayjs, { Dayjs } from 'dayjs';
import {
useGlobalTimeQueryInvalidate,
@@ -65,7 +64,6 @@ function DateTimeSelection({
updateTimeInterval,
globalTimeLoading,
showResetButton = false,
showOldExplorerCTA = false,
defaultRelativeTime = RelativeTimeMap['6hr'] as Time,
isModalTimeSelection = false,
onTimeChange,
@@ -688,12 +686,6 @@ function DateTimeSelection({
</FormItem>
)}
{showOldExplorerCTA && (
<div style={{ marginRight: 12 }}>
<NewExplorerCTA />
</div>
)}
{!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && (
<div className="refresh-text-container">
<RefreshText
@@ -770,7 +762,6 @@ interface DateTimeSelectionV2Props {
showAutoRefresh: boolean;
showRefreshText?: boolean;
hideShareModal?: boolean;
showOldExplorerCTA?: boolean;
showResetButton?: boolean;
defaultRelativeTime?: Time;
isModalTimeSelection?: boolean;
@@ -792,7 +783,6 @@ interface DateTimeSelectionV2Props {
DateTimeSelection.defaultProps = {
hideShareModal: false,
showOldExplorerCTA: false,
showRefreshText: true,
showResetButton: false,
defaultRelativeTime: RelativeTimeMap['6hr'] as Time,

View File

@@ -3,7 +3,6 @@ import { matchPath, useHistory } from 'react-router-dom';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import ROUTES from 'constants/routes';
import NewExplorerCTA from '../NewExplorerCTA';
import DateTimeSelector from './DateTimeSelectionV2';
import { routesToDisable, routesToSkip } from './DateTimeSelectionV2/constants';
@@ -44,7 +43,6 @@ function TopNav(): JSX.Element | null {
return !isRouteToSkip ? (
<div className="top-nav-container">
<NewExplorerCTA />
<DateTimeSelector showAutoRefresh />
<HeaderRightSection enableShare enableFeedback enableAnnouncements={false} />
</div>

View File

@@ -1,200 +0,0 @@
import { useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Tooltip } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import getFilters from 'api/trace/getFilters';
import { AxiosError } from 'axios';
import { useNotifications } from 'hooks/useNotifications';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { CheckBoxContainer, ParaGraph } from './styles';
function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
const {
selectedFilter,
filterLoading,
filterToFetchData,
spansAggregate,
selectedTags,
filter,
userSelectedFilter,
isFilterExclude,
spanKind,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { keyValue, name, value } = props;
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const isUserSelected =
(userSelectedFilter.get(name) || []).find((e) => e === keyValue) !==
undefined;
const { notifications } = useNotifications();
// eslint-disable-next-line sonarjs/cognitive-complexity
const onCheckHandler = async (): Promise<void> => {
try {
setIsLoading(true);
const newSelectedMap = new Map(selectedFilter);
const preUserSelectedMap = new Map(userSelectedFilter);
const preIsFilterExclude = new Map(isFilterExclude);
const isTopicPresent = preUserSelectedMap.get(name);
// append the value
if (!isTopicPresent) {
preUserSelectedMap.set(name, [keyValue]);
} else {
const isValuePresent =
isTopicPresent.find((e) => e === keyValue) !== undefined;
// check the value if present then remove the value or isChecked
if (isValuePresent) {
preUserSelectedMap.set(
name,
isTopicPresent.filter((e) => e !== keyValue),
);
} else {
// if not present add into the array of string
preUserSelectedMap.set(name, [...isTopicPresent, keyValue]);
}
}
if (newSelectedMap.get(name)?.find((e) => e === keyValue)) {
newSelectedMap.set(name, [
...(newSelectedMap.get(name) || []).filter((e) => e !== keyValue),
]);
} else {
newSelectedMap.set(name, [
...new Set([...(newSelectedMap.get(name) || []), keyValue]),
]);
}
if (preIsFilterExclude.get(name) !== false) {
preIsFilterExclude.set(name, true);
}
const response = await getFilters({
other: Object.fromEntries(newSelectedMap),
end: String(globalTime.maxTime),
start: String(globalTime.minTime),
getFilters: filterToFetchData.filter((e) => e !== name),
isFilterExclude: preIsFilterExclude,
spanKind,
});
if (response.statusCode === 200) {
const updatedFilter = getFilter(response.payload);
// updatedFilter.forEach((value, key) => {
// if (key !== 'duration' && name !== key) {
// preUserSelectedMap.set(key, Object.keys(value));
// }
// if (key === 'duration') {
// newSelectedMap.set('duration', [value.maxDuration, value.minDuration]);
// }
// });
updatedFilter.set(name, {
[`${keyValue}`]: '-1',
...(filter.get(name) || {}),
...(updatedFilter.get(name) || {}),
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
selectedTags,
current: spansAggregate.currentPage,
filter: updatedFilter,
filterToFetchData,
selectedFilter: newSelectedMap,
userSelected: preUserSelectedMap,
isFilterExclude: preIsFilterExclude,
order: spansAggregate.order,
orderParam: spansAggregate.orderParam,
pageSize: spansAggregate.pageSize,
spanKind,
},
});
setIsLoading(false);
updateURL(
newSelectedMap,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
preIsFilterExclude,
preUserSelectedMap,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
} else {
setIsLoading(false);
notifications.error({
message: response.error || 'Something went wrong',
});
}
} catch (error) {
notifications.error({
message: (error as AxiosError).toString() || 'Something went wrong',
});
setIsLoading(false);
}
};
const isCheckBoxSelected = isUserSelected;
const TooTipOverLay = useMemo(
(): JSX.Element => <div>{keyValue}</div>,
[keyValue],
);
return (
<CheckBoxContainer>
<Checkbox
disabled={isLoading || filterLoading}
onClick={onCheckHandler}
value={isCheckBoxSelected}
key={keyValue}
>
<Tooltip overlay={TooTipOverLay}>
<ParaGraph truncate={1}>{keyValue}</ParaGraph>
</Tooltip>
</Checkbox>
{isCheckBoxSelected ? (
<Typography>{value}</Typography>
) : (
<Typography>-</Typography>
)}
</CheckBoxContainer>
);
}
interface CheckBoxProps {
keyValue: string;
name: TraceFilterEnum;
value: string;
}
export default CheckBoxComponent;

View File

@@ -1,19 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const CheckBoxContainer = styled.div`
display: flex;
justify-content: space-between;
margin-left: 1rem;
margin-right: 1rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
`;
export const ParaGraph = styled(Typography.Text)`
&&& {
margin: 0;
max-width: 8rem;
}
`;

View File

@@ -1,104 +0,0 @@
import { useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Button, Input } from 'antd';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { INITIAL_FILTER_VALUE } from 'store/reducers/trace';
import AppActions from 'types/actions';
import { UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE } from 'types/actions/trace';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import CheckBoxComponent from '../Common/Checkbox';
const { Search } = Input;
function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
const { filter, filterDisplayValue } = useSelector<AppState, TraceReducer>(
(state) => state.traces,
);
const { name } = props;
const status = filter.get(name) || {};
const statusObj = Object.keys(status);
const numberOfFilters = filterDisplayValue.get(name) || 0;
const dispatch = useDispatch<Dispatch<AppActions>>();
const [searchFilter, setSearchFilter] = useState<string>('');
const onClickMoreHandler = (): void => {
const newFilterDisplayValue = new Map(filterDisplayValue);
const preValue =
(newFilterDisplayValue.get(name) || 0) + INITIAL_FILTER_VALUE;
newFilterDisplayValue.set(name, preValue);
dispatch({
type: UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE,
payload: newFilterDisplayValue,
});
};
const isMoreButtonAvilable = Boolean(
numberOfFilters && statusObj.length > numberOfFilters,
);
return (
<>
{statusObj.length > 0 && (
<Search
value={searchFilter}
onChange={(e): void => setSearchFilter(e.target.value)}
style={{
padding: '0 3%',
}}
placeholder="Filter Values"
/>
)}
{statusObj
.sort((a, b) => {
const countA = +status[a];
const countB = +status[b];
if (countA === countB) {
return a.length - b.length;
}
return countA - countB;
})
.filter((filter) => {
if (searchFilter.length === 0) {
return true;
}
return filter
.toLocaleLowerCase()
.includes(searchFilter.toLocaleLowerCase());
})
.filter((_, index) => index < numberOfFilters)
.map((e) => (
<CheckBoxComponent
key={e}
{...{
name,
keyValue: e,
value: status[e],
}}
/>
))}
{isMoreButtonAvilable && (
<Button onClick={onClickMoreHandler} type="link">
More
</Button>
)}
</>
);
}
interface CommonCheckBoxProps {
name: TraceFilterEnum;
}
export default CommonCheckBox;

View File

@@ -1,223 +0,0 @@
import {
ChangeEventHandler,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Slider } from '@signozhq/ui/slider';
import getFilters from 'api/trace/getFilters';
import useDebouncedFn from 'hooks/useDebouncedFunction';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { Container, InputComponent, InputContainer, Text } from './styles';
import { getMs } from './util';
function Duration(): JSX.Element {
const {
filter,
selectedFilter,
filterToFetchData,
spansAggregate,
selectedTags,
userSelectedFilter,
isFilterExclude,
spanKind,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const preLocalMaxDuration = useRef<number>();
const preLocalMinDuration = useRef<number>();
const getDuration = useMemo(() => {
const selectedDuration = selectedFilter.get('duration');
if (selectedDuration) {
return {
maxDuration: selectedDuration[0],
minDuration: selectedDuration[1],
};
}
return filter.get('duration') || {};
}, [selectedFilter, filter]);
const [preMax, setPreMax] = useState<string>('');
const [preMin, setPreMin] = useState<string>('');
useEffect(() => {
const duration = getDuration || {};
const maxDuration = duration.maxDuration || '0';
const minDuration = duration.minDuration || '0';
if (preLocalMaxDuration.current === undefined) {
preLocalMaxDuration.current = parseFloat(maxDuration);
}
if (preLocalMinDuration.current === undefined) {
preLocalMinDuration.current = parseFloat(minDuration);
}
setPreMax(getMs(maxDuration));
setPreMin(getMs(minDuration));
}, [getDuration]);
const updatedUrl = async (min: number, max: number): Promise<void> => {
const preSelectedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
preSelectedFilter.set('duration', [
String(max * 1000000),
String(min * 1000000),
]);
const response = await getFilters({
end: String(globalTime.maxTime),
getFilters: filterToFetchData,
other: Object.fromEntries(preSelectedFilter),
start: String(globalTime.minTime),
isFilterExclude,
spanKind,
});
if (response.statusCode === 200) {
const preFilter = getFilter(response.payload);
preFilter.forEach((value, key) => {
const values = Object.keys(value);
if (key !== 'duration' && values.length) {
preUserSelected.set(key, values);
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: preFilter,
filterToFetchData,
selectedFilter: preSelectedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
preSelectedFilter,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
userSelectedFilter,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
}
};
const onRangeSliderHandler = (number: [string, string]): void => {
const [min, max] = number;
setPreMin(min);
setPreMax(max);
};
const debouncedFunction = useDebouncedFn(
(min, max) => {
updatedUrl(min as number, max as number);
},
1500,
undefined,
);
const onChangeMaxHandler: ChangeEventHandler<HTMLInputElement> = (event) => {
const { value } = event.target;
const min = preMin;
const max = value;
onRangeSliderHandler([min, max]);
debouncedFunction(min, max);
};
const onChangeMinHandler: ChangeEventHandler<HTMLInputElement> = (event) => {
const { value } = event.target;
const min = value;
const max = preMax;
onRangeSliderHandler([min, max]);
debouncedFunction(min, max);
};
const onRangeHandler = (value: number | number[]): void => {
const [min, max] = value as number[];
updatedUrl(min, max);
};
const TipComponent = useCallback(
(value: number) => <div>{`${value.toString()}ms`}</div>,
[],
);
return (
<div>
<Container>
<InputContainer>
<Text>Min</Text>
</InputContainer>
<InputComponent
addonAfter="ms"
type="number"
onChange={onChangeMinHandler}
value={preMin}
/>
<InputContainer>
<Text>Max</Text>
</InputContainer>
<InputComponent
addonAfter="ms"
type="number"
onChange={onChangeMaxHandler}
value={preMax}
/>
</Container>
<Container>
<Slider
min={Number(getMs(String(preLocalMinDuration.current || 0)))}
max={Number(getMs(String(preLocalMaxDuration.current || 0)))}
range
tooltip={{ formatter: TipComponent }}
onChange={(value): void => {
const [min, max] = value as number[];
onRangeSliderHandler([String(min), String(max)]);
}}
onAfterChange={onRangeHandler}
value={[Number(preMin), Number(preMax)]}
/>
</Container>
</div>
);
}
export default Duration;

View File

@@ -1,43 +0,0 @@
// TODO(@signozhq/ui-input): migrate this styled(Input) once @signozhq/ui
// Input supports `addonAfter` (the consumer renders `<InputComponent addonAfter="ms">`).
import { Typography } from '@signozhq/ui/typography';
import { Input } from 'antd';
import styled from 'styled-components';
export const DurationText = styled.div`
display: flex;
align-items: center;
justify-content: space-around;
min-height: 8vh;
flex-direction: column;
`;
export const InputComponent = styled(Input)`
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
`;
export const InputContainer = styled.div`
width: 100%;
margin-top: 0.5rem;
margin-bottom: 0.2rem;
`;
export const Text = styled(Typography)`
&&& {
text-align: left;
}
`;
export const Container = styled.div`
padding-left: 1rem;
padding-right: 1rem;
`;

View File

@@ -1,13 +0,0 @@
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
dayjs.extend(durationPlugin);
export const getMs = (value: string): string =>
parseFloat(
dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS'),
).toFixed(2);

View File

@@ -1,134 +0,0 @@
import { ChangeEvent, useEffect, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Input } from 'antd';
import getFilters from 'api/trace/getFilters';
import { AxiosError } from 'axios';
import { useNotifications } from 'hooks/useNotifications';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
const { Search } = Input;
function TraceID(): JSX.Element {
const {
selectedFilter,
filterToFetchData,
spansAggregate,
selectedTags,
userSelectedFilter,
isFilterExclude,
spanKind,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [isLoading, setIsLoading] = useState(false);
const [userEnteredValue, setUserEnteredValue] = useState<string>('');
const { notifications } = useNotifications();
useEffect(() => {
setUserEnteredValue(selectedFilter.get('traceID')?.[0] || '');
}, [selectedFilter]);
const onSearch = async (value: string): Promise<void> => {
try {
setIsLoading(true);
const preSelectedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
if (value !== '') {
preUserSelected.set('traceID', [value]);
preSelectedFilter.set('traceID', [value]);
} else {
preUserSelected.delete('traceID');
preSelectedFilter.delete('traceID');
}
const response = await getFilters({
other: Object.fromEntries(preSelectedFilter),
end: String(globalTime.maxTime),
start: String(globalTime.minTime),
getFilters: filterToFetchData,
isFilterExclude,
spanKind,
});
if (response.statusCode === 200) {
const preFilter = getFilter(response.payload);
preFilter.set('traceID', { traceID: value });
preFilter.forEach((value, key) => {
const values = Object.keys(value);
if (key !== 'duration' && values.length) {
preUserSelected.set(key, values);
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: preFilter,
filterToFetchData,
selectedFilter: preSelectedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
preSelectedFilter,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
userSelectedFilter,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
}
} catch (error) {
notifications.error({
message: (error as AxiosError).toString() || 'Something went wrong',
});
} finally {
setIsLoading(false);
}
};
const onChange = (e: ChangeEvent<HTMLInputElement>): void => {
setUserEnteredValue(e.target.value);
};
const onBlur = (): void => {
if (userEnteredValue !== selectedFilter.get('traceID')?.[0]) {
onSearch(userEnteredValue);
}
};
return (
<div>
<Search
placeholder="Filter by Trace ID"
onSearch={onSearch}
style={{
marginBottom: '5rem',
padding: '0 3%',
}}
loading={isLoading}
value={userEnteredValue}
onChange={onChange}
onBlur={onBlur}
/>
</div>
);
}
export default TraceID;

View File

@@ -1,43 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Card } from 'antd';
import Spinner from 'components/Spinner';
import { AppState } from 'store/reducers';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import CommonCheckBox from './CommonCheckBox';
import Duration from './Duration';
import TraceID from './SearchTraceID';
function PanelBody(props: PanelBodyProps): JSX.Element {
const { type } = props;
const { filterLoading } = useSelector<AppState, TraceReducer>(
(state) => state.traces,
);
if (filterLoading) {
return (
<Card bordered={false}>
<Spinner height="10vh" tip="Loading.." />
</Card>
);
}
const renderBody = (type: TraceFilterEnum): JSX.Element => {
switch (type) {
case 'traceID':
return <TraceID />;
case 'duration':
return <Duration />;
default:
return <CommonCheckBox name={type} />;
}
};
return <Card bordered={false}>{renderBody(type)}</Card>;
}
interface PanelBodyProps {
type: TraceFilterEnum;
}
export default PanelBody;

View File

@@ -1,360 +0,0 @@
import { MouseEventHandler, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import { Card } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import getFilters from 'api/trace/getFilters';
import { AxiosError } from 'axios';
import { useNotifications } from 'hooks/useNotifications';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
AllPanelHeading,
TraceFilterEnum,
TraceReducer,
} from 'types/reducer/trace';
import {
ButtonComponent,
ButtonContainer,
Container,
IconContainer,
TextContainer,
} from './styles';
const { Text } = Typography;
function PanelHeading(props: PanelHeadingProps): JSX.Element {
const {
filterLoading,
filterToFetchData,
selectedFilter,
spansAggregate,
selectedTags,
filter,
isFilterExclude,
userSelectedFilter,
spanKind,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const { name: PanelName, isOpen: IsPanelOpen } = props;
const isDefaultOpen =
filterToFetchData.find((e) => e === PanelName) !== undefined;
const [isLoading, setIsLoading] = useState<boolean>(false);
const global = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const defaultErrorMessage = 'Something went wrong';
const { notifications } = useNotifications();
const onExpandHandler: MouseEventHandler<HTMLDivElement> = async (e) => {
try {
e.preventDefault();
e.stopPropagation();
setIsLoading(true);
let updatedFilterData: TraceReducer['filterToFetchData'] = [];
const getprepdatedSelectedFilter = new Map(selectedFilter);
const getPreUserSelected = new Map(userSelectedFilter);
updatedFilterData = [PanelName];
const response = await getFilters({
end: String(global.maxTime),
start: String(global.minTime),
getFilters: updatedFilterData,
other: Object.fromEntries(getprepdatedSelectedFilter),
isFilterExclude,
spanKind,
});
if (response.statusCode === 200) {
const updatedFilter = getFilter(response.payload);
if (!getPreUserSelected.has(PanelName)) {
getPreUserSelected.set(
PanelName,
Object.keys(updatedFilter.get(PanelName) || []),
);
}
updatedFilterData = [...filterToFetchData, PanelName];
filter.forEach((value, key) => {
if (key !== PanelName) {
updatedFilter.set(key, value);
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: updatedFilter,
filterToFetchData: updatedFilterData,
selectedFilter: getprepdatedSelectedFilter,
selectedTags,
userSelected: getPreUserSelected,
isFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
getprepdatedSelectedFilter,
updatedFilterData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
getPreUserSelected,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
} else {
notifications.error({
message: response.error || defaultErrorMessage,
});
}
setIsLoading(false);
} catch (error) {
notifications.error({
message: (error as AxiosError).toString() || defaultErrorMessage,
});
}
};
/**
* @description this function removes the selected filter
*/
const onCloseHandler = (): void => {
const preSelectedFilter = new Map(selectedFilter);
// removing the filter from filter to fetch the data
const preFilterToFetchTheData = [
...filterToFetchData.filter((name) => name !== PanelName),
];
// preSelectedFilter.delete(PanelName);
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter,
filterToFetchData: preFilterToFetchTheData,
selectedFilter: preSelectedFilter,
selectedTags,
userSelected: userSelectedFilter,
isFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
preSelectedFilter,
preFilterToFetchTheData,
spansAggregate.currentPage,
selectedTags,
isFilterExclude,
userSelectedFilter,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
};
const onClearAllHandler = async (): Promise<void> => {
try {
setIsLoading(true);
const updatedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
updatedFilter.delete(PanelName);
preUserSelected.delete(PanelName);
const postIsFilterExclude = new Map(isFilterExclude);
postIsFilterExclude.set(PanelName, false);
const response = await getFilters({
end: String(global.maxTime),
start: String(global.minTime),
getFilters: filterToFetchData,
other: Object.fromEntries(updatedFilter),
isFilterExclude: postIsFilterExclude,
spanKind,
});
if (response.statusCode === 200 && response.payload) {
const getUpdatedFilter = getFilter(response.payload);
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: getUpdatedFilter,
filterToFetchData,
selectedFilter: updatedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude: postIsFilterExclude,
order: spansAggregate.order,
pageSize: spansAggregate.pageSize,
orderParam: spansAggregate.orderParam,
spanKind,
},
});
updateURL(
updatedFilter,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
postIsFilterExclude,
preUserSelected,
spansAggregate.order,
spansAggregate.pageSize,
spansAggregate.orderParam,
);
} else {
notifications.error({
message: response.error || 'Something went wrong',
});
}
setIsLoading(false);
} catch (error) {
notifications.error({
message: (error as AxiosError).toString(),
});
setIsLoading(false);
}
};
// const onSelectAllHandler = async () => {
// try {
// setIsLoading(true);
// const preFilter = new Map(filter);
// const preSelectedFilter = new Map(selectedFilter);
// preSelectedFilter.set(
// props.name,
// Object.keys(preFilter.get(props.name) || {}),
// );
// const response = await getFilters({
// end: String(global.maxTime),
// start: String(global.minTime),
// getFilters: filterToFetchData,
// other: Object.fromEntries(preSelectedFilter),
// });
// if (response.statusCode === 200 && response.payload) {
// const getUpatedFilter = getFilter(response.payload);
// preSelectedFilter.set(
// props.name,
// Object.keys(getUpatedFilter.get(props.name) || {}),
// );
// dispatch({
// type: UPDATE_ALL_FILTERS,
// payload: {
// current: spansAggregate.currentPage,
// filter: preFilter,
// filterToFetchData,
// selectedFilter: preSelectedFilter,
// selectedTags,
// },
// });
// updateURL(
// preSelectedFilter,
// filterToFetchData,
// spansAggregate.currentPage,
// selectedTags,
// preFilter,
// );
// }
// setIsLoading(false);
// } catch (error) {
// setIsLoading(false);
// notification.error({
// message: (error as AxiosError).toString(),
// });
// }
// };
return (
<>
{PanelName !== 'duration' && <Divider plain style={{ margin: 0 }} />}
<Card bordered={false}>
<Container
disabled={filterLoading || isLoading}
aria-disabled={filterLoading || isLoading}
aria-expanded={IsPanelOpen}
>
<TextContainer onClick={isDefaultOpen ? onCloseHandler : onExpandHandler}>
<IconContainer>
{!IsPanelOpen ? <ChevronRight size="lg" /> : <ChevronDown size="lg" />}
</IconContainer>
<Text style={{ textTransform: 'capitalize' }} truncate={1}>
{AllPanelHeading.find((e) => e.key === PanelName)?.displayValue || ''}
</Text>
</TextContainer>
{PanelName !== 'duration' && (
<ButtonContainer>
{/* <ButtonComponent
aria-disabled={isLoading || filterLoading}
disabled={isLoading || filterLoading}
onClick={onSelectAllHandler}
type="link"
>
Select All
</ButtonComponent> */}
<ButtonComponent
aria-disabled={isLoading || filterLoading}
disabled={isLoading || filterLoading}
onClick={onClearAllHandler}
type="link"
>
Clear All
</ButtonComponent>
</ButtonContainer>
)}
</Container>
</Card>
</>
);
}
interface PanelHeadingProps {
name: TraceFilterEnum;
isOpen: boolean;
}
export default PanelHeading;

View File

@@ -1,50 +0,0 @@
import { Button } from 'antd';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import styled, { css } from 'styled-components';
interface Props {
disabled: boolean;
}
export const Container = styled.div<Props>`
&&& {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 0.5rem;
min-height: 5vh;
cursor: ${({ disabled }): string => (disabled ? 'not-allowed' : '')};
${({ disabled }): StyledCSS =>
disabled &&
css`
opacity: 0.5;
`}
}
`;
export const IconContainer = styled.div`
&&& {
margin-right: 0.5rem;
}
`;
export const TextContainer = styled.div`
&&& {
display: flex;
cursor: pointer;
}
`;
export const ButtonComponent = styled(Button)`
&&& {
font-size: 0.75rem;
}
`;
export const ButtonContainer = styled.div`
&&& {
display: flex;
}
`;

View File

@@ -1,30 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import PanelBody from './PanelBody';
import PanelHeading from './PanelHeading';
function Panel(props: PanelProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const { name } = props;
const isDefaultOpen =
traces.filterToFetchData.find((e) => e === name) !== undefined;
return (
<>
<PanelHeading name={name} isOpen={isDefaultOpen} />
{isDefaultOpen && <PanelBody type={name} />}
</>
);
}
interface PanelProps {
name: TraceFilterEnum;
}
export default Panel;

View File

@@ -1,29 +0,0 @@
import { TraceFilterEnum } from 'types/reducer/trace';
import Panel from './Panel';
export const AllTraceFilterEnum: TraceFilterEnum[] = [
'duration',
'status',
'serviceName',
'operation',
'rpcMethod',
'responseStatusCode',
'httpHost',
'httpMethod',
'httpRoute',
'httpUrl',
'traceID',
];
function Filters(): JSX.Element {
return (
<>
{AllTraceFilterEnum.map((panelName) => (
<Panel key={panelName} name={panelName} />
))}
</>
);
}
export default Filters;

View File

@@ -1,126 +0,0 @@
import { ChartData, ChartDatasetProperties } from 'chart.js';
import dayjs from 'dayjs';
import { colors } from 'lib/getRandomColor';
import { keys } from 'lodash-es';
import { TraceReducer } from 'types/reducer/trace';
function transposeArray(array: number[][], arrayLength: number): number[][] {
const newArray: number[][] = [];
for (let i = 0; i < array.length; i += 1) {
newArray.push([]);
}
for (let i = 0; i < array.length; i += 1) {
for (let j = 0; j < arrayLength; j += 1) {
newArray[j]?.push(array[i][j]);
}
}
return newArray;
}
export const getChartData = (
data: TraceReducer['spansGraph']['payload'],
): ChartData<'line'> => {
const allDataPoints = data.items;
const chartDataset: ChartDatasetProperties<'line', number[]> = {
data: [],
type: 'line',
};
const chartLabels: ChartData<'line'>['labels'] = [];
if (allDataPoints && typeof allDataPoints === 'object') {
Object.keys(allDataPoints).forEach((timestamp) => {
const key = allDataPoints[timestamp];
if (key.value) {
chartDataset.data.push(key.value);
const date = dayjs(key.timestamp / 1000000);
chartLabels.push(date.toDate().getTime());
}
});
}
return {
datasets: [
{
...chartDataset,
borderWidth: 1.5,
spanGaps: true,
borderColor: colors[0] || 'red',
showLine: true,
pointRadius: 0,
},
],
labels: chartLabels,
};
};
export const getChartDataforGroupBy = (
props: TraceReducer['spansGraph']['payload'],
): ChartData => {
const { items } = props;
const chartData: ChartData = {
datasets: [],
labels: [],
};
let max = 0;
const allGroupBy = Object.keys(items).map((e) => items[e].groupBy);
keys(allGroupBy).forEach((e: string): void => {
// @ts-expect-error
const { length } = keys(allGroupBy[e]);
if (length >= max) {
max = length;
}
});
const numberOfGraphs = max;
const spansGraph: number[][] = [];
const names: string[] = [];
// number of data points
Object.keys(items).forEach((item) => {
const spanData = items[item];
const date = dayjs(Number(item) / 1000000)
.toDate()
.getTime();
chartData.labels?.push(date);
const { groupBy } = spanData;
const preData: number[] = [];
if (groupBy) {
Object.keys(groupBy).forEach((key) => {
const value = groupBy[key];
preData.push(value);
names.push(key);
});
spansGraph.push(preData);
}
});
const updatedName = [...new Set(names)];
transposeArray(spansGraph, numberOfGraphs).forEach((values, index) => {
chartData.datasets.push({
data: values.map((e) => e || 0),
borderWidth: 1.5,
spanGaps: true,
borderColor: colors[index] || 'red',
showLine: true,
pointRadius: 0,
label: updatedName[index],
});
});
return chartData;
};

View File

@@ -1,62 +0,0 @@
import { useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useMeasure } from 'react-use';
import { Typography } from '@signozhq/ui/typography';
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import { getChartData, getChartDataforGroupBy } from './config';
import { Container } from './styles';
function TraceGraph(): JSX.Element {
const [ref, { width }] = useMeasure();
const { spansGraph, selectedGroupBy, yAxisUnit } = useSelector<
AppState,
TraceReducer
>((state) => state.traces);
const { loading, error, errorMessage, payload } = spansGraph;
const ChartData = useMemo(
() =>
selectedGroupBy.length === 0 || selectedGroupBy === 'none'
? getChartData(payload)
: getChartDataforGroupBy(payload),
[payload, selectedGroupBy],
);
if (error) {
return (
<Container center>
<Typography>{errorMessage || 'Something went wrong'}</Typography>
</Container>
);
}
if (loading) {
return (
<Container>
<Spinner height="20vh" size="small" tip="Loading..." />
</Container>
);
}
return (
<Container ref={ref as never}>
<Graph
animate={false}
data={ChartData}
name="traceGraph"
type="line"
yAxisUnit={yAxisUnit}
forceReRender={width}
/>
</Container>
);
}
export default TraceGraph;

View File

@@ -1,23 +0,0 @@
import { RefObject } from 'react';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import styled, { css } from 'styled-components';
interface Props {
center?: boolean;
ref?: RefObject<HTMLDivElement> | null; // The ref type provided by react-use is incorrect -> https://github.com/streamich/react-use/issues/1264 Open Issue
}
export const Container = styled.div<Props>`
height: 25vh !important;
margin-top: 1rem;
margin-bottom: 1rem;
width: 100% !important;
${({ center }): StyledCSS =>
center &&
css`
display: flex;
justify-content: center;
align-items: center;
`}
`;

View File

@@ -1,97 +0,0 @@
import {
Dispatch,
SetStateAction,
useCallback,
useMemo,
useState,
} from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Input } from '@signozhq/ui/input';
import { AutoComplete } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { getTagKeyOptions, onTagKeySelect } from './utils';
function TagsKey(props: TagsKeysProps): JSX.Element {
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { index, setLocalSelectedTags, tag } = props;
const [selectedKey, setSelectedKey] = useState<string>(tag.Key || '');
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const { isLoading, data } = useQuery(
[
'getTagKeys',
globalTime.minTime,
globalTime.maxTime,
traces.selectedFilter,
traces.isFilterExclude,
traces.spanKind,
],
{
queryFn: () =>
getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
spanKind: traces.spanKind,
}),
cacheTime: 120000,
},
);
const options = useMemo(() => getTagKeyOptions(data?.payload), [data]);
const onSelectHandler = useCallback(
(value: unknown) =>
onTagKeySelect(
value,
options,
setSelectedKey,
setLocalSelectedTags,
index,
tag,
),
[index, options, setLocalSelectedTags, tag],
);
return (
<AutoComplete
style={{ width: '100%' }}
value={selectedKey}
allowClear
disabled={isLoading}
notFoundContent="No tags available"
showSearch
options={options?.map((e) => ({
label: e.label?.toString(),
value: e.value,
}))}
filterOption={(inputValue, option): boolean =>
option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
onChange={(e): void => setSelectedKey(e)}
onSelect={onSelectHandler}
>
<Input placeholder="Please select" />
</AutoComplete>
);
}
interface TagsKeysProps {
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
}
export default TagsKey;

View File

@@ -1,154 +0,0 @@
import {
Dispatch,
memo,
SetStateAction,
useCallback,
useMemo,
useState,
} from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Select } from 'antd';
import type { BaseOptionType } from 'antd/es/select';
import getTagValue from 'api/trace/getTagValue';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { SelectComponent } from './styles';
import {
disableTagValue,
extractTagKey,
extractTagType,
getInitialLocalValue,
getTagValueOptions,
onTagValueChange,
selectOptions,
separateTagValues,
TagValueTypes,
} from './utils';
function TagValue(props: TagValueProps): JSX.Element {
const { tag, setLocalSelectedTags, index, tagKey } = props;
const {
Key: selectedKey,
Operator: selectedOperator,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
} = tag;
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [localTagValue, setLocalTagValue] = useState<TagValueTypes[]>(
getInitialLocalValue(
selectedNumberValues,
selectedBoolValues,
selectedStringValues,
),
);
const globalReducer = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const tagType = useMemo(() => extractTagType(tagKey), [tagKey]);
const { isLoading, data } = useQuery(
[
'tagKey',
globalReducer.minTime,
globalReducer.maxTime,
tagKey,
tagType,
traces.spanKind,
],
{
queryFn: () =>
getTagValue({
end: globalReducer.maxTime,
start: globalReducer.minTime,
tagKey: {
Key: extractTagKey(tagKey),
Type: tagType,
},
spanKind: traces.spanKind,
}),
},
);
const tagValueDisabled = useMemo(
() =>
disableTagValue(
selectedOperator,
setLocalTagValue,
selectedKey,
setLocalSelectedTags,
index,
),
[index, selectedKey, selectedOperator, setLocalSelectedTags],
);
const onChangeHandler = useCallback(
(value: unknown) => {
const updatedValues = onTagValueChange(value);
setLocalTagValue(updatedValues);
const { boolValues, numberValues, stringValues } = separateTagValues(
updatedValues,
selectedKey,
);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
...tags[index],
BoolValues: boolValues,
NumberValues: numberValues,
StringValues: stringValues,
},
...tags.slice(index + 1),
]);
},
[index, setLocalSelectedTags, selectedKey],
);
const getFilterOptions = useCallback(
(inputValue: string, option?: BaseOptionType): boolean => {
if (typeof option?.label === 'string') {
return option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
}
return false;
},
[],
);
return (
<SelectComponent
loading={isLoading}
options={getTagValueOptions(data?.payload, tagType)}
mode="tags"
allowClear
showSearch
filterOption={getFilterOptions}
disabled={isLoading || tagValueDisabled}
value={localTagValue}
onChange={onChangeHandler}
>
{selectOptions(data?.payload, tagType)?.map((suggestion) => (
<Select.Option key={suggestion.toString()} value={suggestion}>
{suggestion}
</Select.Option>
))}
</SelectComponent>
);
}
interface TagValueProps {
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
tagKey: string;
}
export default memo(TagValue);

View File

@@ -1,180 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { X } from '@signozhq/icons';
import { Select } from 'antd';
import { TraceReducer } from 'types/reducer/trace';
import { Container, IconContainer, SelectComponent } from './styles';
import TagsKey from './TagKey';
import TagValue from './TagValue';
import { mapOperators } from './utils';
const { Option } = Select;
type Tags = FlatArray<TraceReducer['selectedTags'], 1>['Operator'];
const StringBoolNumber = ['string', 'number', 'bool'];
const Number = ['number'];
const String = ['string'];
export interface AllMenuProps {
key: Tags | '';
value: string;
supportedTypes: string[];
}
export const AllMenu: AllMenuProps[] = [
{
key: 'Equals',
value: 'EQUALS',
supportedTypes: StringBoolNumber,
},
{
key: 'NotEquals',
value: 'NOT EQUALS',
supportedTypes: StringBoolNumber,
},
{
key: 'In',
value: 'IN',
supportedTypes: String,
},
{
key: 'NotIn',
value: 'NOT IN',
supportedTypes: String,
},
{
key: 'Exists',
value: 'EXISTS',
supportedTypes: StringBoolNumber,
},
{
key: 'NotExists',
value: 'NOT EXISTS',
supportedTypes: StringBoolNumber,
},
{
key: 'GreaterThan',
value: 'GREATER THAN',
supportedTypes: Number,
},
{
key: 'LessThan',
value: 'LESS THAN',
supportedTypes: Number,
},
{
key: 'GreaterThanEquals',
value: 'GREATER THAN OR EQUALS',
supportedTypes: Number,
},
{
key: 'LessThanEquals',
value: 'LESS THAN OR EQUALS',
supportedTypes: Number,
},
{
key: 'StartsWith',
value: 'STARTS WITH',
supportedTypes: String,
},
{
key: 'NotStartsWith',
value: 'NOT STARTS WITH',
supportedTypes: String,
},
{
key: 'Contains',
value: 'CONTAINS',
supportedTypes: String,
},
{
key: 'NotContains',
value: 'NOT CONTAINS',
supportedTypes: String,
},
];
function SingleTags(props: AllTagsProps): JSX.Element {
const { tag, onCloseHandler, setLocalSelectedTags, index, localSelectedTags } =
props;
const {
Key: selectedKey,
Operator: selectedOperator,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
} = tag;
const onDeleteTagHandler = (index: number): void => {
onCloseHandler(index);
};
const onChangeOperatorHandler = (key: unknown): void => {
if (typeof key === 'string') {
setLocalSelectedTags([
...localSelectedTags.slice(0, index),
{
Key: selectedKey,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
Operator: key as Tags,
},
...localSelectedTags.slice(index + 1, localSelectedTags.length),
]);
}
};
return (
<Container>
<TagsKey
index={index}
tag={tag}
setLocalSelectedTags={setLocalSelectedTags}
/>
<SelectComponent
onChange={onChangeOperatorHandler}
value={AllMenu.find((e) => e.key === selectedOperator)?.value || ''}
>
{
// filter out the operator that does not include supported type of the selected key
mapOperators(selectedKey).map((e) => (
<Option key={e.value} value={e.key}>
{e.value}
</Option>
))
}
</SelectComponent>
{selectedKey ? (
<TagValue
index={index}
tag={tag}
setLocalSelectedTags={setLocalSelectedTags}
tagKey={selectedKey}
/>
) : (
<SelectComponent />
)}
<IconContainer role="button" onClick={(): void => onDeleteTagHandler(index)}>
<X size="md" />
</IconContainer>
</Container>
);
}
interface AllTagsProps {
onCloseHandler: (index: number) => void;
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: Dispatch<SetStateAction<TraceReducer['selectedTags']>>;
localSelectedTags: TraceReducer['selectedTags'];
}
export interface Value {
key: string;
label: string;
value: string;
}
export default SingleTags;

View File

@@ -1,38 +0,0 @@
import { Select, Space } from 'antd';
import styled from 'styled-components';
export const SpaceComponent = styled(Space)`
&&& {
width: 100%;
}
`;
export const Container = styled(Space)`
&&& {
display: flex;
margin-top: 1rem;
margin-bottom: 1rem;
}
.ant-space-item:not(:last-child, :nth-child(2)) {
width: 100%;
}
.ant-space-item:nth-child(2) {
width: 50%;
}
`;
export const IconContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
margin-left: 1.125rem;
`;
export const SelectComponent = styled(Select)`
&&& {
width: 100%;
}
`;

View File

@@ -1,232 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { AutoCompleteProps } from 'antd';
import type { DefaultOptionType } from 'antd/es/select';
import { PayloadProps as TagKeyPayload } from 'types/api/trace/getTagFilters';
import { PayloadProps as TagValuePayload } from 'types/api/trace/getTagValue';
import { OperatorValues, Tags } from 'types/reducer/trace';
import { AllMenu, AllMenuProps } from '.';
export type TagValueTypes = string | number | boolean;
/**
* @description extract tag filters from payload
*/
export const extractTagFilters = (
payload: TagKeyPayload,
): DefaultOptionType[] => {
const tagFilters: string[] = [];
payload.stringTagKeys.forEach((element) => {
tagFilters.push(`${element}.(string)`);
});
payload.numberTagKeys.forEach((element) => {
tagFilters.push(`${element}.(number)`);
});
payload.boolTagKeys.forEach((element) => {
tagFilters.push(`${element}.(bool)`);
});
return tagFilters.map((e) => ({
value: e,
label: e,
}));
};
export const extractTagType = (tagKey: string): string => {
if (tagKey?.includes('.(string)')) {
return 'string';
}
if (tagKey?.includes('.(number)')) {
return 'number';
}
if (tagKey?.includes('.(bool)')) {
return 'bool';
}
return 'string';
};
export const extractTagKey = (tagKey: string): string => {
const tag = tagKey.split('.(');
if (tag && tag.length > 0) {
return tag[0];
}
return '';
};
export function onTagValueChange(values: unknown): TagValueTypes[] {
const stringValues = values as string[];
if (!Array.isArray(stringValues) || stringValues.length === 0) {
return [];
}
return values as TagValueTypes[];
}
export function separateTagValues(
values: TagValueTypes[],
selectedKey: string,
): { boolValues: boolean[]; numberValues: number[]; stringValues: string[] } {
if (selectedKey.includes('.(bool)')) {
const boolValues = values.filter(
(value) => typeof value === 'boolean',
) as boolean[];
return {
boolValues,
numberValues: [],
stringValues: [],
};
}
if (selectedKey.includes('.(number)')) {
const numberValues = values
.filter((value) => typeof value === 'number' || !Number.isNaN(Number(value)))
.map((value) => Number(value)) as number[];
return {
boolValues: [],
numberValues,
stringValues: [],
};
}
const stringValues = values.filter(
(value) =>
typeof value === 'string' &&
value !== 'true' &&
value !== 'false' &&
Number.isNaN(Number(value)),
) as string[];
return {
boolValues: [],
numberValues: [],
stringValues,
};
}
export function disableTagValue(
selectedOperator: OperatorValues,
setLocalValue: Dispatch<SetStateAction<TagValueTypes[]>>,
selectedKeys: string,
setLocalSelectedTags: Dispatch<SetStateAction<Tags[]>>,
index: number,
): boolean {
if (selectedOperator === 'Exists' || selectedOperator === 'NotExists') {
setLocalValue([]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKeys,
Operator: selectedOperator,
StringValues: [],
NumberValues: [],
BoolValues: [],
},
...tags.slice(index + 1, tags.length),
]);
return true;
}
return false;
}
export function getInitialLocalValue(
selectedNumberValues: number[],
selectedBoolValues: boolean[],
selectedStringValues: string[],
): TagValueTypes[] {
return [
...selectedBoolValues,
...selectedNumberValues,
...selectedStringValues,
];
}
export function getTagValueOptions(
payload: TagValuePayload | null | undefined,
tagType: string,
): Array<{ label: string; value: TagValueTypes }> | undefined {
if (tagType === 'string') {
return payload?.stringTagValues?.map((e) => ({
label: e,
value: e,
}));
}
if (tagType === 'number') {
return payload?.numberTagValues?.map((e) => ({
label: e.toString(),
value: e,
}));
}
if (tagType === 'bool') {
return payload?.boolTagValues?.map((e) => ({
label: e.toString(),
value: e,
}));
}
return [];
}
export function getTagKeyOptions(
payload: TagKeyPayload | null | undefined,
): DefaultOptionType[] {
if (payload === null) {
return [
{
value: '',
label: 'No tags available',
},
];
}
if (payload != null) {
return extractTagFilters(payload);
}
return [];
}
export function selectOptions(
payload: TagValuePayload | null | undefined,
tagType: string,
): string[] | boolean[] | number[] | undefined {
if (tagType === 'string') {
return payload?.stringTagValues;
}
if (tagType === 'number') {
return payload?.numberTagValues;
}
if (tagType === 'bool') {
return payload?.boolTagValues;
}
return [];
}
export function mapOperators(selectedKey: string): AllMenuProps[] {
return AllMenu.filter((e) =>
e?.supportedTypes?.includes(extractTagType(selectedKey)),
);
}
export function onTagKeySelect(
value: unknown,
options: AutoCompleteProps['options'],
setSelectedKey: Dispatch<SetStateAction<string>>,
setLocalSelectedTags: Dispatch<SetStateAction<Tags[]>>,
index: number,
tag: Tags,
): void {
if (
typeof value === 'string' &&
options &&
options.find((option) => option.value === value)
) {
setSelectedKey(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: value,
Operator: tag.Operator,
StringValues: tag.StringValues,
NumberValues: tag.NumberValues,
BoolValues: tag.BoolValues,
},
...tags.slice(index + 1, tags.length),
]);
}
}

View File

@@ -1,153 +0,0 @@
import { useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useSelector } from 'react-redux';
import { ChevronRight, Plus } from '@signozhq/icons';
import { Button, Space, Flex } from 'antd';
import { Typography } from '@signozhq/ui/typography';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
import { UpdateTagVisibility } from 'store/actions/trace/updateTagPanelVisiblity';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
import { parseTagsToQuery } from '../util';
import {
ButtonContainer,
Container,
CurrentTagsContainer,
ErrorContainer,
} from './styles';
import Tags from './Tag';
const { Text } = Typography;
function AllTags({
updateTagIsError,
onChangeHandler,
updateTagVisibility,
updateFilters,
}: AllTagsProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [localSelectedTags, setLocalSelectedTags] = useState<
TraceReducer['selectedTags']
>(traces.selectedTags);
const onTagAddHandler = (): void => {
setLocalSelectedTags((tags) => [
...tags,
{
Key: '',
Operator: 'Equals',
StringValues: [],
NumberValues: [],
BoolValues: [],
},
]);
};
const onCloseHandler = (index: number): void => {
setLocalSelectedTags([
...localSelectedTags.slice(0, index),
...localSelectedTags.slice(index + 1, localSelectedTags.length),
]);
};
const onRunQueryHandler = (): void => {
const parsedQuery = parseTagsToQuery(localSelectedTags);
if (parsedQuery.isError) {
updateTagIsError(true);
} else {
onChangeHandler(parsedQuery.payload);
updateFilters(localSelectedTags);
updateTagIsError(false);
updateTagVisibility(false);
}
};
const onResetHandler = (): void => {
setLocalSelectedTags([]);
};
if (traces.isTagModalError) {
return (
<ErrorContainer>
<Text style={{ color: 'var(--warning-background)' }}>
Unrecognized query format. Please reset your query by clicking `X` in the
search bar above.
</Text>
<Text style={{ color: 'var(--warning-background)' }}>
Please click on the search bar to get a drop down to select relevant tags
</Text>
</ErrorContainer>
);
}
return (
<Container>
<Typography>Tags</Typography>
<CurrentTagsContainer>
{localSelectedTags.map((tags, index) => (
<Tags
key={tags.Key}
tag={tags}
index={index}
onCloseHandler={(): void => onCloseHandler(index)}
setLocalSelectedTags={setLocalSelectedTags}
localSelectedTags={localSelectedTags}
/>
))}
</CurrentTagsContainer>
<Space wrap direction="horizontal">
<Button type="primary" onClick={onTagAddHandler}>
<Flex gap={4} align="center">
<Plus size="md" />
Add Tags Filter
</Flex>
</Button>
<Text truncate={1}>
Results will include spans with ALL the specified tags ( Rows are `ANDed` )
</Text>
</Space>
<ButtonContainer>
<Space align="start">
<Button onClick={onResetHandler}>Reset</Button>
<Button type="primary" onClick={onRunQueryHandler}>
<Flex gap={4} align="center">
<ChevronRight size="md" />
Run Query
</Flex>
</Button>
</Space>
</ButtonContainer>
</Container>
);
}
interface DispatchProps {
updateTagIsError: (value: boolean) => void;
updateTagVisibility: (value: boolean) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
updateTagVisibility: bindActionCreators(UpdateTagVisibility, dispatch),
});
interface AllTagsProps extends DispatchProps {
updateFilters: (tags: TraceReducer['selectedTags']) => void;
onChangeHandler: (search: string) => void;
}
export default connect(null, mapDispatchToProps)(AllTags);

View File

@@ -1,39 +0,0 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`
min-height: 20vh;
width: 100%;
min-width: 81.5vw;
`;
export const ErrorContainer = styled(Card)`
min-height: 20vh;
width: 100%;
z-index: 2;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;
export const ButtonContainer = styled(Card)`
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 11px !important;
padding-bottom: 11px !important;
padding-right: 38.01px !important;
margin-top: 1rem !important;
.ant-card-body {
padding: 0;
}
`;
export const CurrentTagsContainer = styled.div`
margin-bottom: 1rem;
`;

View File

@@ -1 +0,0 @@
export const contentStyle = { padding: 0 };

View File

@@ -1,152 +0,0 @@
import { useEffect, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useDispatch, useSelector } from 'react-redux';
import { ChevronRight } from '@signozhq/icons';
import { Flex, Popover } from 'antd';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
import { UpdateTagVisibility } from 'store/actions/trace/updateTagPanelVisiblity';
import { updateURL } from 'store/actions/trace/util';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
import Tags from './AllTags';
import { contentStyle } from './config';
import { Container, SearchComponent } from './styles';
import { parseQueryToTags, parseTagsToQuery } from './util';
function Search({
updateTagVisibility,
updateTagIsError,
}: SearchProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [value, setValue] = useState<string>('');
const dispatch = useDispatch<Dispatch<AppActions>>();
useEffect(() => {
if (!traces.filterLoading) {
const initialTags = parseTagsToQuery(traces.selectedTags);
if (!initialTags.isError) {
setValue(initialTags.payload);
}
}
}, [traces.selectedTags, traces.filterLoading]);
useEffect(() => {
if (value.length === 0 && traces.isTagModalError) {
updateTagIsError(false);
}
}, [traces.isTagModalError, value, updateTagIsError]);
const tagRef = useRef<HTMLDivElement>(null);
const onChangeHandler = (search: string): void => {
setValue(search);
};
const setIsTagsModalHandler = (value: boolean): void => {
updateTagVisibility(value);
};
const updateFilters = async (
selectedTags: TraceReducer['selectedTags'],
): Promise<void> => {
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
selectedTags,
current: traces.spansAggregate.currentPage,
filter: traces.filter,
filterToFetchData: traces.filterToFetchData,
selectedFilter: traces.selectedFilter,
userSelected: traces.userSelectedFilter,
isFilterExclude: traces.isFilterExclude,
order: traces.spansAggregate.order,
pageSize: traces.spansAggregate.pageSize,
orderParam: traces.spansAggregate.orderParam,
spanKind: traces.spanKind,
},
});
updateURL(
traces.selectedFilter,
traces.filterToFetchData,
traces.spansAggregate.currentPage,
selectedTags,
traces.isFilterExclude,
traces.userSelectedFilter,
traces.spansAggregate.order,
traces.spansAggregate.pageSize,
traces.spansAggregate.orderParam,
);
};
return (
<Container ref={tagRef}>
<Popover
placement="bottomLeft"
destroyTooltipOnHide
open={traces.isTagModalOpen}
trigger="click"
onOpenChange={setIsTagsModalHandler}
showArrow={false}
overlayInnerStyle={contentStyle}
content={
<Tags updateFilters={updateFilters} onChangeHandler={onChangeHandler} />
}
>
<SearchComponent
onChange={(event): void => onChangeHandler(event.target.value)}
value={value}
allowClear
disabled={traces.filterLoading}
placeholder="Click to filter by tags"
type="search"
enterButton={
<Flex align="center">
<ChevronRight size="md" />
</Flex>
}
onSearch={(string): void => {
if (string.length === 0) {
updateTagVisibility(false);
updateFilters([]);
return;
}
const { isError, payload } = parseQueryToTags(string);
if (isError) {
updateTagIsError(true);
} else {
updateTagIsError(false);
updateTagVisibility(false);
updateFilters(payload);
}
}}
/>
</Popover>
</Container>
);
}
interface DispatchProps {
updateTagVisibility: (value: boolean) => void;
updateTagIsError: (value: boolean) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTagVisibility: bindActionCreators(UpdateTagVisibility, dispatch),
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
});
type SearchProps = DispatchProps;
export default connect(null, mapDispatchToProps)(Search);

View File

@@ -1,19 +0,0 @@
import { Input } from 'antd';
import styled from 'styled-components';
const { Search } = Input;
export const Container = styled.div`
display: flex;
position: relative;
width: 100%;
margin-top: 1rem;
`;
export const SearchComponent = styled(Search)`
.ant-btn-primary {
svg {
transform: scale(1.5);
}
}
`;

View File

@@ -1,158 +0,0 @@
import { AllMenu } from 'container/Trace/Search/AllTags/Tag';
import { TraceReducer } from 'types/reducer/trace';
import { extractTagType, TagValueTypes } from './AllTags/Tag/utils';
type Tags = TraceReducer['selectedTags'];
interface PayloadProps<T> {
isError: boolean;
payload: T;
}
function extractValues(
tagType: string,
filters: string[],
isError: boolean,
): [number[], boolean[], string[], boolean] {
const StringValues: string[] = [];
const NumberValues: number[] = [];
const BoolValues: boolean[] = [];
let isErr = isError;
if (tagType === 'string') {
StringValues.push(...filters);
} else if (tagType === 'number') {
filters.forEach((element) => {
const num = Number(element);
isErr = Number.isNaN(num) ? true : isError;
NumberValues.push(num);
});
} else if (tagType === 'bool') {
filters.forEach((element) => {
if (element === 'true') {
BoolValues.push(true);
} else if (element === 'false') {
BoolValues.push(false);
} else {
isErr = true;
}
});
}
return [NumberValues, BoolValues, StringValues, isErr];
}
export const parseQueryToTags = (query: string): PayloadProps<Tags> => {
let isError = false;
// Split the query string by ' AND '
const noOfTags = query.split(' AND ');
// Map over each tag
const tags: Tags = noOfTags.map((filter) => {
// Find the operator used in the filter
const operator =
AllMenu.find((e) => `${filter} `.includes(` ${e.key} `))?.key || '';
// Split the filter by the operator
const [tagName, tagValues] = filter.split(operator).map((e) => e.trim());
// If the operator is Exists or NotExists, then return the tag object without values
if (operator === 'Exists' || operator === 'NotExists') {
return {
Key: tagName,
StringValues: [],
NumberValues: [],
BoolValues: [],
Operator: operator as FlatArray<Tags, 1>['Operator'],
};
}
// Check for errors in the filter
isError = operator.length === 0 || !tagName || !tagValues ? true : isError;
// Remove the first and last brackets from the tagValues
const formattedTagValues = tagValues.slice(1, -1);
// Split the tagValues by ',' and remove any quotes
const filters = formattedTagValues
.split(',')
.map((e) => e.replaceAll(/"/g, ''));
// Check for errors in the filters
filters.forEach((e) => {
const firstChar = e.charAt(0);
const lastChar = e.charAt(e.length - 1);
isError = firstChar === '"' && lastChar === '"' ? true : isError;
});
// Extract the tag type
const tagType = extractTagType(tagName);
// Extract the values for the tag
const [NumberValues, BoolValues, StringValues, isErr] = extractValues(
tagType,
filters,
isError,
);
isError = isErr;
// Return the tag object
return {
Key: tagName,
StringValues,
NumberValues,
BoolValues,
Operator: operator as FlatArray<Tags, 1>['Operator'],
};
});
return {
isError,
payload: tags,
};
};
const formatValues = (values: TagValueTypes[]): string =>
values.map((e) => `"${e.toString().replaceAll(/"/g, '')}"`).join(',');
export const parseTagsToQuery = (tags: Tags): PayloadProps<string> => {
let isError = false;
// Map over each tag
const payload = tags
.map(({ StringValues, NumberValues, BoolValues, Key, Operator }) => {
// Check if the key of the tag is undefined
if (!Key) {
isError = true;
}
if (Operator === 'Exists' || Operator === 'NotExists') {
return `${Key} ${Operator}`;
}
// Check if the tag has string values
if (StringValues.length > 0) {
// Format the string values and join them with a ','
const formattedStringValues = formatValues(StringValues);
return `${Key} ${Operator} (${formattedStringValues})`;
}
// Check if the tag has number values
if (NumberValues.length > 0) {
// Format the number values and join them with a ','
const formattedNumberValues = formatValues(NumberValues);
return `${Key} ${Operator} (${formattedNumberValues})`;
}
// Check if the tag has boolean values
if (BoolValues.length > 0) {
// Format the boolean values and join them with a ','
const formattedBoolValues = formatValues(BoolValues);
return `${Key} ${Operator} (${formattedBoolValues})`;
}
return '';
})
.join(' AND ');
return {
isError,
payload,
};
};

View File

@@ -1,107 +0,0 @@
import type { DefaultOptionType } from 'antd/es/select';
interface Dropdown {
key: string;
displayValue: string;
yAxisUnit?: string;
}
export const groupBy: DefaultOptionType[] = [
{
label: 'None',
value: 'none',
},
{
label: 'Service Name',
value: 'serviceName',
},
{
label: 'Operation',
value: 'name',
},
{
label: 'HTTP URL',
value: 'httpUrl',
},
{
label: 'HTTP Method',
value: 'httpMethod',
},
{
label: 'HTTP Host',
value: 'httpHost',
},
{
label: 'HTTP Route',
value: 'httpRoute',
},
{
label: 'RPC Method',
value: 'rpcMethod',
},
{
label: 'Status Code',
value: 'responseStatusCode',
},
{
label: 'Database Name',
value: 'dbName',
},
{
label: 'Database System',
value: 'dbSystem',
},
{
label: 'Database Operation',
value: 'dbOperation',
},
{
label: 'Messaging System',
value: 'msgSystem',
},
{
label: 'Messaging Operation',
value: 'msgOperation',
},
];
export const functions: Dropdown[] = [
{ displayValue: 'Count', key: 'count', yAxisUnit: 'short' },
{
displayValue: 'Rate per sec',
key: 'ratePerSec',
yAxisUnit: 'reqps',
},
{ displayValue: 'Sum (duration)', key: 'sum', yAxisUnit: 'ns' },
{ displayValue: 'Avg (duration)', key: 'avg', yAxisUnit: 'ns' },
{
displayValue: 'Max (duration)',
key: 'max',
yAxisUnit: 'ns',
},
{
displayValue: 'Min (duration)',
key: 'min',
yAxisUnit: 'ns',
},
{
displayValue: '50th percentile (duration)',
key: 'p50',
yAxisUnit: 'ns',
},
{
displayValue: '90th percentile (duration)',
key: 'p90',
yAxisUnit: 'ns',
},
{
displayValue: '95th percentile (duration)',
key: 'p95',
yAxisUnit: 'ns',
},
{
displayValue: '99th percentile (duration)',
key: 'p99',
yAxisUnit: 'ns',
},
];

View File

@@ -1,102 +0,0 @@
import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Input } from '@signozhq/ui/input';
import { AutoComplete, Space } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { popupContainer } from 'utils/selectPopupContainer';
import { functions } from './config';
import { SelectComponent } from './styles';
import {
filterGroupBy,
getSelectedValue,
initOptions,
onClickSelectedFunctionHandler,
onClickSelectedGroupByHandler,
selectedGroupByValue,
} from './utils';
const { Option } = SelectComponent;
function TraceGraphFilter(): JSX.Element {
const { selectedFunction, selectedGroupBy } = useSelector<
AppState,
TraceReducer
>((state) => state.traces);
const [selectedGroupByLocal, setSelectedGroupByLocal] =
useState<string>(selectedGroupBy);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const { isLoading, data } = useQuery(
[
'getTagKeys',
globalTime.minTime,
globalTime.maxTime,
traces.selectedFilter,
traces.isFilterExclude,
traces.spanKind,
],
{
queryFn: () =>
getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
spanKind: traces.spanKind,
}),
cacheTime: 120000,
enabled: traces.filter.size > 0,
},
);
const options = useMemo(() => initOptions(data?.payload), [data?.payload]);
return (
<Space>
<label htmlFor="selectedFunction">Function</label>
<SelectComponent
getPopupContainer={popupContainer}
dropdownMatchSelectWidth
data-testid="selectedFunction"
id="selectedFunction"
value={getSelectedValue(selectedFunction)}
onChange={onClickSelectedFunctionHandler}
>
{functions.map((value) => (
<Option value={value.key} key={value.key}>
{value.displayValue}
</Option>
))}
</SelectComponent>
<label htmlFor="selectedGroupBy">Group By</label>
<AutoComplete
getPopupContainer={popupContainer}
dropdownMatchSelectWidth
id="selectedGroupBy"
data-testid="selectedGroupBy"
options={options}
value={selectedGroupByValue(selectedGroupByLocal, options)}
onChange={(e): void => setSelectedGroupByLocal(e.toString())}
onSelect={onClickSelectedGroupByHandler(options)}
filterOption={(inputValue, option): boolean =>
filterGroupBy(inputValue, option)
}
>
<Input disabled={isLoading} placeholder="Please select" />
</AutoComplete>
</Space>
);
}
export default TraceGraphFilter;

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