Compare commits

..

12 Commits

Author SHA1 Message Date
Naman Verma
a9ed313681 Merge branch 'main' into nv/remove-v1-dashboard 2026-09-21 22:52:23 +05:30
Naman Verma
ee97363179 chore: remove v1 dashboards code from backend 2026-09-21 22:49:00 +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
Nityananda Gohain
ea8f95ee08 chore: enable ai 011y processors by default (#12912)
Some checks failed
build-staging / prepare (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
<!--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
Enable the processors by default so that metadata is populated

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
No issue
2026-09-19 13:02:43 +00:00
Nityananda Gohain
b64116d67d feat: support for default attribute mapping (#12809)
<!--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

Until now every org started with no span mapper groups and we had to
create `llm`, `agent` and `tool` by hand. This PR ships them as
defaults.

- The three groups live as JSON in the binary. On startup and when a new
org is created, they get seeded. When we change a definition and bump
its version, the next release updates the org's copy in place.
- Anything SigNoz ships is marked `origin: system` and can only be
switched on or off. Users can't rename or delete these groups and
mappers, and can't take their names. Anything the user adds is theirs to
edit or remove, including new mappers in a shipped group or new sources
on a shipped mapper.
  - Upgrades keep the user's on/off choices and never touch their items.
- Condition substrings and sources now carry `enabled` and `origin`, so
a substring is an object instead of a plain string. Disabled ones are
left out of the collector config.
- - Migration 127 only adds the `origin` and `version` columns. Stored
JSON is not rewritten because these tables are empty on every instance.
- Fixes creating a group or mapper with `enabled: false` being saved as
true (the bun `default:true` tag turned false into SQL `DEFAULT`)
  
Frontend: no UI changes. Generated client regenerated; drafts carry
`enabled` and `origin` so saves from the existing screens round-trip
shipped items intact.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Closes https://github.com/SigNoz/engineering-pod/issues/5329


<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information
* Frontend follow-up: toggles for sources and substrings, "Default"
badge and read-only rows for shipped items, hide rename/delete on system
groups and mappers.
* Deferred: per-group upgrade changelog ( will come back to this later)

---------

Co-authored-by: Gaurav Tewari <gauravtewari111@gmail.com>
Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-19 11:22:46 +00:00
370 changed files with 8975 additions and 12351 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:
@@ -3407,29 +3454,6 @@ components:
required:
- customValue
type: object
DashboardtypesDashboard:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
data:
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
id:
type: string
locked:
type: boolean
org_id:
type: string
source:
$ref: '#/components/schemas/DashboardtypesSource'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
type: object
DashboardtypesDashboardPanelRef:
properties:
dashboardId:
@@ -3552,6 +3576,11 @@ components:
- gradient
- none
type: string
DashboardtypesFillOpacity:
maximum: 1
minimum: 0
nullable: true
type: number
DashboardtypesGettableDashboardV2:
properties:
createdAt:
@@ -3604,13 +3633,6 @@ components:
timeRangeEnabled:
type: boolean
type: object
DashboardtypesGettablePublicDashboardData:
properties:
dashboard:
$ref: '#/components/schemas/DashboardtypesDashboard'
publicDashboard:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
type: object
DashboardtypesGettablePublicDashboardDataV2:
properties:
dashboard:
@@ -4014,6 +4036,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 +4049,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 +4061,7 @@ components:
enum:
- signoz/TimeSeriesPanel
- signoz/BarChartPanel
- signoz/AreaChartPanel
- signoz/NumberPanel
- signoz/PieChartPanel
- signoz/TablePanel
@@ -4044,6 +4069,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,9 +4411,12 @@ components:
are connected.
type: boolean
type: object
DashboardtypesStorableDashboardData:
additionalProperties: {}
type: object
DashboardtypesStackMode:
enum:
- none
- normal
- percent
type: string
DashboardtypesTableFormatting:
properties:
columnUnits:
@@ -7152,22 +7192,6 @@ components:
- attributes
- totalKeys
type: object
MetricsexplorertypesMetricDashboard:
properties:
dashboardId:
type: string
dashboardName:
type: string
widgetId:
type: string
widgetName:
type: string
required:
- dashboardName
- dashboardId
- widgetId
- widgetName
type: object
MetricsexplorertypesMetricDashboardPanelsResponse:
properties:
dashboards:
@@ -7178,16 +7202,6 @@ components:
required:
- dashboards
type: object
MetricsexplorertypesMetricDashboardsResponse:
properties:
dashboards:
items:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
nullable: true
type: array
required:
- dashboards
type: object
MetricsexplorertypesMetricHighlightsResponse:
properties:
activeTimeSeries:
@@ -9732,6 +9746,8 @@ components:
type: string
name:
type: string
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
updatedAt:
format: date-time
type: string
@@ -9744,6 +9760,7 @@ components:
- fieldContext
- config
- enabled
- origin
type: object
SpantypesSpanMapperConfig:
properties:
@@ -9772,48 +9789,75 @@ components:
type: string
orgId:
type: string
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
version:
type: integer
required:
- id
- orgId
- name
- condition
- enabled
- origin
- version
type: object
SpantypesSpanMapperGroupCondition:
nullable: true
properties:
attributes:
items:
type: string
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
nullable: true
type: array
resource:
items:
type: string
$ref: '#/components/schemas/SpantypesSpanMapperGroupConditionKey'
nullable: true
type: array
required:
- attributes
- resource
type: object
SpantypesSpanMapperGroupConditionKey:
properties:
enabled:
type: boolean
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
value:
type: string
required:
- value
- enabled
type: object
SpantypesSpanMapperOperation:
enum:
- move
- copy
type: string
SpantypesSpanMapperOrigin:
enum:
- user
- system
type: string
SpantypesSpanMapperSource:
properties:
context:
$ref: '#/components/schemas/SpantypesFieldContext'
enabled:
type: boolean
key:
type: string
operation:
$ref: '#/components/schemas/SpantypesSpanMapperOperation'
origin:
$ref: '#/components/schemas/SpantypesSpanMapperOrigin'
priority:
type: integer
required:
@@ -9821,6 +9865,7 @@ components:
- context
- operation
- priority
- enabled
type: object
SpantypesSpanMapperTestSpan:
properties:
@@ -11071,9 +11116,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- cloud-integration:read
- tokenizer:
- VIEWER
- cloud-integration:read
summary: Agent check-in
tags:
- cloudintegration
@@ -11123,9 +11168,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:list
- tokenizer:
- ADMIN
- cloud-integration:list
summary: List accounts
tags:
- cloudintegration
@@ -11180,9 +11225,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:create
- tokenizer:
- ADMIN
- cloud-integration:create
summary: Create account
tags:
- cloudintegration
@@ -11225,9 +11270,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:delete
- tokenizer:
- ADMIN
- cloud-integration:delete
summary: Disconnect account
tags:
- cloudintegration
@@ -11293,9 +11338,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:read
- tokenizer:
- ADMIN
- cloud-integration:read
summary: Get account
tags:
- cloudintegration
@@ -11342,9 +11387,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:update
- tokenizer:
- ADMIN
- cloud-integration:update
summary: Update account
tags:
- cloudintegration
@@ -11400,9 +11445,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
@@ -11475,9 +11520,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
@@ -11529,9 +11574,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- ADMIN
- cloud-integration:update
- tokenizer:
- ADMIN
- cloud-integration:update
summary: Update service
tags:
- cloudintegration
@@ -11586,9 +11631,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- VIEWER
- cloud-integration:read
- tokenizer:
- VIEWER
- cloud-integration:read
summary: Agent check-in
tags:
- cloudintegration
@@ -11639,9 +11684,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
@@ -11691,10 +11744,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
@@ -11749,10 +11800,8 @@ paths:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
- api_key: []
- tokenizer: []
summary: Get service
tags:
- cloudintegration
@@ -13195,112 +13244,6 @@ paths:
summary: Update org preference
tags:
- preferences
/api/v1/public/dashboards/{id}:
get:
deprecated: false
description: This endpoint returns the sanitized dashboard data for public access
operationId: GetPublicDashboardData
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardData'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- anonymous:
- public-dashboard:read
summary: Get public dashboard data
tags:
- dashboard
/api/v1/public/dashboards/{id}/widgets/{idx}/query_range:
get:
deprecated: false
description: This endpoint return query range results for a widget of public
dashboard
operationId: GetPublicDashboardWidgetQueryRange
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: idx
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- anonymous:
- public-dashboard:read
summary: Get query range result
tags:
- dashboard
/api/v1/roles:
get:
deprecated: false
@@ -19552,74 +19495,6 @@ paths:
summary: Get metric attributes
tags:
- metrics
/api/v2/metrics/dashboards:
get:
deprecated: false
description: This endpoint returns associated dashboards for a specified metric
operationId: GetMetricDashboards
parameters:
- description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
in: query
name: metricName
required: true
schema:
description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboardsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get metric dashboards
tags:
- metrics
/api/v2/metrics/highlights:
get:
deprecated: false

View File

@@ -52,7 +52,7 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
dashboard, err := module.GetV2(ctx, orgID, publicDashboard.DashboardID)
if err != nil {
return err
}
@@ -90,15 +90,6 @@ func (module *module) GetPublic(ctx context.Context, orgID valuer.UUID, dashboar
return dashboardtypes.NewPublicDashboardFromStorablePublicDashboard(storablePublicDashboard), nil
}
func (module *module) GetDashboardByPublicID(ctx context.Context, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
return nil, err
}
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
}
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
orgIDs := make([]string, len(orgs))
for idx, org := range orgs {
@@ -116,24 +107,6 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
}, storableDashboard.OrgID, nil
}
func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.UUID, widgetIdx, startTime, endTime uint64) (*querybuildertypesv5.QueryRangeResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "dashboard",
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRange",
})
dashboard, err := module.GetDashboardByPublicID(ctx, id)
if err != nil {
return nil, err
}
query, err := dashboard.GetWidgetQuery(startTime, endTime, widgetIdx, module.settings.Logger())
if err != nil {
return nil, err
}
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
@@ -189,7 +162,7 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
dashboard, err := module.GetV2(ctx, orgID, publicDashboard.DashboardID)
if err != nil {
return err
}
@@ -200,34 +173,13 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
}
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
dashboard, err := module.Get(ctx, orgID, id)
if err != nil {
return err
}
if err := dashboard.ErrIfNotDeletable(); err != nil {
return err
}
if dashboard.Locked {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
}
return module.delete(ctx, orgID, id)
}
func (module *module) DeleteUnsafe(ctx context.Context, orgID, id valuer.UUID) error {
return module.delete(ctx, orgID, id)
}
func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, dashboardID)
dashboard, err := module.GetV2(ctx, orgID, dashboardID)
if err != nil {
return err
}
@@ -260,10 +212,6 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return stats, nil
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, source, data)
}
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, source, postable)
}
@@ -346,30 +294,10 @@ func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valu
return module.pkgDashboardModule.DeleteView(ctx, orgID, id)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
return module.pkgDashboardModule.GetByMetricNames(ctx, orgID, metricNames)
}
func (module *module) GetByMetricNamesV2(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
return module.pkgDashboardModule.GetByMetricNamesV2(ctx, orgID, metricNames)
}
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.List(ctx, orgID)
}
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Update(ctx, orgID, id, updatedBy, data, diff)
}
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) ReconcileSystemDashboards(ctx context.Context, orgID valuer.UUID) error {
return module.pkgDashboardModule.ReconcileSystemDashboards(ctx, orgID)
}

View File

@@ -462,7 +462,7 @@ func (m *module) relatedAssetImpact(ctx context.Context, orgID valuer.UUID, metr
droppedSet[label] = struct{}{}
}
if dashboards, err := m.dashboard.GetByMetricNames(ctx, orgID, []string{metricName}); err != nil {
if dashboards, err := m.dashboard.GetByMetricNamesV2(ctx, orgID, []string{metricName}); err != nil {
m.logger.WarnContext(ctx, "failed to fetch related dashboards for reduction preview", slog.String("metric_name", metricName), errors.Attr(err))
} else {
for _, item := range dashboards[metricName] {

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",
@@ -43,6 +42,7 @@
"NOT_FOUND": "SigNoz | Page Not Found",
"LOGS": "SigNoz | Logs",
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
"OLD_LOGS_EXPLORER": "SigNoz | Old Logs Explorer",
"LIVE_LOGS": "SigNoz | Live Logs",
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
"HOME_PAGE": "Open source Observability Platform | SigNoz",
@@ -78,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,20 +1588,19 @@ 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 },
LOGS_EXPLORER: { path: ROUTES.LOGS_EXPLORER, deniedRoles: DENIED_ROLES },
LIVE_LOGS: { path: ROUTES.LIVE_LOGS, deniedRoles: DENIED_ROLES },
OLD_LOGS_EXPLORER: {
path: ROUTES.OLD_LOGS_EXPLORER,
deniedRoles: DENIED_ROLES,
},
METRICS_EXPLORER: {
path: ROUTES.METRICS_EXPLORER,
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(
@@ -169,6 +158,10 @@ export const LogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsModulePage'),
);
export const OldLogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/Logs'),
);
export const LiveLogs = Loadable(
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
);

View File

@@ -32,6 +32,7 @@ import {
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
OldLogsExplorer,
OnboardingV2,
OrgOnboarding,
PasswordReset,
@@ -46,9 +47,7 @@ import {
SomethingWentWrong,
StatusPage,
SupportPage,
TraceDetailOldRedirect,
TraceDetailV3,
TraceFilter,
TracesExplorer,
TracesFunnelDetails,
TracesFunnels,
@@ -131,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,
@@ -223,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,
@@ -307,6 +291,13 @@ const routes: AppRoutes[] = [
key: 'LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.OLD_LOGS_EXPLORER,
exact: true,
component: OldLogsExplorer,
key: 'OLD_LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.LIVE_LOGS,
exact: true,

View File

@@ -36,16 +36,12 @@ import type {
GetDashboardV2200,
GetDashboardV2PathParameters,
GetPublicDashboard200,
GetPublicDashboardData200,
GetPublicDashboardDataPathParameters,
GetPublicDashboardDataV2200,
GetPublicDashboardDataV2PathParameters,
GetPublicDashboardPanelQueryRangeV2200,
GetPublicDashboardPanelQueryRangeV2Params,
GetPublicDashboardPanelQueryRangeV2PathParameters,
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
GetSystemDashboard200,
GetSystemDashboardPathParameters,
ListDashboardViews200,
@@ -474,217 +470,6 @@ export const useUpdatePublicDashboard = <
> => {
return useMutation(getUpdatePublicDashboardMutationOptions(options));
};
/**
* This endpoint returns the sanitized dashboard data for public access
* @summary Get public dashboard data
*/
export const getPublicDashboardData = (
{ id }: GetPublicDashboardDataPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardData200>({
url: `/api/v1/public/dashboards/${id}`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardDataQueryKey = ({
id,
}: GetPublicDashboardDataPathParameters) => {
return [`/api/v1/public/dashboards/${id}`] as const;
};
export const getGetPublicDashboardDataQueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardData>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetPublicDashboardDataQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardData>>
> = ({ signal }) => getPublicDashboardData({ id }, signal);
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardDataQueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardData>>
>;
export type GetPublicDashboardDataQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get public dashboard data
*/
export function useGetPublicDashboardData<
TData = Awaited<ReturnType<typeof getPublicDashboardData>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardDataQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get public dashboard data
*/
export const invalidateGetPublicDashboardData = async (
queryClient: QueryClient,
{ id }: GetPublicDashboardDataPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardDataQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint return query range results for a widget of public dashboard
* @summary Get query range result
*/
export const getPublicDashboardWidgetQueryRange = (
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardWidgetQueryRange200>({
url: `/api/v1/public/dashboards/${id}/widgets/${idx}/query_range`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardWidgetQueryRangeQueryKey = ({
id,
idx,
}: GetPublicDashboardWidgetQueryRangePathParameters) => {
return [`/api/v1/public/dashboards/${id}/widgets/${idx}/query_range`] as const;
};
export const getGetPublicDashboardWidgetQueryRangeQueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetPublicDashboardWidgetQueryRangeQueryKey({ id, idx });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>
> = ({ signal }) => getPublicDashboardWidgetQueryRange({ id, idx }, signal);
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined && idx !== null && idx !== undefined,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardWidgetQueryRangeQueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>
>;
export type GetPublicDashboardWidgetQueryRangeQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get query range result
*/
export function useGetPublicDashboardWidgetQueryRange<
TData = Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardWidgetQueryRangeQueryOptions(
{ id, idx },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get query range result
*/
export const invalidateGetPublicDashboardWidgetQueryRange = async (
queryClient: QueryClient,
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardWidgetQueryRangeQueryKey({ id, idx }) },
options,
);
return queryClient;
};
/**
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
* @summary List dashboard saved views

View File

@@ -24,8 +24,6 @@ import type {
GetMetricAlertsParams,
GetMetricAttributes200,
GetMetricAttributesParams,
GetMetricDashboards200,
GetMetricDashboardsParams,
GetMetricDashboardsV2200,
GetMetricDashboardsV2Params,
GetMetricHighlights200,
@@ -1096,104 +1094,6 @@ export const invalidateGetMetricAttributes = async (
return queryClient;
};
/**
* This endpoint returns associated dashboards for a specified metric
* @summary Get metric dashboards
*/
export const getMetricDashboards = (
params: GetMetricDashboardsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricDashboards200>({
url: `/api/v2/metrics/dashboards`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricDashboardsQueryKey = (
params?: GetMetricDashboardsParams,
) => {
return [`/api/v2/metrics/dashboards`, ...(params ? [params] : [])] as const;
};
export const getGetMetricDashboardsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricDashboards>>
> = ({ signal }) => getMetricDashboards(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricDashboardsQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricDashboards>>
>;
export type GetMetricDashboardsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get metric dashboards
*/
export function useGetMetricDashboards<
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricDashboardsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get metric dashboards
*/
export const invalidateGetMetricDashboards = async (
queryClient: QueryClient,
params: GetMetricDashboardsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricDashboardsQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric
* @summary Get metric highlights

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
@@ -4814,50 +4886,6 @@ export interface DashboardtypesCustomVariableSpecDTO {
customValue: string;
}
export interface DashboardtypesStorableDashboardDataDTO {
[key: string]: unknown;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
integration = 'integration',
}
export interface DashboardtypesDashboardDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
data?: DashboardtypesStorableDashboardDataDTO;
/**
* @type string
*/
id?: string;
/**
* @type boolean
*/
locked?: boolean;
/**
* @type string
*/
org_id?: string;
source?: DashboardtypesSourceDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface DashboardtypesDashboardPanelRefDTO {
/**
* @type string
@@ -4930,29 +4958,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 +5010,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 +5227,7 @@ export interface DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDa
export type DashboardtypesPanelPluginDTO =
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTimeSeriesPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesBarChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesAreaChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesNumberPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesPieChartPanelSpecDTO
| DashboardtypesPanelPluginVariantGithubComSigNozSignozPkgTypesDashboardtypesTablePanelSpecDTO
@@ -5806,6 +5824,11 @@ export interface DashboardtypesDashboardViewDTO {
updatedAt?: string;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
integration = 'integration',
}
export interface TagtypesGettableTagDTO {
/**
* @type string
@@ -5883,11 +5906,6 @@ export interface DashboardtypesGettablePublicDasbhboardDTO {
timeRangeEnabled?: boolean;
}
export interface DashboardtypesGettablePublicDashboardDataDTO {
dashboard?: DashboardtypesDashboardDTO;
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export interface DashboardtypesGettablePublicDashboardDataV2DTO {
dashboard?: DashboardtypesGettableDashboardV2DTO;
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
@@ -6134,6 +6152,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',
@@ -8959,25 +8978,6 @@ export interface MetricsexplorertypesMetricAttributesResponseDTO {
totalKeys: number;
}
export interface MetricsexplorertypesMetricDashboardDTO {
/**
* @type string
*/
dashboardId: string;
/**
* @type string
*/
dashboardName: string;
/**
* @type string
*/
widgetId: string;
/**
* @type string
*/
widgetName: string;
}
export interface MetricsexplorertypesMetricDashboardPanelsResponseDTO {
/**
* @type array,null
@@ -8985,13 +8985,6 @@ export interface MetricsexplorertypesMetricDashboardPanelsResponseDTO {
dashboards: DashboardtypesDashboardPanelRefDTO[] | null;
}
export interface MetricsexplorertypesMetricDashboardsResponseDTO {
/**
* @type array,null
*/
dashboards: MetricsexplorertypesMetricDashboardDTO[] | null;
}
export interface MetricsexplorertypesMetricHighlightsResponseDTO {
/**
* @type integer
@@ -10817,6 +10810,22 @@ export interface SpantypesGettableFlamegraphTraceDTO {
startTimestampMillis: number;
}
export enum SpantypesSpanMapperOriginDTO {
user = 'user',
system = 'system',
}
export interface SpantypesSpanMapperGroupConditionKeyDTO {
/**
* @type boolean
*/
enabled: boolean;
origin?: SpantypesSpanMapperOriginDTO;
/**
* @type string
*/
value: string;
}
/**
* @nullable
*/
@@ -10824,11 +10833,11 @@ export type SpantypesSpanMapperGroupConditionDTO = {
/**
* @type array,null
*/
attributes: string[] | null;
attributes: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
/**
* @type array,null
*/
resource: string[] | null;
resource: SpantypesSpanMapperGroupConditionKeyDTO[] | null;
} | null;
export interface SpantypesSpanMapperGroupDTO {
@@ -10858,6 +10867,7 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
*/
orgId: string;
origin: SpantypesSpanMapperOriginDTO;
/**
* @type string
* @format date-time
@@ -10867,6 +10877,10 @@ export interface SpantypesSpanMapperGroupDTO {
* @type string
*/
updatedBy?: string;
/**
* @type integer
*/
version: number;
}
export interface SpantypesGettableSpanMapperGroupsDTO {
@@ -10924,11 +10938,16 @@ export enum SpantypesSpanMapperOperationDTO {
}
export interface SpantypesSpanMapperSourceDTO {
context: SpantypesFieldContextDTO;
/**
* @type boolean
*/
enabled: boolean;
/**
* @type string
*/
key: string;
operation: SpantypesSpanMapperOperationDTO;
origin?: SpantypesSpanMapperOriginDTO;
/**
* @type integer
*/
@@ -10970,6 +10989,7 @@ export interface SpantypesSpanMapperDTO {
* @type string
*/
name: string;
origin: SpantypesSpanMapperOriginDTO;
/**
* @type string
* @format date-time
@@ -12398,29 +12418,6 @@ export type GetOrgPreference200 = {
export type UpdateOrgPreferencePathParameters = {
name: string;
};
export type GetPublicDashboardDataPathParameters = {
id: string;
};
export type GetPublicDashboardData200 = {
data: DashboardtypesGettablePublicDashboardDataDTO;
/**
* @type string
*/
status: string;
};
export type GetPublicDashboardWidgetQueryRangePathParameters = {
id: string;
idx: string;
};
export type GetPublicDashboardWidgetQueryRange200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status: string;
};
export type ListRoles200 = {
/**
* @type array
@@ -13378,22 +13375,6 @@ export type GetMetricAttributes200 = {
status: string;
};
export type GetMetricDashboardsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricDashboards200 = {
data: MetricsexplorertypesMetricDashboardsResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricHighlightsParams = {
/**
* @type string

View File

@@ -0,0 +1,23 @@
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/logs/addToSelectedFields';
const addToSelectedFields = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.post(`/logs/fields`, props);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return Promise.reject(ErrorResponseHandler(error as AxiosError));
}
};
export default addToSelectedFields;

View File

@@ -0,0 +1,26 @@
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/logs/getLogs';
const GetLogs = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.get(`/logs`, {
params: props,
});
return {
statusCode: 200,
error: null,
message: '',
payload: data.data.results,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetLogs;

View File

@@ -0,0 +1,26 @@
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/logs/getLogsAggregate';
const GetLogsAggregate = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.get(`/logs/aggregate`, {
params: props,
});
return {
statusCode: 200,
error: null,
message: '',
payload: data.data.items,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetLogsAggregate;

View File

@@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/logs/getSearchFields';
const GetSearchFields = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const data = await axios.get(`/logs/fields`);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetSearchFields;

View File

@@ -0,0 +1,23 @@
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/logs/addToSelectedFields';
const removeSelectedField = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.post(`/logs/fields`, props);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return Promise.reject(ErrorResponseHandler(error as AxiosError));
}
};
export default removeSelectedField;

View File

@@ -0,0 +1,22 @@
import apiV1 from 'api/apiV1';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { withBasePath } from 'utils/basePath';
// 10 min in ms
const TIMEOUT_IN_MS = 10 * 60 * 1000;
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
new EventSourcePolyfill(
ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
{
headers: {
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
},
heartbeatTimeout: TIMEOUT_IN_MS,
},
);

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

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { Button, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { LogViewMode } from 'container/OptionsMenu/types';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import {
Check,

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',
@@ -38,6 +37,7 @@ const ROUTES = {
NOT_FOUND: '/not-found',
LOGS_BASE: '/logs',
LOGS: '/logs/logs-explorer',
OLD_LOGS_EXPLORER: '/logs/old-logs-explorer',
LOGS_EXPLORER: '/logs/logs-explorer',
LIVE_LOGS: '/logs/logs-explorer/live',
LOGS_PIPELINES: '/logs/pipelines',

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
import {
SpantypesFieldContextDTO,
SpantypesSpanMapperDTO,
SpantypesSpanMapperGroupConditionKeyDTO,
SpantypesSpanMapperGroupDTO,
SpantypesSpanMapperOperationDTO,
SpantypesSpanMapperOriginDTO,
SpantypesSpanMapperSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
export type MapperGroup = SpantypesSpanMapperGroupDTO;
@@ -11,21 +14,23 @@ export const FieldContext = SpantypesFieldContextDTO;
export type FieldContextValue = SpantypesFieldContextDTO;
export const MapperOperation = SpantypesSpanMapperOperationDTO;
export type MapperOperationValue = SpantypesSpanMapperOperationDTO;
export const MapperOrigin = SpantypesSpanMapperOriginDTO;
export type MapperOriginValue = SpantypesSpanMapperOriginDTO;
export type ConditionKey = SpantypesSpanMapperGroupConditionKeyDTO;
export type MapperDraftMode = 'add' | 'edit';
export interface SourceConfig {
key: string;
context: SpantypesFieldContextDTO;
operation: SpantypesSpanMapperOperationDTO;
}
// `priority` is left out: it is derived from list order when the draft is
// serialized.
export type SourceConfig = Omit<SpantypesSpanMapperSourceDTO, 'priority'>;
// Editable form state for a mapper. `sources` is ordered highest priority
// first; `fieldContext` is where the standardized target is written.
export interface MapperDraft {
id: string | null;
name: string;
fieldContext: SpantypesFieldContextDTO;
fieldContext: FieldContextValue;
sources: SourceConfig[];
enabled: boolean;
}
@@ -33,26 +38,20 @@ export interface MapperDraft {
export interface GroupDraft {
id: string | null;
name: string;
attributes: string[];
resource: string[];
attributes: ConditionKey[];
resource: ConditionKey[];
enabled: boolean;
}
export interface DraftMapper {
// The editor tree identifies rows by `localId` so unsaved ones are addressable;
// `serverId` is null until the row has been persisted.
export type DraftMapper = Omit<MapperDraft, 'id'> & {
localId: string;
serverId: string | null;
name: string;
fieldContext: SpantypesFieldContextDTO;
sources: SourceConfig[];
enabled: boolean;
}
};
export interface DraftGroup {
export type DraftGroup = Omit<GroupDraft, 'id'> & {
localId: string;
serverId: string | null;
name: string;
attributes: string[];
resource: string[];
enabled: boolean;
mappers: DraftMapper[];
}
};

View File

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

View File

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

@@ -0,0 +1,136 @@
import { memo, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Button, Flex } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import Controls from 'container/Controls';
import Download from 'container/Download/Download';
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import dayjs from 'dayjs';
import { Pagination } from 'hooks/queryPagination';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { FlatLogData } from 'lib/logs/flatLogData';
import { OrderPreferenceItems } from 'pages/Logs/config';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
GET_NEXT_LOG_LINES,
GET_PREVIOUS_LOG_LINES,
RESET_ID_START_AND_END,
SET_LOG_LINES_PER_PAGE,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles';
import { SkipBack } from '@signozhq/icons';
function LogControls(): JSX.Element | null {
const {
logLinesPerPage,
liveTail,
isLoading: isLogsLoading,
isLoadingAggregate,
logs,
order,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const handleLogLinesPerPageChange = (e: Pagination['limit']): void => {
dispatch({
type: SET_LOG_LINES_PER_PAGE,
payload: {
logsLinesPerPage: e,
},
});
};
const handleGoToLatest = (): void => {
const { maxTime, minTime } = getMinMaxForSelectedTime(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
const updatedGlobalTime = getGlobalTime(globalTime.selectedTime, {
maxTime,
minTime,
});
if (updatedGlobalTime) {
dispatch({
type: RESET_ID_START_AND_END,
payload: updatedGlobalTime,
});
}
};
const handleNavigatePrevious = (): void => {
dispatch({
type: GET_PREVIOUS_LOG_LINES,
});
};
const handleNavigateNext = (): void => {
dispatch({
type: GET_NEXT_LOG_LINES,
});
};
const flattenLogData = useMemo(
() =>
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format(DATE_TIME_FORMATS.ISO_DATETIME_MS)
: dayjs(log.timestamp / 1e6).format(DATE_TIME_FORMATS.ISO_DATETIME_MS);
return FlatLogData({
...log,
timestamp,
});
}),
[logs],
);
const isLoading = isLogsLoading || isLoadingAggregate;
if (liveTail !== 'STOPPED') {
return null;
}
return (
<Container>
<Download data={flattenLogData} isLoading={isLoading} fileName="log_data" />
<Button
loading={isLoading}
size="small"
type="link"
disabled={order === OrderPreferenceItems.ASC}
onClick={handleGoToLatest}
>
<Flex align="center" gap="4px">
<SkipBack size="md" /> Go to latest
</Flex>
</Button>
<Divider type="vertical" />
<Controls
isLoading={isLoading}
totalCount={logs.length}
countPerPage={logLinesPerPage}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}
handleCountItemsPerPageChange={handleLogLinesPerPageChange}
/>
</Container>
);
}
export default memo(LogControls);

View File

@@ -0,0 +1,14 @@
import { Button } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
`;
export const DownloadLogButton = styled(Button)`
display: flex;
align-items: center;
`;

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

@@ -1,4 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { Link, Pin } from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
@@ -12,19 +14,23 @@ import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { openInNewTab } from 'utils/navigation';
import { ActionItemProps } from './ActionItem';
import { RESTRICTED_SELECTED_FIELDS } from './config';
import FieldRenderer from './FieldRenderer';
import TableViewActions from './TableView/TableViewActions';
import {
@@ -59,6 +65,7 @@ function TableView({
listViewPanelSelectedFields,
handleChangeSelectedView,
}: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
@@ -178,6 +185,11 @@ function TableView({
const spanId = flattenLogData?.span_id;
if (traceId) {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: null,
});
const basePath = generatePath(ROUTES.TRACE_DETAIL, {
id: traceId,
});

View File

@@ -10,6 +10,7 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
@@ -26,7 +27,6 @@ import {
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { RESTRICTED_SELECTED_FIELDS } from '../config';
import { DataType } from '../TableView';
import {
filterKeyForField,
@@ -141,9 +141,10 @@ export default function TableViewActions(
const { stagedQuery, updateQueriesData } = useQueryBuilder();
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
// there is no option for where clause in live logs page or infra monitoring
const isLiveLogsOrInfraPage = useMemo(
// there is no option for where clause in old logs explorer and live logs page or infra monitoring
const isOldLogsExplorerOrLiveLogsPage = useMemo(
() =>
pathname === ROUTES.OLD_LOGS_EXPLORER ||
pathname === ROUTES.LIVE_LOGS ||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_HOSTS ||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
@@ -399,7 +400,7 @@ export default function TableViewActions(
)}
/>
</Tooltip>
{!isLiveLogsOrInfraPage && (
{!isOldLogsExplorerOrLiveLogsPage && (
<Popover
open={isOpen}
onOpenChange={setIsOpen}
@@ -486,7 +487,7 @@ export default function TableViewActions(
)}
/>
</Tooltip>
{!isLiveLogsOrInfraPage && (
{!isOldLogsExplorerOrLiveLogsPage && (
<Popover
open={isOpen}
onOpenChange={setIsOpen}

View File

@@ -1,9 +1,9 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { RESTRICTED_SELECTED_FIELDS } from '../../config';
import TableViewActions from '../TableViewActions';
import useAsyncJSONProcessing from '../useAsyncJSONProcessing';

View File

@@ -1,10 +1,5 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
// Fields that can be filtered on but not grouped by in the log details view.
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
export const typeToArrayTypeMapper: { [key in DataTypes]: DataTypes } = {
[DataTypes.String]: DataTypes.ArrayString,
[DataTypes.Float64]: DataTypes.ArrayFloat64,

View File

@@ -61,7 +61,8 @@ export function useLogAttributeActions({
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
?.active || false;
const isLiveLogs = pathname === ROUTES.LIVE_LOGS;
const isOldExplorerOrLive =
pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS;
const filterFor = useCallback(
(context: FieldContext, isFilterIn: boolean): void => {
@@ -220,7 +221,7 @@ export function useLogAttributeActions({
!handleChangeSelectedView ||
!buildLogFilterTarget(fieldKeyPath, undefined, isBodyJsonQueryEnabled)
.groupBySupported ||
isLiveLogs,
isOldExplorerOrLive,
},
{
key: LogDetailsAction.REPLACE_FILTER,
@@ -228,7 +229,9 @@ export function useLogAttributeActions({
icon: <RefreshCw size={12} />,
onClick: replaceFilter,
shouldHide: (_key, fieldKeyPath): boolean =>
!handleChangeSelectedView || isRestricted(fieldKeyPath) || isLiveLogs,
!handleChangeSelectedView ||
isRestricted(fieldKeyPath) ||
isOldExplorerOrLive,
},
];
}, [
@@ -236,7 +239,7 @@ export function useLogAttributeActions({
groupBy,
replaceFilter,
isBodyJsonQueryEnabled,
isLiveLogs,
isOldExplorerOrLive,
handleChangeSelectedView,
onApplyLogFilter,
]);

View File

@@ -0,0 +1,168 @@
import { memo, useCallback } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ROUTES from 'constants/routes';
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import getStep from 'lib/getStep';
import { getIdConditions } from 'pages/Logs/utils';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
SET_DETAILED_LOG_DATA,
SET_SEARCH_QUERY_STRING,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
type LogDetailedViewProps = {
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
};
function LogDetailedView({
getLogs,
getLogsAggregate,
}: LogDetailedViewProps): JSX.Element {
const history = useHistory();
const {
detailedLog,
searchFilter: { queryString },
logLinesPerPage,
idStart,
liveTail,
idEnd,
order,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const onDrawerClose = (): void => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: null,
});
};
const handleAddToQuery = useCallback(
(fieldKey: string, fieldValue: string, operator: string) => {
const newOperator = getOldLogsOperatorFromNew(operator);
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
newOperator,
queryString,
);
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
},
[history, queryString],
);
const handleClickActionItem = useCallback(
(fieldKey: string, fieldValue: string, operator: string): void => {
const newOperator = getOldLogsOperatorFromNew(operator);
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
newOperator,
queryString,
);
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: {
searchQueryString: updatedQueryString,
},
});
if (liveTail === 'STOPPED') {
getLogs({
q: updatedQueryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order,
timestampStart: minTime,
timestampEnd: maxTime,
...getIdConditions(idStart, idEnd, order),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: updatedQueryString,
});
} else if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
}
},
[
dispatch,
getLogs,
getLogsAggregate,
idEnd,
idStart,
liveTail,
logLinesPerPage,
maxTime,
minTime,
order,
queryString,
],
);
return (
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={detailedLog}
onClose={onDrawerClose}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleClickActionItem}
/>
);
}
interface DispatchProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: never) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(LogDetailedView as any));

View File

@@ -5,6 +5,10 @@ import {
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { OPERATORS as QUERY_BUILDER_OPERATORS } from 'constants/queryBuilder';
import {
RESTRICTED_GROUP_BY_FIELDS,
RESTRICTED_SELECTED_FIELDS,
} from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
@@ -14,10 +18,6 @@ import {
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
RESTRICTED_GROUP_BY_FIELDS,
RESTRICTED_SELECTED_FIELDS,
} from './config';
import { LogAttributeBucket } from './constants';
import { generateFieldKeyForArray, getDataTypes } from './utils';

View File

@@ -0,0 +1,26 @@
export const TIME_PICKER_OPTIONS = [
{
value: 5,
label: '5m',
},
{
value: 15,
label: '15m',
},
{
value: 30,
label: '30m',
},
{
value: 60,
label: '1hr',
},
{
value: 360,
label: '6hrs',
},
{
value: 720,
label: '12hrs',
},
];

View File

@@ -0,0 +1,270 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useDispatch, useSelector } from 'react-redux';
import { green } from '@ant-design/colors';
import { Pause, Play, EllipsisVertical } from '@signozhq/icons';
import { Button, Flex, Popover, Select, Space } from 'antd';
import { LiveTail } from 'api/logs/livetail';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import getStep from 'lib/getStep';
import { throttle } from 'lodash-es';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_AUTO_REFRESH_DISABLED } from 'types/actions/globalTime';
import {
FLUSH_LOGS,
PUSH_LIVE_TAIL_EVENT,
SET_LIVE_TAIL_START_TIME,
SET_LOADING,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
import { ILog } from 'types/api/logs/log';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { popupContainer } from 'utils/selectPopupContainer';
import { TIME_PICKER_OPTIONS } from './config';
import { StopContainer, TimePickerCard, TimePickerSelect } from './styles';
function LogLiveTail({ getLogsAggregate }: Props): JSX.Element {
const {
liveTail,
searchFilter: { queryString },
liveTailStartRange,
logs,
idEnd,
idStart,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const isDarkMode = useIsDarkMode();
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { notifications } = useNotifications();
const dispatch = useDispatch<Dispatch<AppActions>>();
const handleLiveTail = (toggleState: TLogsLiveTailState): void => {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: toggleState,
});
dispatch({
type: UPDATE_AUTO_REFRESH_DISABLED,
payload: toggleState === 'PLAYING',
});
};
const batchedEventsRef = useRef<ILog[]>([]);
const pushLiveLog = useCallback(() => {
dispatch({
type: PUSH_LIVE_TAIL_EVENT,
payload: batchedEventsRef.current.reverse(),
});
batchedEventsRef.current = [];
}, [dispatch]);
const pushLiveLogThrottled = useMemo(
() => throttle(pushLiveLog, 1000),
[pushLiveLog],
);
const batchLiveLog = useCallback(
(e: { data: string }): void => {
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
pushLiveLogThrottled();
},
[pushLiveLogThrottled],
);
const firstLogsId = useMemo(() => logs[0]?.id, [logs]);
// This ref depicts thats whether the live tail is played from paused state or not.
const liveTailSourceRef = useRef<EventSource>();
useEffect(() => {
if (liveTail === 'PLAYING') {
const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf();
const queryParams = new URLSearchParams({
...(queryString ? { q: queryString } : {}),
timestampStart: (timeStamp * 1e6) as never,
...(liveTailSourceRef.current && firstLogsId
? {
idGt: firstLogsId,
}
: {}),
});
if (liveTailSourceRef.current) {
liveTailSourceRef.current.close();
}
const source = LiveTail(queryParams.toString());
liveTailSourceRef.current = source;
source.onmessage = function connectionMessage(e): void {
batchLiveLog(e);
};
source.onerror = function connectionError(event: unknown): void {
console.error(event);
source.close();
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'STOPPED',
});
dispatch({
type: SET_LOADING,
payload: false,
});
notifications.error({
message: 'Live tail stopped due to some error.',
});
};
}
if (liveTail === 'STOPPED') {
liveTailSourceRef.current = undefined;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [liveTail, queryString, notifications, dispatch]);
const handleLiveTailStart = (): void => {
handleLiveTail('PLAYING');
const startTime =
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
const endTime = dayjs().valueOf() * 1e6;
getLogsAggregate({
timestampStart: startTime,
timestampEnd: endTime,
step: getStep({
start: startTime,
end: endTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
if (!liveTailSourceRef.current) {
dispatch({
type: FLUSH_LOGS,
});
}
};
const OptionsPopOverContent = useMemo(
() => (
<TimePickerSelect
getPopupContainer={popupContainer}
disabled={liveTail === 'PLAYING'}
value={liveTailStartRange}
onChange={(value): void => {
if (typeof value === 'number') {
dispatch({
type: SET_LIVE_TAIL_START_TIME,
payload: value,
});
}
}}
>
{TIME_PICKER_OPTIONS.map((optionData) => (
<Select.Option key={optionData.label} value={optionData.value}>
Last {optionData.label}
</Select.Option>
))}
</TimePickerSelect>
),
[dispatch, liveTail, liveTailStartRange],
);
const isDisabled = useMemo(
() => selectedAutoRefreshInterval?.length > 0,
[selectedAutoRefreshInterval],
);
const onLiveTailStop = (): void => {
handleLiveTail('STOPPED');
dispatch({
type: UPDATE_AUTO_REFRESH_DISABLED,
payload: false,
});
dispatch({
type: SET_LOADING,
payload: false,
});
if (liveTailSourceRef.current) {
liveTailSourceRef.current.close();
}
};
return (
<TimePickerCard>
<Space size={0} align="center">
{liveTail === 'PLAYING' ? (
<Button
type="primary"
onClick={onLiveTailStop}
title="Pause live tail"
style={{ background: green[6] }}
>
<Flex align="center" gap={4}>
<span>Pause</span>
<Pause size="md" />
</Flex>
</Button>
) : (
<Button
type="primary"
onClick={handleLiveTailStart}
title="Start live tail"
disabled={isDisabled}
>
<Flex align="center" gap={4}>
Go Live <Play size="md" />
</Flex>
</Button>
)}
{liveTail !== 'STOPPED' && (
<Button type="dashed" onClick={onLiveTailStop} title="Exit live tail">
<StopContainer isDarkMode={isDarkMode} />
</Button>
)}
<Popover
getPopupContainer={popupContainer}
placement="bottomRight"
title="Select Live Tail Timing"
trigger="click"
content={OptionsPopOverContent}
>
<EllipsisVertical size="lg" />
</Popover>
</Space>
</TimePickerCard>
);
}
interface DispatchProps {
getLogsAggregate: typeof getLogsAggregate;
}
type Props = DispatchProps;
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(LogLiveTail);

View File

@@ -0,0 +1,25 @@
import { Card, Select } from 'antd';
import styled from 'styled-components';
export const TimePickerCard = styled(Card)`
.ant-card-body {
display: flex;
padding: 0;
}
`;
export const TimePickerSelect = styled(Select)`
min-width: 100px;
`;
interface Props {
isDarkMode: boolean;
}
export const StopContainer = styled.div<Props>`
height: 0.8rem;
width: 0.8rem;
border-radius: 0.1rem;
background-color: ${({ isDarkMode }): string =>
isDarkMode ? '#fff' : '#000'};
`;

View File

@@ -0,0 +1,95 @@
import { useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useSelector } from 'react-redux';
import { blue } from '@ant-design/colors';
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import dayjs from 'dayjs';
import useInterval from 'hooks/useInterval';
import getStep from 'lib/getStep';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles';
function LogsAggregate({ getLogsAggregate }: DispatchProps): JSX.Element {
const {
searchFilter: { queryString },
idEnd,
idStart,
isLoadingAggregate,
logsAggregate,
liveTail,
liveTailStartRange,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
useInterval(
() => {
const startTime =
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
const endTime = dayjs().valueOf() * 1e6;
getLogsAggregate({
timestampStart: startTime,
timestampEnd: endTime,
step: getStep({
start: startTime,
end: endTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
},
60000,
liveTail === 'PLAYING',
);
const graphData = useMemo(
() => ({
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
data: logsAggregate.map((s) => s.value),
backgroundColor: blue[4],
},
],
}),
[logsAggregate],
);
return (
<Container>
{isLoadingAggregate ? (
<Spinner size="default" height="100%" />
) : (
<Graph
name="usage"
data={graphData}
type="bar"
containerHeight="100%"
animate
/>
)}
</Container>
);
}
interface DispatchProps {
getLogsAggregate: typeof getLogsAggregate;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(LogsAggregate);

View File

@@ -0,0 +1,11 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`
position: relative;
margin: 0.5rem 0;
.ant-card-body {
height: 20vh;
min-height: 200px;
}
`;

View File

@@ -0,0 +1,93 @@
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { Loader } from '@signozhq/icons';
import { Button, Popover, Spin } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useIsDarkMode } from 'hooks/useDarkMode';
import {
IField,
IInterestingFields,
ISelectedFields,
} from 'types/api/logs/fields';
import { ICON_STYLE } from './config';
import { Field } from './styles';
function FieldItem({
name,
buttonIcon,
buttonOnClick,
fieldData,
fieldIndex,
isLoading,
iconHoverText,
}: FieldItemProps): JSX.Element {
const [isHovered, setIsHovered] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const onClickHandler = useCallback(() => {
if (!isLoading && buttonOnClick) {
buttonOnClick({ fieldData, fieldIndex });
}
}, [buttonOnClick, fieldData, fieldIndex, isLoading]);
const renderContent = useMemo(() => {
if (isLoading) {
return (
<Spin
spinning
size="small"
indicator={<Loader className="animate-spin" />}
/>
);
}
if (isHovered) {
return (
<Popover content={<Typography>{iconHoverText}</Typography>}>
<Button
size="small"
type="text"
icon={buttonIcon}
onClick={onClickHandler}
/>
</Popover>
);
}
return null;
}, [buttonIcon, iconHoverText, isHovered, isLoading, onClickHandler]);
const onMouseHoverHandler = useCallback(
(value: boolean) => (): void => {
setIsHovered(value);
},
[],
);
return (
<Field
onMouseEnter={onMouseHoverHandler(true)}
onMouseLeave={onMouseHoverHandler(false)}
isDarkMode={isDarkMode}
>
<Typography style={ICON_STYLE.PLUS}>{name}</Typography>
{renderContent}
</Field>
);
}
interface FieldItemProps {
name: string;
buttonIcon: ReactNode;
buttonOnClick: (props: {
fieldData: IInterestingFields | ISelectedFields;
fieldIndex: number;
}) => void;
fieldData: IField;
fieldIndex: number;
isLoading: boolean;
iconHoverText: string;
}
export default FieldItem;

View File

@@ -0,0 +1,11 @@
import { blue, red } from '@ant-design/colors';
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
// Fields that can be filtered on but not grouped by in the log details view.
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
export const ICON_STYLE = {
PLUS: { color: blue[5] },
CLOSE: { color: red[5] },
};

View File

@@ -0,0 +1,119 @@
import { ChangeEvent, useCallback, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { CirclePlus, X } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Col } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import { ICON_STYLE, RESTRICTED_SELECTED_FIELDS } from './config';
import FieldItem from './FieldItem';
import { CategoryContainer, FieldContainer } from './styles';
import { IHandleInterestProps, IHandleRemoveInterestProps } from './types';
import { onHandleAddInterest, onHandleRemoveInterest } from './utils';
function LogsFilters(): JSX.Element {
const {
fields: { interesting, selected },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const [selectedFieldLoading, setSelectedFieldLoading] = useState<number[]>([]);
const [interestingFieldLoading, setInterestingFieldLoading] = useState<
number[]
>([]);
const [filterValuesInput, setFilterValuesInput] = useState('');
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
setFilterValuesInput((e.target as HTMLInputElement).value);
};
const onHandleAddSelectedToInteresting = useCallback(
({ fieldData, fieldIndex }: IHandleInterestProps) =>
(): Promise<void> =>
onHandleAddInterest({
fieldData,
fieldIndex,
interesting,
interestingFieldLoading,
setInterestingFieldLoading,
selected,
}),
[interesting, interestingFieldLoading, selected],
);
const onHandleRemoveSelected = useCallback(
({ fieldData, fieldIndex }: IHandleRemoveInterestProps) =>
(): Promise<void> =>
onHandleRemoveInterest({
fieldData,
fieldIndex,
interesting,
interestingFieldLoading,
selected,
setSelectedFieldLoading,
}),
[interesting, interestingFieldLoading, selected, setSelectedFieldLoading],
);
return (
<Col flex="250px">
<Input
placeholder="Filter Values"
onInput={handleSearch}
value={filterValuesInput}
onChange={handleSearch}
/>
<CategoryContainer>
<CategoryHeading>SELECTED FIELDS</CategoryHeading>
<FieldContainer>
{selected
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.filter((field) => RESTRICTED_SELECTED_FIELDS.indexOf(field.name) === -1)
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}`}
name={field.name}
fieldData={field}
fieldIndex={idx}
buttonIcon={<X style={ICON_STYLE.CLOSE} size="md" />}
buttonOnClick={onHandleRemoveSelected({
fieldData: field,
fieldIndex: idx,
})}
isLoading={selectedFieldLoading.includes(idx)}
iconHoverText="Remove from Selected Fields"
/>
))}
</FieldContainer>
</CategoryContainer>
<CategoryContainer>
<CategoryHeading>INTERESTING FIELDS</CategoryHeading>
<FieldContainer>
{interesting
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}`}
name={field.name}
fieldData={field}
fieldIndex={idx}
buttonIcon={<CirclePlus style={ICON_STYLE.PLUS} size="md" />}
buttonOnClick={onHandleAddSelectedToInteresting({
fieldData: field,
fieldIndex: idx,
})}
isLoading={interestingFieldLoading.includes(idx)}
iconHoverText="Add to Selected Fields"
/>
))}
</FieldContainer>
</CategoryContainer>
</Col>
);
}
export default LogsFilters;

View File

@@ -0,0 +1,29 @@
import { blue, grey } from '@ant-design/colors';
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const CategoryContainer = styled.div`
margin: 1rem 0;
padding-left: 0.2rem;
`;
export const FieldContainer = styled(Typography.Text)`
margin: 0.2rem 0;
color: ${blue[4]};
`;
export const Field = styled.div<{ isDarkMode: boolean }>`
border-radius: 0.5rem;
padding: 0.3rem 0.5rem;
height: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
&:hover {
background: ${({ isDarkMode }): string => (isDarkMode ? grey[7] : '#ddd')};
}
`;
export const ExtractField = styled(Typography.Text)`
color: ${blue[4]};
`;

View File

@@ -0,0 +1,36 @@
import { SetStateAction } from 'react';
import {
IField,
IInterestingFields,
ISelectedFields,
} from 'types/api/logs/fields';
type SetLoading = (value: SetStateAction<number[]>) => void;
export type IHandleInterestProps = {
fieldData: IInterestingFields;
fieldIndex: number;
};
export type IHandleRemoveInterestProps = {
fieldData: ISelectedFields;
fieldIndex: number;
};
export interface OnHandleAddInterestProps {
setInterestingFieldLoading: SetLoading;
fieldIndex: number;
fieldData: ISelectedFields;
interesting: IField[];
interestingFieldLoading: number[];
selected: IField[];
}
export interface OnHandleRemoveInterestProps {
setSelectedFieldLoading: SetLoading;
selected: IField[];
interesting: IField[];
interestingFieldLoading: number[];
fieldData: IInterestingFields;
fieldIndex: number;
}

View File

@@ -0,0 +1,105 @@
import { message } from 'antd';
import addToSelectedFields from 'api/logs/AddToSelectedField';
import removeSelectedField from 'api/logs/RemoveFromSelectedField';
import store from 'store';
import {
UPDATE_INTERESTING_FIELDS,
UPDATE_SELECTED_FIELDS,
} from 'types/actions/logs';
import { ErrorResponse } from 'types/api';
import { RESTRICTED_SELECTED_FIELDS } from './config';
import { OnHandleAddInterestProps, OnHandleRemoveInterestProps } from './types';
export const onHandleAddInterest = async ({
setInterestingFieldLoading,
fieldIndex,
fieldData,
interesting,
interestingFieldLoading,
selected,
}: OnHandleAddInterestProps): Promise<void> => {
const { dispatch } = store;
setInterestingFieldLoading((prevState: number[]) => {
prevState.push(fieldIndex);
return [...prevState];
});
try {
await addToSelectedFields({
...fieldData,
selected: true,
});
dispatch({
type: UPDATE_INTERESTING_FIELDS,
payload: {
field: interesting.filter((e) => e.name !== fieldData.name),
type: 'selected',
},
});
dispatch({
type: UPDATE_SELECTED_FIELDS,
payload: {
field: [...selected, fieldData],
type: 'selected',
},
});
} catch (errRes) {
message.error((errRes as ErrorResponse)?.error);
} finally {
setInterestingFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
}
};
export const onHandleRemoveInterest = async ({
setSelectedFieldLoading,
selected,
interesting,
interestingFieldLoading,
fieldData,
fieldIndex,
}: OnHandleRemoveInterestProps): Promise<void> => {
if (RESTRICTED_SELECTED_FIELDS.includes(fieldData.name)) {
return;
}
const { dispatch } = store;
setSelectedFieldLoading((prevState) => {
prevState.push(fieldIndex);
return [...prevState];
});
try {
await removeSelectedField({
...fieldData,
selected: false,
});
dispatch({
type: UPDATE_SELECTED_FIELDS,
payload: {
field: selected.filter((e) => e.name !== fieldData.name),
type: 'selected',
},
});
dispatch({
type: UPDATE_INTERESTING_FIELDS,
payload: {
field: [...interesting, fieldData],
type: 'interesting',
},
});
} catch (errRes) {
message.error((errRes as ErrorResponse)?.error);
} finally {
setSelectedFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
}
};

View File

@@ -0,0 +1,27 @@
import { Button, Row } from 'antd';
interface SearchFieldsActionBarProps {
applyUpdate: VoidFunction;
clearFilters: VoidFunction;
}
export function SearchFieldsActionBar({
applyUpdate,
clearFilters,
}: SearchFieldsActionBarProps): JSX.Element | null {
return (
<Row style={{ justifyContent: 'flex-end', paddingRight: '2.4rem' }}>
<Button
type="default"
onClick={clearFilters}
style={{ marginRight: '1rem' }}
>
Clear Filter
</Button>
<Button type="primary" onClick={applyUpdate}>
Apply
</Button>
</Row>
);
}
export default SearchFieldsActionBar;

View File

@@ -0,0 +1,19 @@
import { Typography } from '@signozhq/ui/typography';
interface FieldKeyProps {
name: string;
type: string;
}
function FieldKey({ name, type }: FieldKeyProps): JSX.Element {
return (
<span style={{ margin: '0.25rem 0', display: 'flex', gap: '0.5rem' }}>
<Typography.Text>{name}</Typography.Text>
<Typography.Text color="muted" italic>
{type}
</Typography.Text>
</span>
);
}
export default FieldKey;

View File

@@ -0,0 +1,259 @@
import { useCallback, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { SquareX, X } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Select } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import {
ConditionalOperators,
QueryOperatorsMultiVal,
QueryOperatorsSingleVal,
} from 'lib/logql/tokens';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import FieldKey from '../FieldKey';
import { QueryFieldContainer } from '../styles';
import { QueryFields } from '../utils';
import { Container, QueryWrapper } from './styles';
const { Option } = Select;
function QueryConditionField({
query,
queryIndex,
onUpdate,
}: QueryConditionFieldProps): JSX.Element {
const allOptions = Object.values(ConditionalOperators);
return (
<Select
defaultValue={
(query as QueryFields).value &&
(
(query as QueryFields)
?.value as unknown as QueryFields as unknown as string
).toUpperCase()
}
onChange={(e): void => {
onUpdate({ ...query, value: e }, queryIndex);
}}
>
{allOptions.map((cond) => (
<Option key={cond} value={cond} label={cond}>
{cond}
</Option>
))}
</Select>
);
}
interface QueryFieldProps {
query: Query;
queryIndex: number;
onUpdate: (query: Query, queryIndex: number) => void;
onDelete: (queryIndex: number) => void;
}
function QueryField({
query,
queryIndex,
onUpdate,
onDelete,
}: QueryFieldProps): JSX.Element | null {
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const getFieldType = useCallback(
(inputKey: string): string => {
const selectedField = selected.find((field) => inputKey === field.name);
if (selectedField) {
return selectedField.type;
}
return '';
},
[selected],
);
const fieldType = useMemo(
() => getFieldType(query[0].value as string),
[getFieldType, query],
);
const handleChange = (qIdx: number, value: string): void => {
const updatedQuery = [...query];
updatedQuery[qIdx].value = value || '';
if (qIdx === 1) {
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
if (!Array.isArray(updatedQuery[2].value)) {
updatedQuery[2].value = [];
}
} else if (
Object.values(QueryOperatorsSingleVal).includes(value) &&
Array.isArray(updatedQuery[2].value)
) {
updatedQuery[2].value = '';
}
}
onUpdate(updatedQuery, queryIndex);
};
const handleClear = (): void => {
onDelete(queryIndex);
};
if (!Array.isArray(query)) {
return null;
}
return (
<QueryFieldContainer
style={{ ...(queryIndex === 0 && { gridColumnStart: 2 }) }}
>
<div style={{ flex: 1, minWidth: 100 }}>
<FieldKey name={(query[0] && query[0].value) as string} type={fieldType} />
</div>
<Select
defaultActiveFirstOption={false}
placeholder="Select Operator"
defaultValue={
query[1] && query[1].value
? (query[1].value as string).toUpperCase()
: null
}
onChange={(e): void => handleChange(1, e)}
style={{ minWidth: 150 }}
>
{Object.values({
...QueryOperatorsMultiVal,
...QueryOperatorsSingleVal,
}).map((cond) => (
<Option key={cond} value={cond} label={cond}>
{cond}
</Option>
))}
</Select>
<div style={{ flex: 2 }}>
{Array.isArray(query[2].value) ||
Object.values(QueryOperatorsMultiVal).some(
(op) => op.toUpperCase() === (query[1].value as string)?.toUpperCase(),
) ? (
<Select
mode="tags"
style={{ width: '100%' }}
open={isDropDownOpen}
onChange={(e): void => handleChange(2, e as never)}
defaultValue={(query[2] && query[2].value) || []}
notFoundContent={null}
onInputKeyDown={(): void => setIsDropDownOpen(true)}
onSelect={(): void => setIsDropDownOpen(false)}
/>
) : (
<Input
onChange={(e): void => {
handleChange(2, e.target.value);
}}
style={{ width: '100%' }}
defaultValue={query[2] && query[2].value}
value={query[2] && query[2].value}
/>
)}
</div>
<Button
icon={<X size="md" />}
type="text"
size="small"
onClick={handleClear}
/>
</QueryFieldContainer>
);
}
interface QueryConditionFieldProps {
query: QueryFields;
queryIndex: number;
onUpdate: (arg0: unknown, arg1: number) => void;
}
export type Query = { value: string | string[]; type: string }[];
export interface QueryBuilderProps {
keyPrefix: string;
onDropDownToggleHandler: (value: boolean) => VoidFunction;
fieldsQuery: QueryFields[][];
setFieldsQuery: (q: QueryFields[][]) => void;
syncKeyPrefix: () => void;
}
function QueryBuilder({
keyPrefix,
fieldsQuery,
setFieldsQuery,
onDropDownToggleHandler,
syncKeyPrefix,
}: QueryBuilderProps): JSX.Element {
const handleUpdate = (query: Query, queryIndex: number): void => {
const updated = [...fieldsQuery];
updated[queryIndex] = query as never; // parseQuery(query) as never;
setFieldsQuery(updated);
};
const handleDelete = (queryIndex: number): void => {
const updated = [...fieldsQuery];
if (queryIndex !== 0) {
updated.splice(queryIndex - 1, 2);
} else {
updated.splice(queryIndex, 2);
}
setFieldsQuery(updated);
// initiate re-render query panel
syncKeyPrefix();
};
const QueryUI = (
fieldsQuery: QueryFields[][],
): JSX.Element | JSX.Element[] => {
const result: JSX.Element[] = [];
fieldsQuery.forEach((query, idx) => {
if (Array.isArray(query) && query.length > 1) {
result.push(
<QueryField
key={keyPrefix}
query={query}
queryIndex={idx}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>,
);
} else {
result.push(
<div key={keyPrefix}>
<QueryConditionField
query={Array.isArray(query) ? query[0] : query}
queryIndex={idx}
onUpdate={handleUpdate as never}
/>
</div>,
);
}
});
return result;
};
return (
<>
<Container isMargin={fieldsQuery.length === 0}>
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
<SquareX onClick={onDropDownToggleHandler(false)} size="md" />
</Container>
<QueryWrapper key={keyPrefix}>{QueryUI(fieldsQuery)}</QueryWrapper>
</>
);
}
export default QueryBuilder;

View File

@@ -0,0 +1,17 @@
import styled from 'styled-components';
interface Props {
isMargin: boolean;
}
export const Container = styled.div<Props>`
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: ${(props): string => (props.isMargin ? '2rem' : '0')};
`;
export const QueryWrapper = styled.div`
display: grid;
grid-template-columns: 80px 1fr;
margin: 0.5rem 0px;
`;

View File

@@ -0,0 +1,62 @@
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Button } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import map from 'lodash-es/map';
import { AppState } from 'store/reducers';
// import { ADD_SEARCH_FIELD_QUERY_STRING } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
import FieldKey from './FieldKey';
interface SuggestedItemProps {
name: string;
type: string;
applySuggestion: (name: string) => void;
}
function SuggestedItem({
name,
type,
applySuggestion,
}: SuggestedItemProps): JSX.Element {
const addSuggestedField = (): void => {
applySuggestion(name);
};
return (
<Button
type="text"
style={{ display: 'block', padding: '0.2rem' }}
onClick={addSuggestedField}
>
<FieldKey name={name} type={type} />
</Button>
);
}
interface SuggestionsProps {
applySuggestion: (name: string) => void;
}
function Suggestions({ applySuggestion }: SuggestionsProps): JSX.Element {
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
return (
<div>
<CategoryHeading>SUGGESTIONS</CategoryHeading>
<div>
{map(selected, (field) => (
<SuggestedItem
key={JSON.stringify(field)}
name={field.name}
type={field.type}
applySuggestion={applySuggestion}
/>
))}
</div>
</div>
);
}
export default Suggestions;

View File

@@ -0,0 +1,123 @@
import { useCallback, useEffect, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useNotifications } from 'hooks/useNotifications';
import { reverseParser } from 'lib/logql';
import { flatten } from 'lodash-es';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import { SearchFieldsActionBar } from './ActionBar';
import QueryBuilder from './QueryBuilder/QueryBuilder';
import Suggestions from './Suggestions';
import {
createParsedQueryStructure,
fieldsQueryIsvalid,
hashCode,
initQueryKOVPair,
prepareConditionOperator,
QueryFields,
} from './utils';
export interface SearchFieldsProps {
onDropDownToggleHandler: (value: boolean) => VoidFunction;
updateQueryString: (value: string) => void;
}
function SearchFields({
onDropDownToggleHandler,
updateQueryString,
}: SearchFieldsProps): JSX.Element {
const {
searchFilter: { parsedQuery },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const [fieldsQuery, setFieldsQuery] = useState(
createParsedQueryStructure([...parsedQuery] as never[]),
);
const keyPrefixRef = useRef(hashCode(JSON.stringify(fieldsQuery)));
const { notifications } = useNotifications();
useEffect(() => {
const updatedFieldsQuery = createParsedQueryStructure([
...parsedQuery,
] as never[]);
setFieldsQuery(updatedFieldsQuery);
const incomingHashCode = hashCode(JSON.stringify(updatedFieldsQuery));
if (incomingHashCode !== keyPrefixRef.current) {
keyPrefixRef.current = incomingHashCode;
}
}, [parsedQuery]);
// syncKeyPrefix initiates re-render. useful in situations like
// delete field (in search panel). this method allows condiitonally
// setting keyPrefix as doing it on every update of query initiates
// a re-render. this is a problem for text fields where input focus goes away.
const syncKeyPrefix = (): void => {
keyPrefixRef.current = hashCode(JSON.stringify(fieldsQuery));
};
const addSuggestedField = useCallback(
(name: string): void => {
if (!name) {
return;
}
const query = [...fieldsQuery];
if (fieldsQuery.length > 0) {
query.push([prepareConditionOperator()]);
}
const newField: QueryFields[] = [];
initQueryKOVPair(name).forEach((q) => newField.push(q));
query.push(newField);
keyPrefixRef.current = hashCode(JSON.stringify(query));
setFieldsQuery(query);
},
[fieldsQuery, setFieldsQuery],
);
const applyUpdate = useCallback((): void => {
const flatParsedQuery = flatten(fieldsQuery);
if (!fieldsQueryIsvalid(flatParsedQuery)) {
notifications.error({
message: 'Please enter a valid criteria for each of the selected fields',
});
return;
}
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
updateQueryString(reverseParser(flatParsedQuery));
onDropDownToggleHandler(false)();
}, [fieldsQuery, notifications, onDropDownToggleHandler, updateQueryString]);
const clearFilters = useCallback((): void => {
keyPrefixRef.current = hashCode(JSON.stringify([]));
setFieldsQuery([]);
updateQueryString('');
}, [updateQueryString]);
return (
<>
<QueryBuilder
key={keyPrefixRef.current}
keyPrefix={keyPrefixRef.current}
onDropDownToggleHandler={onDropDownToggleHandler}
fieldsQuery={fieldsQuery}
setFieldsQuery={setFieldsQuery}
syncKeyPrefix={syncKeyPrefix}
/>
<SearchFieldsActionBar
applyUpdate={applyUpdate}
clearFilters={clearFilters}
/>
<Suggestions applySuggestion={addSuggestedField} />
</>
);
}
export default SearchFields;

View File

@@ -0,0 +1,16 @@
import { blue } from '@ant-design/colors';
import styled from 'styled-components';
export const QueryFieldContainer = styled.div`
padding: 0.25rem 0.5rem;
margin: 0.1rem 0.5rem 0;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 0.25rem;
gap: 1rem;
width: 100%;
&:hover {
background: ${blue[6]};
}
`;

View File

@@ -0,0 +1,137 @@
// @ts-nocheck
import {
ConditionalOperators,
QueryTypes,
ValidTypeSequence,
ValidTypeValue,
} from 'lib/logql/tokens';
export interface QueryFields {
type: keyof typeof QueryTypes;
value: string | string[];
}
export function fieldsQueryIsvalid(queryFields: QueryFields[]): boolean {
let lastOp: string;
let result = true;
queryFields.forEach((q, idx) => {
if (!q.value || q.value === null || q.value === '') {
result = false;
}
if (Array.isArray(q.value) && q.value.length === 0) {
result = false;
}
const nextOp = idx < queryFields.length ? queryFields[idx + 1] : undefined;
if (!ValidTypeSequence(lastOp?.type, q?.type, nextOp?.type)) {
result = false;
}
if (!ValidTypeValue(lastOp?.value, q.value)) {
result = false;
}
lastOp = q;
});
return result;
}
export const queryKOVPair = (): QueryFields[] => [
{
type: QueryTypes.QUERY_KEY,
value: null,
},
{
type: QueryTypes.QUERY_OPERATOR,
value: null,
},
{
type: QueryTypes.QUERY_VALUE,
value: null,
},
];
export const initQueryKOVPair = (
name: string = null,
op: string = null,
value: string | string[] = null,
): QueryFields[] => [
{
type: QueryTypes.QUERY_KEY,
value: name,
},
{
type: QueryTypes.QUERY_OPERATOR,
value: op,
},
{
type: QueryTypes.QUERY_VALUE,
value: value,
},
];
export const prepareConditionOperator = (
op: string = ConditionalOperators.AND,
): QueryFields => {
return {
type: QueryTypes.CONDITIONAL_OPERATOR,
value: op,
};
};
export const createParsedQueryStructure = (
parsedQuery = [],
): QueryFields[][] => {
if (parsedQuery.length === 0) {
return parsedQuery;
}
const structuredArray = [queryKOVPair()];
let cond;
let qCtr = -1;
parsedQuery.forEach((query) => {
if (cond) {
structuredArray.push(cond);
structuredArray.push(queryKOVPair());
cond = null;
qCtr = -1;
}
const stagingArr = structuredArray.at(-1);
const prevQuery =
Array.isArray(stagingArr) && qCtr >= 0 ? stagingArr[qCtr] : null;
if (query.type === QueryTypes.QUERY_KEY) {
stagingArr[qCtr + 1] = query;
} else if (
query.type === QueryTypes.QUERY_OPERATOR &&
prevQuery &&
prevQuery.type === QueryTypes.QUERY_KEY
) {
stagingArr[qCtr + 1] = query;
} else if (
query.type === QueryTypes.QUERY_VALUE &&
prevQuery &&
prevQuery.type === QueryTypes.QUERY_OPERATOR
) {
stagingArr[qCtr + 1] = query;
} else if (query.type === QueryTypes.CONDITIONAL_OPERATOR) {
cond = query;
}
qCtr++;
});
return structuredArray;
};
export const hashCode = (s: string): string => {
if (!s) {
return '0';
}
return `${Math.abs(
[...s].reduce((a, b) => {
a = (a << 5) - a + b.codePointAt(0);
return a & a;
}, 0),
)}`;
};

View File

@@ -0,0 +1,230 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useDispatch, useSelector } from 'react-redux';
import { Input, InputRef, Popover } from 'antd';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import debounce from 'lodash-es/debounce';
import { getIdConditions } from 'pages/Logs/utils';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetLogsFields } from 'store/actions/logs/getFields';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
FLUSH_LOGS,
SET_LOADING,
SET_LOADING_AGGREGATE,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { popupContainer } from 'utils/selectPopupContainer';
import SearchFields from './SearchFields';
import { Container, DropDownContainer } from './styles';
import { useSearchParser } from './useSearchParser';
function SearchFilter({
getLogs,
getLogsAggregate,
getLogsFields,
}: SearchFilterProps): JSX.Element {
const { updateQueryString, queryString } = useSearchParser();
const [searchText, setSearchText] = useState(queryString);
const [showDropDown, setShowDropDown] = useState(false);
const searchRef = useRef<InputRef>(null);
const { logLinesPerPage, idEnd, idStart, liveTail, order } = useSelector<
AppState,
ILogsReducer
>((state) => state.logs);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
// keep sync with url queryString
useEffect(() => {
setSearchText(queryString);
}, [queryString]);
const debouncedupdateQueryString = useMemo(
() => debounce(updateQueryString, 300),
[updateQueryString],
);
const onDropDownToggleHandler = useCallback(
(value: boolean) => (): void => {
setShowDropDown(value);
},
[],
);
const handleSearch = useCallback(
(customQuery: string) => {
getLogsFields();
const { maxTime, minTime } = globalTime;
if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
dispatch({
type: FLUSH_LOGS,
});
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
});
dispatch({
type: SET_LOADING,
payload: false,
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: customQuery,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
} else {
getLogs({
q: customQuery,
limit: logLinesPerPage,
orderBy: 'timestamp',
order,
timestampStart: minTime,
timestampEnd: maxTime,
...getIdConditions(idStart, idEnd, order),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: customQuery,
});
}
},
[
dispatch,
getLogs,
getLogsAggregate,
idEnd,
idStart,
liveTail,
logLinesPerPage,
globalTime,
getLogsFields,
order,
],
);
const urlQuery = useUrlQuery();
const urlQueryString = urlQuery.get('q');
useEffect(() => {
dispatch({
type: SET_LOADING,
payload: true,
});
dispatch({
type: SET_LOADING_AGGREGATE,
payload: true,
});
const debouncedHandleSearch = debounce(handleSearch, 600);
debouncedHandleSearch(urlQueryString || '');
return (): void => {
debouncedHandleSearch.cancel();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
urlQueryString,
idEnd,
idStart,
logLinesPerPage,
dispatch,
globalTime.maxTime,
globalTime.minTime,
order,
]);
const onPopOverChange = useCallback(
(isVisible: boolean) => {
onDropDownToggleHandler(isVisible)();
},
[onDropDownToggleHandler],
);
return (
<Container>
<Popover
getPopupContainer={popupContainer}
placement="bottom"
content={
<DropDownContainer>
<SearchFields
updateQueryString={updateQueryString}
onDropDownToggleHandler={onDropDownToggleHandler}
/>
</DropDownContainer>
}
trigger="click"
overlayInnerStyle={{
width: `${searchRef?.current?.input?.offsetWidth || 0}px`,
}}
open={showDropDown}
destroyTooltipOnHide
onOpenChange={onPopOverChange}
>
<Input.Search
ref={searchRef}
placeholder="Search Filter"
value={searchText}
onChange={(e): void => {
const { value } = e.target;
setSearchText(value);
}}
onSearch={debouncedupdateQueryString}
allowClear
/>
</Popover>
</Container>
);
}
interface DispatchProps {
getLogs: typeof getLogs;
getLogsAggregate: typeof getLogsAggregate;
getLogsFields: typeof GetLogsFields;
}
type SearchFilterProps = DispatchProps;
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(SearchFilter));

View File

@@ -0,0 +1,14 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const DropDownContainer = styled(Card)`
.ant-card-body {
width: 100%;
}
`;
export const Container = styled.div`
width: 100%;
flex: 1;
position: relative;
`;

View File

@@ -0,0 +1,81 @@
import { useCallback, useEffect } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import history from 'lib/history';
import { parseQuery } from 'lib/logql';
import isEqual from 'lodash-es/isEqual';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
SET_SEARCH_QUERY_PARSED_PAYLOAD,
SET_SEARCH_QUERY_STRING,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { getGlobalTime } from './utils';
export function useSearchParser(): {
queryString: string;
parsedQuery: unknown;
updateQueryString: (arg0: string) => void;
} {
const dispatch = useDispatch<Dispatch<AppActions>>();
const {
searchFilter: { parsedQuery, queryString },
order,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const urlQuery = useUrlQuery();
const parsedFilters = urlQuery.get('q');
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((store) => store.globalTime);
const updateQueryString = useCallback(
(updatedQueryString: string) => {
history.replace({
pathname: history.location.pathname,
search: `?${QueryParams.q}=${updatedQueryString}&${QueryParams.order}=${order}`,
});
const globalTime = getMinMaxForSelectedTime(selectedTime, minTime, maxTime);
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: {
searchQueryString: updatedQueryString,
globalTime: getGlobalTime(selectedTime, globalTime),
},
});
const parsedQueryFromString = parseQuery(updatedQueryString);
if (!isEqual(parsedQuery, parsedQueryFromString)) {
dispatch({
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
payload: parsedQueryFromString,
});
}
},
// need to hide this warning as we don't want to update the query string on every change
// eslint-disable-next-line react-hooks/exhaustive-deps
[dispatch, parsedQuery, selectedTime, queryString],
);
useEffect(() => {
updateQueryString(parsedFilters || '');
}, [parsedFilters, updateQueryString]);
return {
queryString,
parsedQuery,
updateQueryString,
};
}

View File

@@ -0,0 +1,15 @@
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { GetMinMaxPayload } from 'lib/getMinMax';
export const getGlobalTime = (
selectedTime: Time | CustomTimeType,
globalTime: GetMinMaxPayload,
): GetMinMaxPayload | undefined => {
if (selectedTime === 'custom') {
return undefined;
}
return globalTime;
};

View File

@@ -0,0 +1,164 @@
import { memo, useCallback, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso';
import { Card } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { AppState } from 'store/reducers';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
// interfaces
import { ILogsReducer } from 'types/reducer/logs';
// styles
import { Container, Heading } from './styles';
import './logsTable.styles.scss';
export type LogViewMode = 'raw' | 'table' | 'list';
type LogsTableProps = {
viewMode: LogViewMode;
linesPerRow: number;
};
function LogsTable(props: LogsTableProps): JSX.Element {
const { viewMode, linesPerRow } = props;
const { activeLog, onClearActiveLog, onAddToQuery, onSetActiveLog } =
useActiveLog();
const {
logs,
fields: { selected },
isLoading,
liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const isLiveTail = useMemo(
() => logs.length === 0 && liveTail === 'PLAYING',
[logs?.length, liveTail],
);
const isNoLogs = useMemo(
() => logs.length === 0 && liveTail === 'STOPPED',
[logs?.length, liveTail],
);
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
// this component will alwyays be called on old logs explorer page itself!
dataSource: DataSource.LOGS,
// and we do not have table / timeseries aggregated views in the old logs explorer!
aggregateOperator: StringOperators.NOOP,
});
const getItemContent = useCallback(
(index: number): JSX.Element => {
const log = logs[index];
if (viewMode === 'raw') {
return (
<RawLogView
key={log.id}
data={log}
linesPerRow={linesPerRow}
selectedFields={selected}
fontSize={options.fontSize}
/>
);
}
return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selected}
linesPerRow={linesPerRow}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
/>
);
},
[
logs,
viewMode,
selected,
linesPerRow,
onAddToQuery,
onSetActiveLog,
options.fontSize,
],
);
const renderContent = useMemo(() => {
if (viewMode === 'table') {
return (
<LogsTableView
onClickExpand={onSetActiveLog}
logs={logs}
fields={selected}
linesPerRow={linesPerRow}
fontSize={options.fontSize}
/>
);
}
return (
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
<OverlayScrollbar isVirtuoso>
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
</OverlayScrollbar>
</Card>
);
}, [
getItemContent,
linesPerRow,
logs,
onSetActiveLog,
options.fontSize,
selected,
viewMode,
]);
if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />;
}
return (
<Container>
{viewMode !== 'table' && (
<Heading>
<Typography.Text>Event</Typography.Text>
</Heading>
)}
{isLiveTail && <Typography>Getting live logs...</Typography>}
{isNoLogs && <Typography>No logs lines found</Typography>}
{renderContent}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</Container>
);
}
export default memo(LogsTable);

View File

@@ -0,0 +1,3 @@
.logs-card {
flex: 1;
}

View File

@@ -0,0 +1,20 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
overflow-x: hidden;
width: 100%;
margin-bottom: 1rem;
margin-top: 0.5rem;
display: flex;
flex-direction: column;
flex: 1;
`;
export const Heading = styled(Card)`
margin-bottom: 0.1rem;
height: 32px;
.ant-card-body {
padding: 0.3rem 0.5rem;
}
`;

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