Compare commits

...

83 Commits

Author SHA1 Message Date
Prashant Shahi
ec0185da61 Merge branch 'develop' into release/v0.43.x 2024-04-15 18:24:21 +05:45
Srikanth Chekuri
fc2bdb610f chore: make send resolved notifs configurable (#4833) 2024-04-15 13:46:12 +05:30
Srikanth Chekuri
a9464de62d chore: use last 1day data for apdex latency metric meta (#4846) 2024-04-15 13:37:08 +05:30
Yunus M
57bfdedfe1 feat: send event if users click in facing issues button in get started (#4859)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-04-15 13:26:20 +05:30
SagarRajput-7
7bdc9c0cb0 fix: fixed sidenav alignment with and without get-started (#4829) 2024-04-15 11:40:40 +05:30
SagarRajput-7
0d5934d56b chore: added test cases for Logs (#4828)
* chore: add test cases for Logs

* chore: add test cases for Logs - explorer

* chore: add test cases for Logs - toolbarAction

* chore: add test cases for Logs - list and table view

* chore: add test cases for Logs - list and table view

* chore: code fix
2024-04-15 11:30:25 +05:30
Vikrant Gupta
3a5a61aff9 fix: wrong payload being sent in the dashboard payload (#4854)
* fix: wrong payload being sent in the dashboard payload

* fix: sync the update set dashboard function

* fix: syncronise the var updates

* fix: jest test cases

* fix: added review comments

* fix: do not make query range API call until the queue is empty

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-04-15 11:11:14 +05:30
Rajat Dabade
a54b7baa7d feat: add support for pie chart panel type (#4751) 2024-04-13 09:55:02 +05:30
Prashant Shahi
cd63dd972d chore(pre-release): 📌 pin versions: SigNoz 0.43.0, SigNoz OtelCollector 0.88.20
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-10 23:55:39 +05:45
Nityananda Gohain
389058b9b4 feat: allow query restrictions for log queries (#4778)
* feat: allow query restrictions for log queries

* fix: error check

* fix: set default only if not present

* chore: add error log for query restriction error

* fix: add limtations for traces

* fix: fix wrapper

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-04-10 17:25:57 +05:30
Rajat Dabade
27e412d1ee refactor: removed the error body (#4850)
* refactor: removed the error body

* refactor: check for error response body condition

* refactor: empty object check
2024-04-10 15:19:01 +05:30
Vikrant Gupta
03dccb0101 fix: trace page breaking (#4844)
* fix: trace page breaking

* fix: crash on reload

* fix: hide update button for viewer role in collapsed mode
2024-04-09 19:20:08 +05:30
Rajat Dabade
25b74b48a5 [Fix]: selected time in right container to be used in query-range (#4842) 2024-04-09 13:48:54 +05:30
Vikrant Gupta
6815a96d29 feat: support of changing panel type in dashboards (#4759)
* feat: support of changing panel type in dashboards

* feat: add handle query change function

* feat: last bit of minor change

* feat: apply current query updates to superset query

* feat: pr cleanup

* feat: handle list type change

* fix: build issues

* fix: changes required due to refactor

* fix: handle offset and page size for list queries

* feat: handle functions propagation

* feat: handle the spaceAggregation value to retain

* fix: handle list panel type changes

* feat: handle removing the graph list from the side selection in case of metrics

* feat: handle list type qb changes

* feat: handle page breaking

* feat: pick dataSource from newQUeryItem

* feat: handle page reload
2024-04-09 13:36:19 +05:30
SagarRajput-7
e9bb05cc5d fix: added billing page condition to not show when its not either cloud or enterprise (#4827) 2024-04-09 12:05:39 +05:30
dependabot[bot]
31c0b94ae6 chore(deps): bump webpack-dev-middleware in /frontend (#4838)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 11:58:39 +05:30
Vikrant Gupta
59c242961f feat: revamp integration flow (#4832)
* feat: revamp integration flow

* feat: final design changes

* feat: make test connection button grey
2024-04-09 11:29:54 +05:30
Yunus M
872ed9e963 fix: time preference from the panel should be used to fetch data (#4836) 2024-04-09 11:05:49 +05:30
Yunus M
d6cd155988 feat: update styles for toolbar (#4824) 2024-04-08 10:52:58 +05:30
Prashant Shahi
7f4a61ffb1 Merge pull request #4822 from SigNoz/fix/heartbeat-ratelimit
fix: avoid rate limiting heartbeat events
2024-04-06 01:24:36 +05:45
makeavish
7737d513a7 fix: avoid rate limiting heartbeat events 2024-04-06 00:54:14 +05:30
Vishal Sharma
2bd666efae fix: query range API validation (#4821) 2024-04-05 23:57:11 +05:30
Srikanth Chekuri
d98265f345 chore: add log comment for prom queries (#4819) 2024-04-05 21:11:53 +05:30
Vikrant Gupta
b480ff1e48 revert: prevent stage and run query to apply legends (#4816) 2024-04-05 02:58:09 +05:30
Vishal Sharma
af353b9340 fix: sidebar jitter (#4815) 2024-04-04 20:53:36 +05:30
Raj Kamal Singh
96e7505922 Chore: logs pipelines cover all available processors with tests (#4454)
* chore: add explicit happy case test for regex parsing processor

* chore: add explicit happy case test for grok parsing processor

* chore: add explicit happy case test for JSON parsing processor

* chore: log pipelines: move trace parser test to processors_test.go

* chore: add explicit happy case test for ADD processor

* chore: add explicit happy case test for remove processor

* chore: add explicit happy case test for copy processor

* chore: add explicit happy case test for move processor

* fix: fix broken grok parser test: change test int value to int64

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2024-04-04 18:48:01 +05:30
Yunus M
8f6f2f0018 feat: views toolbar visibility, light mode (#4813) 2024-04-04 18:47:41 +05:30
Yunus M
1f25d386df feat: retry on chunk load error (#4803)
* feat: retry on chunk load error

* feat: consistent return

* feat: use lazyRetry for help tooltip

* feat: enable sourcemaps
2024-04-04 18:00:50 +05:30
SagarRajput-7
2d7a3733da fix: [SIG-578]: changed color of function block connector (#4800) 2024-04-04 16:45:59 +05:30
SagarRajput-7
ff2a3bc4b0 fix: [SIG-577]: QB functions - states not clearing (#4810) 2024-04-04 16:45:15 +05:30
Vikrant Gupta
33383a4503 fix: alerts tag popup vibrating (#4812) 2024-04-04 13:58:26 +05:30
CheetoDa
f05b94c01e Merge pull request #4799 from SigNoz/java-instructions-update
chore: fixed java instructions
2024-04-04 13:14:18 +05:30
CheetoDa
fd632f9952 Merge pull request #4798 from SigNoz/otel-version-change
feat: updated otel version number
2024-04-04 13:14:04 +05:30
CheetoDa
fd84d7b492 Merge pull request #4797 from SigNoz/docker-instructions
Docker instructions
2024-04-04 13:13:48 +05:30
Vikrant Gupta
e4808e585a fix: app content overlapping sidenav (#4811) 2024-04-04 12:46:33 +05:30
CheetoDa
5cfeb56f9c Created a variable for OTel version 2024-04-04 12:13:19 +05:30
CheetoDa
b947f823d7 chore: minor fix 2024-04-04 12:02:52 +05:30
CheetoDa
1520c1c57d Merge branch 'develop' into otel-version-change 2024-04-04 12:00:09 +05:30
CheetoDa
f8477981d8 chore: fixed review comments 2024-04-04 11:53:46 +05:30
Vishal Sharma
9b1d596816 chore: update events (#4808)
* chore: update events

* Update ee/query-service/app/server.go

Co-authored-by: Prashant Shahi <prashant@signoz.io>

---------

Co-authored-by: Prashant Shahi <prashant@signoz.io>
2024-04-04 11:31:27 +05:30
Yunus M
6a4aa9a956 QB - Logs - Enable TimeShift function (#4792)
* feat: qb - logs - enable time shift function

* feat: qb - logs - enable time shift function

* feat: show functions for logs in v3 version too
2024-04-04 11:05:58 +05:30
Nityananda Gohain
a7b0ef55ad fix: querier v2 synced and tablePanel result processor updated (#4807) 2024-04-03 17:52:45 +05:30
Srikanth Chekuri
87534b6fb6 fix: incorrect error rate query (#4805) 2024-04-03 16:42:00 +05:30
Vikrant Gupta
c76cef47ba fix: remove integrations page view and add event for the same (#4802) 2024-04-03 12:31:38 +05:30
CheetoDa
3276dfa03e chore: fixed java instructions 2024-04-03 04:42:19 +05:30
CheetoDa
1a14cc305c feat: updated otel version number 2024-04-03 04:28:15 +05:30
CheetoDa
0c7e63d735 chore: saved unsaved files 2024-04-03 04:08:46 +05:30
CheetoDa
eb74cb4c5e feat: docker completed 2024-04-03 04:03:00 +05:30
hulk
a47d3289d0 fix: typo in the log message (#4769) 2024-04-02 20:39:05 +05:30
Yunus M
8ad827130e feat: docked sidebar (#4794)
* feat: docked sidebar

* feat: update styles
2024-04-02 19:43:03 +05:30
CheetoDa
93bdfd3d83 chore: merged latest develop 2024-04-02 18:19:20 +05:30
CheetoDa
22d8889a07 feat: added docker instructions 2024-04-02 18:05:39 +05:30
Vikrant Gupta
7c93944d40 fix: chunk load webpack error (#4795) 2024-04-02 17:55:36 +05:30
Rajat Dabade
ec9dbb6853 Dashboard Clean up and list view improvement. (#4675)
* refactor: initial setup

* refactor: created panelWrapper to separate panel data

* fix: type error

* fix: the dimension issue for graphs

* refactor: done with table value uplot panels

* refactor: done with logs panel component

* refactor: updated props for log panel component

* fix: query range duplicate issue for logs

* refactor: trace list view done

* fix: full view support

* refactor: done with edit mode for panels

* refactor: type and props

* refactor: reduce an extra api call on edit for list view

* refactor: done with full graph visibility handler

* refactor: removed commented code

* refactor: removed commented code

* fix: build failure

* refactor: updated service layer graphs

* refactor: updated top level oparation query key

* refactor: added drag select

* refactor: done with drag select in chart

* refactor: code cleanup

* refactor: legend should not need stage and run query
2024-04-02 16:40:41 +05:30
Vikrant Gupta
7a7d814288 fix: sidenav items overlapping in small screens (#4789) 2024-04-02 12:38:10 +05:30
Vikrant Gupta
3babce3ecf fix: added dashboard and QB shortcuts to the sidenav (#4791) 2024-04-02 11:31:42 +05:30
Yunus M
1610b95b84 feat: onboarding flow - enable users to submit request for a new data… (#4786)
* feat: onboarding flow - enable users to submit request for a new data source , environment

* chore: request data source to be available for all modules

* chore: remove hardcoded value
2024-04-01 19:09:16 +05:30
Vishal Sharma
8c02f8ec31 chore: rate limit param (#4785) 2024-04-01 15:06:38 +05:30
Nityananda Gohain
5e0e9da6c4 fix: hotfix bug in enhance query (#4783) 2024-04-01 14:51:40 +05:30
Vikrant Gupta
51abe71421 fix: do not move to next step if env not selected in onboarding (#4784) 2024-04-01 13:56:59 +05:30
Vikrant Gupta
00d74bfebb feat: add integrations to the side-nav for cloud users (#4756)
* feat: add integrations to the side-nav for cloud users

* feat: change the route from integrations/installed to /integrations

* feat: light mode table color

* feat: increase the width of the integrations panel by 25 percent

* feat: added telemetry constants and page view

* feat: added telemetry events for integrations

* feat: address review comments
2024-04-01 12:40:15 +05:30
Raj Kamal Singh
39e0ef68ca chore: integration instructions: add typical log file locations on macOS (#4779) 2024-04-01 12:06:08 +05:30
Ankit Nayan
cff20f88cd merging main 2024-03-30 18:30:46 +01:00
Prashant Shahi
4fbb71484d Merge pull request #4755 from SigNoz/release/v0.42.0
Release/v0.42.0
2024-03-27 23:40:08 +05:45
Prashant Shahi
f8e8132b58 chore(signoz): 📌 pin versions: SigNoz 0.42.0, SigNoz OtelCollector 0.88.17
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-03-27 23:23:15 +05:45
Prashant Shahi
a1dd170641 Merge branch 'main' into release/v0.42.0 2024-03-27 23:17:30 +05:45
Prashant Shahi
fe2ddf9d60 Merge branch 'develop' into main-public 2024-03-27 23:15:57 +05:45
Prashant Shahi
dfc99a7756 Revert "Revert "Explorer Toolbar maximised and minimised (#4656)" (#4705)"
This reverts commit c04d0e9419.
2024-03-27 23:15:40 +05:45
Prashant Shahi
c2556facc2 Merge branch 'main' into release/v0.42.0 2024-03-27 22:49:48 +05:45
Prashant Shahi
2a7ad596a1 Merge pull request #4707 from SigNoz/release/v0.41.1
chore(pre-release): 📌 pin versions: SigNoz 0.41.1
2024-03-15 02:19:23 +05:30
Prashant Shahi
6c455ab5ce Merge branch 'main' into release/v0.41.1 2024-03-15 02:11:34 +05:30
Prashant Shahi
7c062163a1 chore(release): 📌 pin versions: SigNoz 0.41.1
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-03-15 02:24:09 +05:45
Prashant Shahi
d6a256247c Merge pull request #4706 from SigNoz/release/v0.41.1
Release/v0.41.1
2024-03-15 02:00:37 +05:30
Prashant Shahi
0e2c699518 Merge branch 'main' into release/v0.41.1 2024-03-15 01:46:02 +05:30
Vikrant Gupta
c04d0e9419 Revert "Explorer Toolbar maximised and minimised (#4656)" (#4705)
This reverts commit aadb962b6c.

(cherry picked from commit cf22039562)
2024-03-15 01:57:03 +05:45
Prashant Shahi
d0feff00a7 Merge pull request #4690 from SigNoz/release/v0.41.0
Release/v0.41.0
2024-03-14 00:50:36 +05:30
Prashant Shahi
6c2a3d5d43 chore(signoz): 📌 pin versions: SigNoz 0.41.0, SigNoz OtelCollector 0.88.15
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-03-14 00:23:51 +05:45
Prashant Shahi
914b035b3f Merge branch 'main' into release/v0.41.0 2024-03-14 00:23:02 +05:45
Prashant Shahi
71c4fcc382 Merge pull request #4616 from SigNoz/release/v0.40.0
Release/v0.40.0
2024-02-28 19:46:06 +05:30
Prashant Shahi
9af1c2320b chore(signoz): 📌 pin versions: SigNoz 0.40.0, SigNoz OtelCollector 0.88.14
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-02-28 19:13:23 +05:45
Prashant Shahi
cdabf9060e Merge branch 'main' into release/v0.40.0 2024-02-28 19:11:34 +05:45
Prashant Shahi
f069ecdb76 Merge pull request #4582 from SigNoz/release/v0.39.1
Release/v0.39.1
2024-02-21 17:53:50 +05:30
Prashant Shahi
493aef0241 chore(signoz): 📌 pin versions: SigNoz 0.39.1, SigNoz OtelCollector 0.88.13
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-02-21 16:22:48 +05:45
441 changed files with 10006 additions and 3817 deletions

View File

@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.39.0
image: signoz/query-service:0.43.0
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.39.0
image: signoz/frontend:0.43.0
deploy:
restart_policy:
condition: on-failure
@@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.88.12
image: signoz/signoz-otel-collector:0.88.20
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.12
image: signoz/signoz-schema-migrator:0.88.20
deploy:
restart_policy:
condition: on-failure

View File

@@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.20}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -81,7 +81,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.88.12
image: signoz/signoz-otel-collector:0.88.20
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.39.0}
image: signoz/query-service:${DOCKER_TAG:-0.43.0}
container_name: signoz-query-service
command:
[
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.39.0}
image: signoz/frontend:${DOCKER_TAG:-0.43.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.20}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -229,7 +229,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.12}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.20}
container_name: signoz-otel-collector
command:
[

View File

@@ -152,7 +152,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
// PAT APIs
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)

View File

@@ -1,236 +0,0 @@
package api
import (
"bytes"
"fmt"
"net/http"
"sync"
"text/template"
"time"
"go.signoz.io/signoz/pkg/query-service/app/metrics"
"go.signoz.io/signoz/pkg/query-service/app/parser"
"go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate"
"go.uber.org/zap"
)
func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.CustomMetricsFunction) {
zap.L().Info("CustomMetricsFunction feature is not enabled in this plan")
ah.APIHandler.QueryRangeMetricsV2(w, r)
return
}
metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r)
if apiErrorObj != nil {
zap.L().Error("Error in parsing metric query params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}
// prometheus instant query needs same timestamp
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.PROM {
metricsQueryRangeParams.Start = metricsQueryRangeParams.End
}
// round up the end to nearest multiple
if metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER {
end := (metricsQueryRangeParams.End) / 1000
step := metricsQueryRangeParams.Step
metricsQueryRangeParams.End = (end / step * step) * 1000
}
type channelResult struct {
Series []*basemodel.Series
TableName string
Err error
Name string
Query string
}
execClickHouseQueries := func(queries map[string]string) ([]*basemodel.Series, []string, error, map[string]string) {
var seriesList []*basemodel.Series
var tableName []string
ch := make(chan channelResult, len(queries))
var wg sync.WaitGroup
for name, query := range queries {
wg.Add(1)
go func(name, query string) {
defer wg.Done()
seriesList, tableName, err := ah.opts.DataConnector.GetMetricResultEE(r.Context(), query)
for _, series := range seriesList {
series.QueryName = name
}
if err != nil {
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query}
return
}
ch <- channelResult{Series: seriesList, TableName: tableName}
}(name, query)
}
wg.Wait()
close(ch)
var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel
for r := range ch {
if r.Err != nil {
errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue
}
seriesList = append(seriesList, r.Series...)
tableName = append(tableName, r.TableName)
}
if len(errs) != 0 {
return nil, nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
}
return seriesList, tableName, nil, nil
}
execPromQueries := func(metricsQueryRangeParams *basemodel.QueryRangeParamsV2) ([]*basemodel.Series, error, map[string]string) {
var seriesList []*basemodel.Series
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
var wg sync.WaitGroup
for name, query := range metricsQueryRangeParams.CompositeMetricQuery.PromQueries {
if query.Disabled {
continue
}
wg.Add(1)
go func(name string, query *basemodel.PromQuery) {
var seriesList []*basemodel.Series
defer wg.Done()
tmpl := template.New("promql-query")
tmpl, tmplErr := tmpl.Parse(query.Query)
if tmplErr != nil {
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
return
}
var queryBuf bytes.Buffer
tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables)
if tmplErr != nil {
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
return
}
query.Query = queryBuf.String()
queryModel := basemodel.QueryRangeParams{
Start: time.UnixMilli(metricsQueryRangeParams.Start),
End: time.UnixMilli(metricsQueryRangeParams.End),
Step: time.Duration(metricsQueryRangeParams.Step * int64(time.Second)),
Query: query.Query,
}
promResult, _, err := ah.opts.DataConnector.GetQueryRangeResult(r.Context(), &queryModel)
if err != nil {
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query}
return
}
matrix, _ := promResult.Matrix()
for _, v := range matrix {
var s basemodel.Series
s.QueryName = name
s.Labels = v.Metric.Copy().Map()
for _, p := range v.Floats {
s.Points = append(s.Points, basemodel.MetricPoint{Timestamp: p.T, Value: p.F})
}
seriesList = append(seriesList, &s)
}
ch <- channelResult{Series: seriesList}
}(name, query)
}
wg.Wait()
close(ch)
var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel
for r := range ch {
if r.Err != nil {
errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue
}
seriesList = append(seriesList, r.Series...)
}
if len(errs) != 0 {
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
}
return seriesList, nil, nil
}
var seriesList []*basemodel.Series
var tableName []string
var err error
var errQuriesByName map[string]string
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
case basemodel.QUERY_BUILDER:
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME)
if runQueries.Err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: runQueries.Err}, nil)
return
}
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(runQueries.Queries)
case basemodel.CLICKHOUSE:
queries := make(map[string]string)
for name, chQuery := range metricsQueryRangeParams.CompositeMetricQuery.ClickHouseQueries {
if chQuery.Disabled {
continue
}
tmpl := template.New("clickhouse-query")
tmpl, err := tmpl.Parse(chQuery.Query)
if err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
return
}
var query bytes.Buffer
// replace go template variables
querytemplate.AssignReservedVars(metricsQueryRangeParams)
err = tmpl.Execute(&query, metricsQueryRangeParams.Variables)
if err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
return
}
queries[name] = query.String()
}
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(queries)
case basemodel.PROM:
seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams)
default:
err = fmt.Errorf("invalid query type")
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, errQuriesByName)
return
}
if err != nil {
apiErrObj := &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
len(seriesList) > 1 &&
(metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER ||
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.CLICKHOUSE) {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: fmt.Errorf("invalid: query resulted in more than one series for value type")}, nil)
return
}
type ResponseFormat struct {
ResultType string `json:"resultType"`
Result []*basemodel.Series `json:"result"`
TableName []string `json:"tableName"`
}
resp := ResponseFormat{ResultType: "matrix", Result: seriesList, TableName: tableName}
ah.Respond(w, resp)
}

View File

@@ -10,6 +10,7 @@ import (
"net/http"
_ "net/http/pprof" // http profiler
"os"
"regexp"
"time"
"github.com/gorilla/handlers"
@@ -328,7 +329,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
r.Use(loggingMiddleware)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterMetricsRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
@@ -393,13 +393,14 @@ func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFrom := "/api/v3/query_range"
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
data := map[string]interface{}{}
var postData *v3.QueryRangeParamsV3
if path == pathToExtractBodyFrom && (r.Method == "POST") {
if (r.Method == "POST") && ((path == pathToExtractBodyFromV3) || (path == pathToExtractBodyFromV4)) {
if r.Body != nil {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
@@ -417,6 +418,25 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
return nil, false
}
referrer := r.Header.Get("Referer")
dashboardMatched, err := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the referrer", zap.Error(err))
}
alertMatched, err := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the alert: ", zap.Error(err))
}
logsExplorerMatched, err := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the logs explorer: ", zap.Error(err))
}
traceExplorerMatched, err := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the trace explorer: ", zap.Error(err))
}
signozMetricsUsed := false
signozLogsUsed := false
signozTracesUsed := false
@@ -445,6 +465,20 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
data["tracesUsed"] = signozTracesUsed
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
// switch case to set data["screen"] based on the referrer
switch {
case dashboardMatched:
data["screen"] = "panel"
case alertMatched:
data["screen"] = "alert"
case logsExplorerMatched:
data["screen"] = "logs-explorer"
case traceExplorerMatched:
data["screen"] = "traces-explorer"
default:
data["screen"] = "unknown"
return data, true
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
@@ -472,7 +506,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
queryRangeV3data, metadataExists := extractQueryRangeV3Data(path, r)
queryRangeData, metadataExists := extractQueryRangeData(path, r)
getActiveLogs(path, r)
lrw := NewLoggingResponseWriter(w)
@@ -480,7 +514,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
if metadataExists {
for key, value := range queryRangeV3data {
for key, value := range queryRangeData {
data[key] = value
}
}

3
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Sentry Config File
.env.sentry-build-plugin

View File

@@ -41,9 +41,12 @@
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "7.102.1",
"@sentry/webpack-plugin": "2.14.2",
"@sentry/webpack-plugin": "2.16.0",
"@signozhq/design-tokens": "0.0.8",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
@@ -121,6 +124,7 @@
"web-vitals": "^0.2.4",
"webpack": "5.88.2",
"webpack-dev-server": "^4.15.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
"browserslist": {

View File

@@ -15,6 +15,7 @@
"button_test_channel": "Test",
"button_return": "Back",
"field_channel_name": "Name",
"field_send_resolved": "Send resolved alerts",
"field_channel_type": "Type",
"field_webhook_url": "Webhook URL",
"field_slack_recipient": "Recipient",

View File

@@ -15,6 +15,7 @@
"button_test_channel": "Test",
"button_return": "Back",
"field_channel_name": "Name",
"field_send_resolved": "Send resolved alerts",
"field_channel_type": "Type",
"field_webhook_url": "Webhook URL",
"field_slack_recipient": "Recipient",

View File

@@ -48,5 +48,5 @@
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations"
"INTEGRATIONS": "SigNoz | Integrations"
}

View File

@@ -147,7 +147,11 @@ function App(): JSX.Element {
}
}
if (isOnBasicPlan || (isLoggedInState && role && role !== 'ADMIN')) {
if (
isOnBasicPlan ||
(isLoggedInState && role && role !== 'ADMIN') ||
!(isCloudUserVal || isEECloudUser())
) {
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
setRoutes(newRoutes);
}

View File

@@ -197,11 +197,3 @@ export const InstalledIntegrations = Loadable(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const IntegrationsMarketPlace = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
),
);

View File

@@ -15,7 +15,6 @@ import {
ErrorDetails,
IngestionSettings,
InstalledIntegrations,
IntegrationsMarketPlace,
LicensePage,
ListAllALertsPage,
LiveLogs,
@@ -338,18 +337,11 @@ const routes: AppRoutes[] = [
key: 'SHORTCUTS',
},
{
path: ROUTES.INTEGRATIONS_INSTALLED,
path: ROUTES.INTEGRATIONS,
exact: true,
component: InstalledIntegrations,
isPrivate: true,
key: 'INTEGRATIONS_INSTALLED',
},
{
path: ROUTES.INTEGRATIONS_MARKETPLACE,
exact: true,
component: IntegrationsMarketPlace,
isPrivate: true,
key: 'INTEGRATIONS_MARKETPLACE',
key: 'INTEGRATIONS',
},
];

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
email_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
to: props.to,
html: props.html,
headers: props.headers,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
msteams_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
pagerduty_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
routing_key: props.routing_key,
client: props.client,
client_url: props.client_url,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
slack_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_url: props.api_url,
channel: props.channel,
title: props.title,

View File

@@ -30,7 +30,7 @@ const create = async (
name: props.name,
webhook_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
url: props.api_url,
http_config: httpConfig,
},

View File

@@ -12,7 +12,7 @@ const editEmail = async (
name: props.name,
email_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
to: props.to,
html: props.html,
headers: props.headers,

View File

@@ -12,7 +12,7 @@ const editMsTeams = async (
name: props.name,
msteams_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,

View File

@@ -12,7 +12,7 @@ const editOpsgenie = async (
name: props.name,
opsgenie_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_key: props.api_key,
description: props.description,
priority: props.priority,

View File

@@ -12,7 +12,7 @@ const editPager = async (
name: props.name,
pagerduty_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
routing_key: props.routing_key,
client: props.client,
client_url: props.client_url,

View File

@@ -12,7 +12,7 @@ const editSlack = async (
name: props.name,
slack_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_url: props.api_url,
channel: props.channel,
title: props.title,

View File

@@ -29,7 +29,7 @@ const editWebhook = async (
name: props.name,
webhook_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
url: props.api_url,
http_config: httpConfig,
},

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { EventSuccessPayloadProps } from 'types/api/events/types';
const logEvent = async (
eventName: string,
attributes: Record<string, unknown>,
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/event', {
eventName,
attributes,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default logEvent;

View File

@@ -1,4 +1,4 @@
import axios from 'api';
import { ApiV4Instance } from 'api';
import { AxiosResponse } from 'axios';
import { MetricMetaProps } from 'types/api/metrics/getApDex';
@@ -6,4 +6,6 @@ export const getMetricMeta = (
metricName: string,
servicename: string,
): Promise<AxiosResponse<MetricMetaProps>> =>
axios.get(`/metric_meta?metricName=${metricName}&serviceName=${servicename}`);
ApiV4Instance.get(
`/metric/metric_metadata?metricName=${metricName}&serviceName=${servicename}`,
);

View File

@@ -1,27 +0,0 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricNameProps,
MetricNamesPayloadProps,
} from 'types/api/metrics/getMetricName';
export const getMetricName = async (
props: MetricNameProps,
): Promise<SuccessResponse<MetricNamesPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/list?match=${props || ''}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,6 +1,7 @@
import { ApiV2Instance as axios } from 'api';
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
TagKeyProps,
@@ -8,15 +9,19 @@ import {
TagValueProps,
TagValuesPayloadProps,
} from 'types/api/metrics/getResourceAttributes';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
export const getResourceAttributesTagKeys = async (
props: TagKeyProps,
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/tagKey?metricName=${props.metricName}${
props.match ? `&match=${props.match}` : ''
}`,
`/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator: MetricAggregateOperator.RATE,
searchText: props.match,
dataSource: DataSource.METRICS,
aggregateAttribute: props.metricName,
})}`,
);
return {
@@ -35,7 +40,13 @@ export const getResourceAttributesTagValues = async (
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/tagValue?metricName=${props.metricName}&tagKey=${props.tagKey}`,
`/autocomplete/attribute_values?${createQueryParams({
aggregateOperator: MetricAggregateOperator.RATE,
dataSource: DataSource.METRICS,
aggregateAttribute: props.metricName,
attributeKey: props.tagKey,
searchText: '',
})}`,
);
return {

View File

@@ -1,9 +1,10 @@
import { ComponentType, lazy, LazyExoticComponent } from 'react';
import { lazyRetry } from 'utils/lazyWithRetries';
function Loadable(importPath: {
(): LoadableProps;
}): LazyExoticComponent<LazyComponent> {
return lazy(() => importPath());
return lazy(() => lazyRetry(() => importPath()));
}
type LazyComponent = ComponentType<Record<string, unknown>>;

View File

@@ -1,10 +1,10 @@
import './LabelColumn.styles.scss';
import { Popover, Tag } from 'antd';
import { popupContainer } from 'utils/selectPopupContainer';
import { LabelColumnProps } from './TableRenderer.types';
import TagWithToolTip from './TagWithToolTip';
import { getLabelAndValueContent } from './utils';
function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
const newLabels = labels.length > 3 ? labels.slice(0, 3) : labels;
@@ -19,19 +19,17 @@ function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
)}
{remainingLabels.length > 0 && (
<Popover
getPopupContainer={popupContainer}
placement="bottomRight"
showArrow={false}
content={
<div>
{labels.map(
(label: string): JSX.Element => (
<TagWithToolTip
key={label}
label={label}
color={color}
value={value}
/>
<div key={label}>
<Tag className="label-column--tag" color={color}>
{getLabelAndValueContent(label, value && value[label])}
</Tag>
</div>
),
)}
</div>

View File

@@ -38,6 +38,16 @@ export const getLabelRenderingValue = (
return label;
};
export const getLabelAndValueContent = (
label: string,
value?: string,
): string => {
if (value) {
return `${label}: ${value}`;
}
return `${label}`;
};
interface GeneratorResizeTableColumnsProp<T> {
baseColumnOptions: ColumnsType<T>;
dynamicColumnOption: { key: string; columnOption: ColumnType<T> }[];

View File

@@ -29,6 +29,7 @@ export const getComponentForPanelType = (
[PANEL_TYPES.LIST]:
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
[PANEL_TYPES.BAR]: Uplot,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
};

View File

@@ -285,6 +285,7 @@ export enum PANEL_TYPES {
LIST = 'list',
TRACE = 'trace',
BAR = 'bar',
PIE = 'pie',
EMPTY_WIDGET = 'EMPTY_WIDGET',
}

View File

@@ -2,7 +2,7 @@
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const queryFunctionOptions: SelectOption<string, string>[] = [
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.CUTOFF_MIN,
label: 'Cut Off Min',
@@ -65,6 +65,12 @@ export const queryFunctionOptions: SelectOption<string, string>[] = [
},
];
export const logsQueryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
];
interface QueryFunctionConfigType {
[key: string]: {
showInput: boolean;

View File

@@ -51,9 +51,7 @@ const ROUTES = {
TRACES_SAVE_VIEWS: '/traces/saved-views',
WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts',
INTEGRATIONS_BASE: '/integrations',
INTEGRATIONS_INSTALLED: '/integrations/installed',
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
INTEGRATIONS: '/integrations',
} as const;
export default ROUTES;

View File

@@ -0,0 +1,3 @@
export enum SESSIONSTORAGE {
RETRY_LAZY_REFRESHED = 'retry-lazy-refreshed',
}

View File

@@ -9,9 +9,10 @@ export const DashboardShortcuts = {
export const DashboardShortcutsName = {
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
DiscardChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+d`,
};
export const DashboardShortcutsDescription = {
SaveChanges: 'Save Changes',
DiscardChanges: 'Discard Changes',
SaveChanges: 'Save Changes for panel',
DiscardChanges: 'Discard Changes for panel',
};

View File

@@ -13,5 +13,5 @@ export const QBShortcutsName = {
};
export const QBShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the query',
StageAndRunQuery: 'Stage and Run the current query',
};

View File

@@ -4,8 +4,9 @@
width: 100%;
.app-content {
width: 100%;
width: calc(100% - 64px);
overflow: auto;
z-index: 0;
.content-container {
position: relative;
@@ -16,6 +17,12 @@
width: 100%;
}
}
&.docked {
.app-content {
width: calc(100% - 240px);
}
}
}
.isDarkMode {

View File

@@ -311,7 +311,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
)}
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
<Flex
className={cx(
'app-layout',
isDarkMode ? 'darkMode' : 'lightMode',
!collapsed && !renderFullScreen ? 'docked' : '',
)}
>
{isToDisplayLayout && !renderFullScreen && (
<SideNav
licenseData={licenseData}

View File

@@ -3,6 +3,7 @@ import Spinner from 'components/Spinner';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { lazy, Suspense, useMemo } from 'react';
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
import { lazyRetry } from 'utils/lazyWithRetries';
import ErrorLink from './ErrorLink';
import LinkContainer from './Link';
@@ -17,8 +18,9 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
const items = sortedConfig.map((item) => {
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
const Component = lazy(
() => import(`@ant-design/icons/es/icons/${iconName}.js`),
const Component = lazy(() =>
lazyRetry(() => import(`@ant-design/icons/es/icons/${iconName}.js`)),
);
return {
key: item.text + item.href,

View File

@@ -53,6 +53,7 @@ function CreateAlertChannels({
EmailChannel
>
>({
send_resolved: true,
text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
@@ -119,7 +120,7 @@ function CreateAlertChannels({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
}),
@@ -158,7 +159,7 @@ function CreateAlertChannels({
let request: WebhookChannel = {
api_url: selectedConfig?.api_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
};
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
@@ -226,7 +227,7 @@ function CreateAlertChannels({
return {
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
routing_key: selectedConfig?.routing_key || '',
client: selectedConfig?.client || '',
client_url: selectedConfig?.client_url || '',
@@ -274,7 +275,7 @@ function CreateAlertChannels({
() => ({
api_key: selectedConfig?.api_key || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
description: selectedConfig?.description || '',
message: selectedConfig?.message || '',
priority: selectedConfig?.priority || '',
@@ -312,7 +313,7 @@ function CreateAlertChannels({
const prepareEmailRequest = useCallback(
() => ({
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
to: selectedConfig?.to || '',
html: selectedConfig?.html || '',
headers: selectedConfig?.headers || {},
@@ -350,7 +351,7 @@ function CreateAlertChannels({
() => ({
webhook_url: selectedConfig?.webhook_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
}),

View File

@@ -72,7 +72,7 @@ function EditAlertChannels({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,
@@ -115,7 +115,7 @@ function EditAlertChannels({
return {
api_url: selectedConfig?.api_url || '',
name: name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
username,
password,
id,
@@ -284,7 +284,7 @@ function EditAlertChannels({
() => ({
webhook_url: selectedConfig?.webhook_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,

View File

@@ -1,10 +1,10 @@
.hide-update {
left: calc(50% - 41px) !important;
left: calc(50% - 72px) !important;
}
.explorer-update {
position: fixed;
bottom: 24px;
left: calc(50% - 250px);
left: calc(50% - 352px);
display: flex;
align-items: center;
gap: 12px;
@@ -37,7 +37,6 @@
}
}
.explorer-options {
position: fixed;
bottom: 24px;
@@ -78,11 +77,9 @@
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
border: none;
border: 1px solid #1d2023;
color: #c0c1c3;
background-color: #161922;
box-shadow: none !important;

View File

@@ -373,34 +373,42 @@ function ExplorerOptions({
onClick={handleSaveViewModalToggle}
className={isEditDeleteSupported ? '' : 'hidden'}
disabled={viewsIsLoading || isRefetching}
icon={<Disc3 size={16} />}
>
<Disc3 size={16} /> Save this view
Save this view
</Button>
</div>
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
<Tooltip title="Create Alerts">
<Button
disabled={disabled}
shape="round"
onClick={onCreateAlertsHandler}
icon={<ConciergeBell size={16} />}
>
Create an Alert
</Button>
<Button
type="primary"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={16} />}
>
Add to Dashboard
</Button>
</div>
<div className="actions">
<Tooltip title="Hide">
<Button
disabled={disabled}
shape="circle"
onClick={onCreateAlertsHandler}
>
<ConciergeBell size={16} />
</Button>
</Tooltip>
<Tooltip title="Add to Dashboard">
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
<Plus size={16} />
</Button>
</Tooltip>
<Tooltip title="Hide">
<Button disabled={disabled} shape="circle" onClick={hideToolbar}>
<PanelBottomClose size={16} />
</Button>
onClick={hideToolbar}
icon={<PanelBottomClose size={16} />}
/>
</Tooltip>
</div>
</div>
@@ -413,6 +421,7 @@ function ExplorerOptions({
isQueryUpdated={isQueryUpdated}
handleClearSelect={handleClearSelect}
onUpdateQueryHandler={onUpdateQueryHandler}
isEditDeleteSupported={isEditDeleteSupported}
/>
<Modal

View File

@@ -16,6 +16,7 @@ interface DroppableAreaProps {
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
handleClearSelect: () => void;
onUpdateQueryHandler: () => void;
isEditDeleteSupported: boolean;
}
function ExplorerOptionsHideArea({
@@ -25,6 +26,7 @@ function ExplorerOptionsHideArea({
setIsExplorerOptionHidden,
handleClearSelect,
onUpdateQueryHandler,
isEditDeleteSupported,
}: DroppableAreaProps): JSX.Element {
const handleShowExplorerOption = (): void => {
if (setIsExplorerOptionHidden) {
@@ -47,14 +49,16 @@ function ExplorerOptionsHideArea({
icon={<X size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}
className="action-btn"
style={{ background: Color.BG_ROBIN_500 }}
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
{isEditDeleteSupported && (
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}
className="action-btn"
style={{ background: Color.BG_ROBIN_500 }}
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
)}
</div>
)}
<Button

View File

@@ -1,4 +1,4 @@
import { Form, FormInstance, Input, Select, Typography } from 'antd';
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
import { Store } from 'antd/lib/form/interface';
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
import { FeatureKeys } from 'constants/features';
@@ -95,6 +95,22 @@ function FormAlertChannels({
/>
</Form.Item>
<Form.Item
label={t('field_send_resolved')}
labelAlign="left"
name="send_resolved"
>
<Switch
defaultChecked={initialValue?.send_resolved}
onChange={(value): void => {
setSelectedConfig((state) => ({
...state,
send_resolved: value,
}));
}}
/>
</Form.Item>
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
<Select.Option value="slack" key="slack">

View File

@@ -56,8 +56,9 @@ function QuerySection({
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
}}
showFunctions={
alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4
(alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4) ||
alertType === AlertTypes.LOGS_BASED_ALERT
}
version={alertDef.version || 'v3'}
/>

View File

@@ -26,5 +26,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
LIST: false,
TRACE: false,
BAR: true,
PIE: false,
EMPTY_WIDGET: false,
};

View File

@@ -1,62 +1,60 @@
import './WidgetFullView.styles.scss';
import { SyncOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
import { Button, Spin } from 'antd';
import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import {
timeItems,
timePreferance,
} from 'container/NewWidget/RightContainer/timeItems';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useChartMutable } from 'hooks/useChartMutable';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { getLocalStorageGraphVisibilityState } from '../utils';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager';
import { GraphContainer, TimeContainer } from './styles';
import { FullViewProps } from './types';
function FullView({
widget,
fullViewOptions = true,
onClickHandler,
name,
version,
originalName,
yAxisUnit,
onDragSelect,
isDependedDataLoaded = false,
onToggleModelHandler,
parentChartRef,
}: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const location = useLocation();
const fullViewRef = useRef<HTMLDivElement>(null);
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
const { selectedDashboard, isDashboardLocked } = useDashboard();
const getSelectedTime = useCallback(
@@ -74,24 +72,70 @@ function FullView({
const updatedQuery = useStepInterval(widget?.query);
const response = useGetQueryRange(
{
selectedTime: selectedTime.enum,
graphType:
widget.panelTypes === PANEL_TYPES.BAR
? PANEL_TYPES.TIME_SERIES
: widget.panelTypes,
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) {
return {
selectedTime: selectedTime.enum,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(selectedDashboard?.data.variables),
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: widget?.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(selectedDashboard?.data.variables),
},
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
useEffect(() => {
setRequestData((prev) => ({
...prev,
selectedTime: selectedTime.enum,
}));
}, [selectedTime]);
const response = useGetQueryRange(
requestData,
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
{
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
enabled: !isDependedDataLoaded && widget.panelTypes !== PANEL_TYPES.LIST, // Internally both the list view panel has it's own query range api call, so we don't need to call it again
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
enabled: !isDependedDataLoaded,
keepPreviousData: true,
},
);
const onDragSelect = useCallback(
(start: number, end: number): void => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
const { maxTime, minTime } = GetMinMax('custom', [
startTimestamp,
endTimestamp,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
boolean[]
>(Array(response.data?.payload.data.result.length).fill(true));
@@ -118,60 +162,6 @@ function FullView({
response.data.payload.data.result = sortedSeriesData;
}
const chartData = getUPlotChartData(response?.data?.payload, widget.fillSpans);
const isDarkMode = useIsDarkMode();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(response);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, response]);
useEffect(() => {
if (!response.isFetching && fullViewRef.current) {
const width = fullViewRef.current?.clientWidth
? fullViewRef.current.clientWidth - 45
: 700;
const height = fullViewRef.current?.clientWidth
? fullViewRef.current.clientHeight
: 300;
const newChartOptions = getUPlotChartOptions({
id: originalName,
yAxisUnit: yAxisUnit || '',
apiResponse: response.data?.payload,
dimensions: {
height,
width,
},
isDarkMode,
onDragSelect,
graphsVisibilityStates,
setGraphsVisibilityStates,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
panelType: widget.panelTypes,
});
setChartOptions(newChartOptions);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [response.isFetching, graphsVisibilityStates, fullViewRef.current]);
useEffect(() => {
graphsVisibilityStates?.forEach((e, i) => {
fullViewChartRef?.current?.toggleGraph(i, e);
@@ -180,7 +170,7 @@ function FullView({
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
if (response.isFetching) {
if (response.isLoading && widget.panelTypes !== PANEL_TYPES.LIST) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
@@ -189,6 +179,9 @@ function FullView({
<div className="full-view-header-container">
{fullViewOptions && (
<TimeContainer $panelType={widget.panelTypes}>
{response.isFetching && (
<Spin spinning indicator={<LoadingOutlined spin />} />
)}
<TimePreference
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
@@ -214,47 +207,24 @@ function FullView({
})}
ref={fullViewRef}
>
{chartOptions && (
<GraphContainer
style={{
height: isListView ? '100%' : '90%',
}}
isGraphLegendToggleAvailable={canModifyChart}
>
<GridPanelSwitch
panelType={widget.panelTypes}
data={chartData}
options={chartOptions}
onClickHandler={onClickHandler}
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
panelData={response.data?.payload.data.newResult.data.result || []}
query={widget.query}
ref={fullViewChartRef}
thresholds={widget.thresholds}
selectedLogFields={widget.selectedLogFields}
dataSource={widget.query.builder.queryData[0].dataSource}
selectedTracesFields={widget.selectedTracesFields}
selectedTime={selectedTime}
/>
</GraphContainer>
)}
<GraphContainer
style={{
height: isListView ? '100%' : '90%',
}}
isGraphLegendToggleAvailable={canModifyChart}
>
<PanelWrapper
queryResponse={response}
widget={widget}
setRequestData={setRequestData}
isFullViewMode
onToggleModelHandler={onToggleModelHandler}
setGraphVisibility={setGraphsVisibilityStates}
graphVisibility={graphsVisibilityStates}
onDragSelect={onDragSelect}
/>
</GraphContainer>
</div>
{canModifyChart && chartOptions && !isDashboardLocked && (
<GraphManager
data={chartData}
name={originalName}
options={chartOptions}
yAxisUnit={yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
setGraphsVisibilityStates={setGraphsVisibilityStates}
graphsVisibilityStates={graphsVisibilityStates}
lineChartRef={fullViewChartRef}
parentChartRef={parentChartRef}
/>
)}
</div>
);
}

View File

@@ -18,6 +18,7 @@ export const NotFoundContainer = styled.div`
export const TimeContainer = styled.div<Props>`
display: flex;
justify-content: flex-end;
align-items: center;
${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE
? css`

View File

@@ -53,10 +53,8 @@ export interface FullViewProps {
version?: string;
originalName: string;
yAxisUnit?: string;
onDragSelect: (start: number, end: number) => void;
isDependedDataLoaded?: boolean;
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
parentChartRef: GraphManagerProps['lineChartRef'];
}
export interface GraphManagerProps extends UplotProps {

View File

@@ -6,7 +6,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -33,23 +33,20 @@ import FullView from './FullView';
import { Modal } from './styles';
import { WidgetGraphComponentProps } from './types';
import { getLocalStorageGraphVisibilityState } from './utils';
// import { getLocalStorageGraphVisibilityState } from './utils';
function WidgetGraphComponent({
widget,
queryResponse,
errorMessage,
name,
version,
threshold,
headerMenuList,
isWarning,
data,
options,
graphVisibiltyState,
isFetchingResponse,
setRequestData,
onClickHandler,
onDragSelect,
setGraphVisibility,
isFetchingResponse,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@@ -61,12 +58,15 @@ function WidgetGraphComponent({
const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id;
const lineChartRef = useRef<ToggleGraphProps>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
const graphRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!lineChartRef.current) return;
graphVisibiltyState.forEach((state, index) => {
graphVisibility.forEach((state, index) => {
lineChartRef.current?.toggleGraph(index, state);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -210,7 +210,7 @@ function WidgetGraphComponent({
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data.payload.data.result,
name,
name: widget.id,
});
setGraphVisibility(localStoredVisibilityState);
}
@@ -252,7 +252,7 @@ function WidgetGraphComponent({
onBlur={(): void => {
setHovered(false);
}}
id={name}
id={widget.id}
>
<Modal
destroyOnClose
@@ -278,14 +278,12 @@ function WidgetGraphComponent({
className="widget-full-view"
>
<FullView
name={`${name}expanded`}
name={`${widget.id}expanded`}
version={version}
originalName={name}
originalName={widget.id}
widget={widget}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
parentChartRef={lineChartRef}
onDragSelect={onDragSelect}
/>
</Modal>
@@ -305,26 +303,22 @@ function WidgetGraphComponent({
isFetchingResponse={isFetchingResponse}
/>
</div>
{queryResponse.isLoading && <Skeleton />}
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
<Skeleton />
)}
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
<div
className={cx('widget-graph-container', widget.panelTypes)}
ref={graphRef}
>
<GridPanelSwitch
panelType={widget.panelTypes}
data={data}
name={name}
ref={lineChartRef}
options={options}
yAxisUnit={widget.yAxisUnit}
<PanelWrapper
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}
setGraphVisibility={setGraphVisibility}
graphVisibility={graphVisibility}
onClickHandler={onClickHandler}
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
query={widget.query}
thresholds={widget.thresholds}
selectedLogFields={widget.selectedLogFields}
dataSource={widget.query.builder?.queryData[0]?.dataSource}
selectedTracesFields={widget.selectedTracesFields}
onDragSelect={onDragSelect}
/>
</div>
)}

View File

@@ -4,80 +4,47 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import GetMinMax from 'lib/getMinMax';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import isEmpty from 'lodash-es/isEmpty';
import _noop from 'lodash-es/noop';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import EmptyWidget from '../EmptyWidget';
import { MenuItemKeys } from '../WidgetHeader/contants';
import { GridCardGraphProps } from './types';
import { getLocalStorageGraphVisibilityState } from './utils';
import WidgetGraphComponent from './WidgetGraphComponent';
function GridCardGraph({
widget,
name,
onClickHandler = _noop,
headerMenuList = [MenuItemKeys.View],
isQueryEnabled,
threshold,
variables,
fillSpans = false,
version,
onClickHandler,
onDragSelect,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const urlQuery = useUrlQuery();
const location = useLocation();
const {
toScrollWidgetId,
setToScrollWidgetId,
variablesToGetUpdated,
} = useDashboard();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const onDragSelect = useCallback(
(start: number, end: number): void => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
const { maxTime, minTime } = GetMinMax('custom', [
startTimestamp,
endTimestamp,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
@@ -131,15 +98,39 @@ function GridCardGraph({
isVisible &&
!isEmptyWidget &&
isQueryEnabled &&
widget.panelTypes !== PANEL_TYPES.LIST;
isEmpty(variablesToGetUpdated);
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) {
return {
selectedTime: widget?.timePreferance,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(variables),
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval,
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
const queryResponse = useGetQueryRange(
{
selectedTime: widget?.timePreferance,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval,
...requestData,
variables: getDashboardVariables(variables),
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval,
},
version || DEFAULT_ENTITY_VERSION,
{
@@ -151,6 +142,7 @@ function GridCardGraph({
widget?.query,
widget?.panelTypes,
widget.timePreferance,
requestData,
],
retry(failureCount, error): boolean {
if (
@@ -173,15 +165,6 @@ function GridCardGraph({
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
const containerDimensions = useResizeObserver(graphRef);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result,
@@ -189,89 +172,30 @@ function GridCardGraph({
queryResponse.data.payload.data.result = sortedSeriesData;
}
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
const isDarkMode = useIsDarkMode();
const menuList =
widget.panelTypes === PANEL_TYPES.TABLE ||
widget.panelTypes === PANEL_TYPES.LIST
widget.panelTypes === PANEL_TYPES.LIST ||
widget.panelTypes === PANEL_TYPES.PIE
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
: headerMenuList;
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
useEffect(() => {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data?.payload.data.result || [],
name,
});
setGraphVisibility(localStoredVisibilityState);
}, [name, queryResponse.data?.payload.data.result]);
const options = useMemo(
() =>
getUPlotChartOptions({
id: widget?.id,
apiResponse: queryResponse.data?.payload,
dimensions: containerDimensions,
isDarkMode,
onDragSelect,
yAxisUnit: widget?.yAxisUnit,
onClickHandler,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
panelType: widget.panelTypes,
}),
[
widget?.id,
widget?.yAxisUnit,
widget.thresholds,
widget.softMax,
widget.softMin,
queryResponse.data?.payload,
containerDimensions,
isDarkMode,
onDragSelect,
onClickHandler,
minTimeScale,
maxTimeScale,
graphVisibility,
setGraphVisibility,
widget.panelTypes,
],
);
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
{isEmptyLayout ? (
<EmptyWidget />
) : (
<WidgetGraphComponent
data={chartData}
options={options}
widget={widget}
queryResponse={queryResponse}
errorMessage={errorMessage}
isWarning={false}
name={name}
version={version}
onDragSelect={onDragSelect}
threshold={threshold}
headerMenuList={menuList}
onClickHandler={onClickHandler}
graphVisibiltyState={graphVisibility}
setGraphVisibility={setGraphVisibility}
isFetchingResponse={queryResponse.isFetching}
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
/>
)}
</div>

View File

@@ -1,9 +1,9 @@
import { ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponse } from 'types/api';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
@@ -16,35 +16,32 @@ export interface GraphVisibilityLegendEntryProps {
legendEntry: LegendEntryProps[];
}
export interface WidgetGraphComponentProps extends UplotProps {
export interface WidgetGraphComponentProps {
widget: Widgets;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
errorMessage: string | undefined;
name: string;
version?: string;
onDragSelect: (start: number, end: number) => void;
onClickHandler?: OnClickPluginOpts['onClick'];
threshold?: ReactNode;
headerMenuList: MenuItemKeys[];
isWarning: boolean;
graphVisibiltyState: boolean[];
setGraphVisibility: Dispatch<SetStateAction<boolean[]>>;
isFetchingResponse: boolean;
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void;
}
export interface GridCardGraphProps {
widget: Widgets;
name: string;
onDragSelect?: (start: number, end: number) => void;
onClickHandler?: OnClickPluginOpts['onClick'];
threshold?: ReactNode;
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
onClickHandler?: OnClickPluginOpts['onClick'];
isQueryEnabled: boolean;
variables?: Dashboard['data']['variables'];
fillSpans?: boolean;
version?: string;
onDragSelect: (start: number, end: number) => void;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -5,6 +5,12 @@
.react-grid-layout {
border: none !important;
margin-top: 0;
.widget-graph-container {
&.graph {
height: 100%;
}
}
}
}
@@ -23,7 +29,7 @@
.ant-modal-header {
background-color: var(--bg-ink-400);
}
}
}
}
.lightMode {

View File

@@ -3,20 +3,25 @@ import './GridCardLayout.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import isEqual from 'lodash-es/isEqual';
import { FullscreenIcon } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
@@ -45,6 +50,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
} = useDashboard();
const { data } = selectedDashboard || {};
const handle = useFullScreenHandle();
const { pathname } = useLocation();
const dispatch = useDispatch();
const { widgets, variables } = data || {};
@@ -61,6 +68,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
const updateDashboardMutation = useUpdateDashboard();
const { notifications } = useNotifications();
const urlQuery = useUrlQuery();
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
@@ -126,6 +134,23 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
}
};
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.replace(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, pathname, urlQuery],
);
useEffect(() => {
if (
dashboardLayout &&
@@ -200,11 +225,10 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
>
<GridCard
widget={currentWidget || ({ id, query: {} } as Widgets)}
name={currentWidget?.id || ''}
headerMenuList={widgetActions}
variables={variables}
fillSpans={currentWidget?.fillSpans}
version={selectedDashboard?.data?.version}
onDragSelect={onDragSelect}
/>
</Card>
</CardContainer>

View File

@@ -1,10 +1,8 @@
import { ToggleGraphProps } from 'components/Graph/types';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { getComponentForPanelType } from 'constants/panelTypes';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
import { FC, forwardRef, memo, useMemo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
@@ -21,10 +19,7 @@ const GridPanelSwitch = forwardRef<
query,
options,
thresholds,
selectedLogFields,
selectedTracesFields,
dataSource,
selectedTime,
},
ref,
): JSX.Element | null => {
@@ -46,20 +41,8 @@ const GridPanelSwitch = forwardRef<
query,
thresholds,
},
[PANEL_TYPES.LIST]:
dataSource === DataSource.LOGS
? {
selectedLogsFields: selectedLogFields || [],
query,
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
selectedTime,
}
: {
selectedTracesFields: selectedTracesFields || [],
query,
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
selectedTime,
},
[PANEL_TYPES.LIST]: null,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.BAR]: {
data,
@@ -70,19 +53,7 @@ const GridPanelSwitch = forwardRef<
};
return result;
}, [
data,
options,
ref,
yAxisUnit,
thresholds,
panelData,
query,
dataSource,
selectedLogFields,
selectedTime,
selectedTracesFields,
]);
}, [data, options, ref, yAxisUnit, thresholds, panelData, query]);
const Component = getComponentForPanelType(panelType, dataSource) as FC<
PropsTypePropsMap[typeof panelType]

View File

@@ -2,9 +2,7 @@ import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot';
import { GridTableComponentProps } from 'container/GridTableComponent/types';
import { GridValueComponentProps } from 'container/GridValueComponent/types';
import { LogsPanelComponentProps } from 'container/LogsPanelTable/LogsPanelComponent';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { TracesTableComponentProps } from 'container/TracesTableComponent/TracesTableComponent';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { ForwardedRef } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -40,7 +38,8 @@ export type PropsTypePropsMap = {
[PANEL_TYPES.VALUE]: GridValueComponentProps;
[PANEL_TYPES.TABLE]: GridTableComponentProps;
[PANEL_TYPES.TRACE]: null;
[PANEL_TYPES.LIST]: LogsPanelComponentProps | TracesTableComponentProps;
[PANEL_TYPES.PIE]: null;
[PANEL_TYPES.LIST]: null;
[PANEL_TYPES.BAR]: UplotProps & {
ref: ForwardedRef<ToggleGraphProps | undefined>;
};

View File

@@ -608,6 +608,7 @@ function LogsExplorerViews({
className="periscope-btn"
onClick={handleToggleShowFormatOptions}
icon={<Sliders size={14} />}
data-testid="periscope-btn"
/>
{showFormatMenuItems && (

View File

@@ -0,0 +1,151 @@
import { render, RenderResult } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { VirtuosoMockContext } from 'react-virtuoso';
import i18n from 'ReactI18';
import store from 'store';
import LogsExplorerViews from '..';
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
const logExplorerRoute = '/logs/logs-explorer';
const queryRangeURL = 'http://localhost/api/v3/query_range';
const lodsQueryServerRequest = (): void =>
server.use(
rest.post(queryRangeURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(logsQueryRangeSuccessResponse)),
),
);
// mocking the graph components in this test as this should be handled separately
jest.mock(
'container/TimeSeriesView/TimeSeriesView',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function () {
return <div>Time Series Chart</div>;
},
);
jest.mock(
'container/LogsExplorerChart',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function () {
return <div>Histogram Chart</div>;
},
);
jest.mock('constants/panelTypes', () => ({
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
}));
jest.mock('d3-interpolate', () => ({
interpolate: jest.fn(),
}));
jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
__esModule: true,
useGetExplorerQueryRange: jest.fn(),
}));
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
beforeEach(() => {
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
});
});
const renderer = (): RenderResult =>
render(
<MemoryRouter initialEntries={[logExplorerRoute]}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<MockQueryClientProvider>
<QueryBuilderProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews selectedView={SELECTED_VIEWS.SEARCH} showHistogram />
</VirtuosoMockContext.Provider>
</QueryBuilderProvider>
</MockQueryClientProvider>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
describe('LogsExplorerViews -', () => {
it('render correctly with props - list and table', async () => {
lodsQueryServerRequest();
const { queryByText, queryByTestId } = renderer();
expect(queryByTestId('periscope-btn')).toBeInTheDocument();
await userEvent.click(queryByTestId('periscope-btn') as HTMLElement);
expect(document.querySelector('.menu-container')).toBeInTheDocument();
const menuItems = document.querySelectorAll('.menu-items .item');
expect(menuItems.length).toBe(3);
// switch to table view
// eslint-disable-next-line sonarjs/no-duplicate-string
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText(
'{"container_id":"container_id","container_name":"container_name","driver":"driver","eta":"2m0s","location":"frontend","log_level":"INFO","message":"Dispatch successful","service":"frontend","span_id":"span_id","trace_id":"span_id"}',
),
).toBeInTheDocument();
});
it('check isLoading state', async () => {
lodsQueryServerRequest();
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
isLoading: true,
isFetching: false,
});
const { queryByText, queryByTestId } = renderer();
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText(
'Just a bit of patience, just a little bits enough ⎯ were getting your logs!',
),
).toBeInTheDocument();
});
it('check error state', async () => {
lodsQueryServerRequest();
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
isLoading: false,
isFetching: false,
isError: true,
});
const { queryByText, queryByTestId } = renderer();
expect(
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,51 @@
export const logsQueryRangeSuccessNewFormatResponse = {
data: {
result: [],
resultType: '',
newResult: {
status: 'success',
data: {
resultType: '',
result: [
{
queryName: 'A',
series: null,
list: [
{
timestamp: '2024-02-15T21:20:22Z',
data: {
attributes_bool: {},
attributes_float64: {},
attributes_int64: {},
attributes_string: {
container_id: 'container_id',
container_name: 'container_name',
driver: 'driver',
eta: '2m0s',
location: 'frontend',
log_level: 'INFO',
message: 'Dispatch successful',
service: 'frontend',
span_id: 'span_id',
trace_id: 'span_id',
},
body:
'2024-02-15T21:20:22.035Z\tINFO\tfrontend\tDispatch successful\t{"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}',
id: 'id',
resources_string: {
'container.name': 'container_name',
},
severity_number: 0,
severity_text: '',
span_id: '',
trace_flags: 0,
trace_id: '',
},
},
],
},
],
},
},
},
};

View File

@@ -4,82 +4,53 @@ import { Table } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { tableStyles } from 'container/TracesExplorer/ListView/styles';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { Pagination } from 'hooks/queryPagination';
import { useLogsData } from 'hooks/useLogsData';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { FlatLogData } from 'lib/logs/flatLogData';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
Dispatch,
HTMLAttributes,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { getLogPanelColumnsList } from './utils';
import { getLogPanelColumnsList, getNextOrPreviousItems } from './utils';
function LogsPanelComponent({
selectedLogsFields,
query,
selectedTime,
widget,
setRequestData,
queryResponse,
}: LogsPanelComponentProps): JSX.Element {
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [pagination, setPagination] = useState<Pagination>({
offset: 0,
limit: query.builder.queryData[0].limit || 0,
});
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
const updatedQuery = { ...query };
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
tableParams: {
pagination,
},
};
limit: widget.query.builder.queryData[0].limit || 0,
});
useEffect(() => {
setRequestData({
...requestData,
globalSelectedInterval: globalSelectedTime,
setRequestData((prev) => ({
...prev,
tableParams: {
pagination,
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pagination]);
}));
}, [pagination, setRequestData]);
const [pageSize, setPageSize] = useState<number>(10);
const { selectedDashboard } = useDashboard();
const handleChangePageSize = (value: number): void => {
setPagination({
@@ -88,53 +59,35 @@ function LogsPanelComponent({
offset: value,
});
setPageSize(value);
const newQueryData = { ...requestData.query };
newQueryData.builder.queryData[0].pageSize = value;
const newRequestData = {
...requestData,
query: newQueryData,
tableParams: {
pagination,
},
};
setRequestData(newRequestData);
setRequestData((prev) => {
const newQueryData = { ...prev.query };
newQueryData.builder.queryData[0].pageSize = value;
return {
...prev,
query: newQueryData,
tableParams: {
pagination: {
limit: 0,
offset: value,
},
},
};
});
};
const { data, isFetching, isError } = useGetQueryRange(
{
...requestData,
globalSelectedInterval: globalSelectedTime,
selectedTime: selectedTime?.enum || 'GLOBAL_TIME',
variables: getDashboardVariables(selectedDashboard?.data.variables),
},
DEFAULT_ENTITY_VERSION,
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
maxTime,
minTime,
requestData,
pagination,
selectedDashboard?.data.variables,
],
enabled: !!requestData.query && !!selectedLogsFields?.length,
},
);
const columns = getLogPanelColumnsList(selectedLogsFields);
const columns = getLogPanelColumnsList(widget.selectedLogFields);
const dataLength =
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
const [firstLog, setFirstLog] = useState<ILog>();
const [lastLog, setLastLog] = useState<ILog>();
const { logs } = useLogsData({
result: data?.payload.data.newResult.data.result,
result: queryResponse.data?.payload.data.newResult.data.result,
panelType: PANEL_TYPES.LIST,
stagedQuery: query,
stagedQuery: widget.query,
});
useEffect(() => {
@@ -167,92 +120,86 @@ function LogsPanelComponent({
);
const isOrderByTimeStamp =
query.builder.queryData[0].orderBy.length > 0 &&
query.builder.queryData[0].orderBy[0].columnName === 'timestamp';
widget.query.builder.queryData[0].orderBy.length > 0 &&
widget.query.builder.queryData[0].orderBy[0].columnName === 'timestamp';
const handlePreviousPagination = (): void => {
if (isOrderByTimeStamp) {
setRequestData({
...requestData,
setRequestData((prev) => ({
...prev,
query: {
...requestData.query,
...prev.query,
builder: {
...requestData.query.builder,
...prev.query.builder,
queryData: [
{
...requestData.query.builder.queryData[0],
...prev.query.builder.queryData[0],
filters: {
...requestData.query.builder.queryData[0].filters,
...prev.query.builder.queryData[0].filters,
items: [
{
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['>'],
value: firstLog?.id || '',
},
...getNextOrPreviousItems(
prev.query.builder.queryData[0].filters.items,
'PREV',
firstLog,
),
],
},
limit: 0,
offset: 0,
},
],
},
},
});
return;
}));
}
if (!isOrderByTimeStamp) {
setPagination({
...pagination,
limit: 0,
offset: pagination.offset - pageSize,
});
}
setPagination({
...pagination,
limit: 0,
offset: pagination.offset - pageSize,
});
};
const handleNextPagination = (): void => {
if (isOrderByTimeStamp) {
setRequestData({
...requestData,
setRequestData((prev) => ({
...prev,
query: {
...requestData.query,
...prev.query,
builder: {
...requestData.query.builder,
...prev.query.builder,
queryData: [
{
...requestData.query.builder.queryData[0],
...prev.query.builder.queryData[0],
filters: {
...requestData.query.builder.queryData[0].filters,
...prev.query.builder.queryData[0].filters,
items: [
{
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['<'],
value: lastLog?.id || '',
},
...getNextOrPreviousItems(
prev.query.builder.queryData[0].filters.items,
'NEXT',
lastLog,
),
],
},
limit: 0,
offset: 0,
},
],
},
},
});
return;
}));
}
if (!isOrderByTimeStamp) {
setPagination({
...pagination,
limit: 0,
offset: pagination.offset + pageSize,
});
}
setPagination({
...pagination,
limit: 0,
offset: pagination.offset + pageSize,
});
};
if (isError) {
if (queryResponse.isError) {
return <div>{SOMETHING_WENT_WRONG}</div>;
}
@@ -265,19 +212,19 @@ function LogsPanelComponent({
tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }}
sticky
loading={isFetching}
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={flattenLogData}
columns={columns}
onRow={handleRow}
/>
</div>
{!query.builder.queryData[0].limit && (
{!widget.query.builder.queryData[0].limit && (
<div className="controller">
<Controls
totalCount={totalCount}
perPageOptions={PER_PAGE_OPTIONS}
isLoading={isFetching}
isLoading={queryResponse.isFetching}
offset={pagination.offset}
countPerPage={pageSize}
handleNavigatePrevious={handlePreviousPagination}
@@ -301,13 +248,12 @@ function LogsPanelComponent({
}
export type LogsPanelComponentProps = {
selectedLogsFields: Widgets['selectedLogFields'];
query: Query;
selectedTime?: timePreferance;
};
LogsPanelComponent.defaultProps = {
selectedTime: undefined,
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
widget: Widgets;
};
export default LogsPanelComponent;

View File

@@ -1,10 +1,15 @@
import { ColumnsType } from 'antd/es/table';
import { Typography } from 'antd/lib';
import { OPERATORS } from 'constants/queryBuilder';
// import Typography from 'antd/es/typography/Typography';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export const getLogPanelColumnsList = (
selectedLogFields: Widgets['selectedLogFields'],
@@ -36,3 +41,49 @@ export const getLogPanelColumnsList = (
return [...initialColumns, ...columns];
};
export const getNextOrPreviousItems = (
items: TagFilterItem[],
direction: 'NEXT' | 'PREV',
log?: ILog,
): TagFilterItem[] => {
const nextItem = {
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['<'],
value: log?.id || '',
};
const prevItem = {
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['>'],
value: log?.id || '',
};
let index = items.findIndex((item) => item.op === OPERATORS['<']);
if (index === -1) {
index = items.findIndex((item) => item.op === OPERATORS['>']);
}
if (index === -1) {
if (direction === 'NEXT') {
return [...items, nextItem];
}
return [...items, prevItem];
}
const newItems = [...items];
if (direction === 'NEXT') {
newItems[index] = nextItem;
} else {
newItems[index] = prevItem;
}
return newItems;
};

View File

@@ -8,6 +8,7 @@ export const getWidgetQueryBuilder = ({
title = '',
panelTypes,
yAxisUnit = '',
fillSpans = false,
id,
}: GetWidgetQueryBuilderProps): Widgets => ({
description: '',
@@ -24,4 +25,5 @@ export const getWidgetQueryBuilder = ({
softMin: null,
selectedLogFields: [],
selectedTracesFields: [],
fillSpans,
});

View File

@@ -70,6 +70,7 @@ function DBCall(): JSX.Element {
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'reqps',
id: SERVICE_CHART_ID.dbCallsRPS,
fillSpans: false,
}),
[servicename, tagFilterItems],
);
@@ -89,7 +90,8 @@ function DBCall(): JSX.Element {
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.dbCallsAvgDuration,
id: GraphTitle.DATABASE_CALLS_AVG_DURATION,
fillSpans: true,
}),
[servicename, tagFilterItems],
);
@@ -112,8 +114,6 @@ function DBCall(): JSX.Element {
<Card data-testid="database_call_rps">
<GraphContainer>
<Graph
fillSpans={false}
name="database_call_rps"
widget={databaseCallsRPSWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)(
@@ -147,8 +147,6 @@ function DBCall(): JSX.Element {
<Card data-testid="database_call_avg_duration">
<GraphContainer>
<Graph
fillSpans
name="database_call_avg_duration"
widget={databaseCallsAverageDurationWidget}
headerMenuList={MENU_ITEMS}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {

View File

@@ -18,7 +18,7 @@ import { useParams } from 'react-router-dom';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { GraphTitle, legend, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
@@ -60,7 +60,7 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.externalCallErrorPercentage,
id: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
}),
[servicename, tagFilterItems],
);
@@ -86,7 +86,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.externalCallDuration,
id: GraphTitle.EXTERNAL_CALL_DURATION,
fillSpans: true,
}),
[servicename, tagFilterItems],
);
@@ -108,7 +109,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'reqps',
id: SERVICE_CHART_ID.externalCallRPSByAddress,
id: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
fillSpans: true,
}),
[servicename, tagFilterItems],
);
@@ -130,7 +132,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.externalCallDurationByAddress,
id: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
fillSpans: true,
}),
[servicename, tagFilterItems],
);
@@ -155,9 +158,7 @@ function External(): JSX.Element {
<Card data-testid="external_call_error_percentage">
<GraphContainer>
<Graph
fillSpans={false}
headerMenuList={MENU_ITEMS}
name="external_call_error_percentage"
widget={externalCallErrorWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)(
@@ -192,8 +193,6 @@ function External(): JSX.Element {
<Card data-testid="external_call_duration">
<GraphContainer>
<Graph
fillSpans
name="external_call_duration"
headerMenuList={MENU_ITEMS}
widget={externalCallDurationWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
@@ -230,8 +229,6 @@ function External(): JSX.Element {
<Card data-testid="external_call_rps_by_address">
<GraphContainer>
<Graph
fillSpans
name="external_call_rps_by_address"
widget={externalCallRPSWidget}
headerMenuList={MENU_ITEMS}
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
@@ -267,10 +264,8 @@ function External(): JSX.Element {
<Card data-testid="external_call_duration_by_address">
<GraphContainer>
<Graph
name="external_call_duration_by_address"
widget={externalCallDurationAddressWidget}
headerMenuList={MENU_ITEMS}
fillSpans
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)(
xValue,

View File

@@ -19,13 +19,10 @@ import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { defaultTo } from 'lodash-es';
import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { v4 as uuid } from 'uuid';
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
@@ -49,9 +46,6 @@ import {
} from './util';
function Application(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
@@ -59,10 +53,6 @@ function Application(): JSX.Element {
const { queries } = useResourceAttribute();
const urlQuery = useUrlQuery();
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
[queries],
);
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
?.active;
@@ -94,7 +84,7 @@ function Application(): JSX.Element {
isLoading: topLevelOperationsIsLoading,
isError: topLevelOperationsIsError,
} = useQuery<ServiceDataProps>({
queryKey: [servicename, minTime, maxTime, selectedTags],
queryKey: [servicename],
queryFn: getTopLevelOperations,
});
@@ -116,49 +106,41 @@ function Application(): JSX.Element {
[servicename, topLevelOperations],
);
const operationPerSecWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: operationPerSec({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
const operationPerSecWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: operationPerSec({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
});
const errorPercentageWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: errorPercentage({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
const errorPercentageWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: errorPercentage({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
});
const onDragSelect = useCallback(
(start: number, end: number) => {

View File

@@ -89,8 +89,6 @@ function ApDexMetrics({
return (
<Graph
name="apdex"
fillSpans={false}
widget={apDexMetricsWidget}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')}

View File

@@ -50,7 +50,6 @@ function ApDexTraces({
return (
<Graph
name="apdex"
widget={apDexTracesWidget}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')}

View File

@@ -1,3 +1,4 @@
import { Skeleton } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -46,28 +47,24 @@ function ServiceOverview({
[isSpanMetricEnable, queries],
);
const latencyWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: latency({
servicename,
tagFilterItems,
isSpanMetricEnable,
topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
const latencyWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: latency({
servicename,
tagFilterItems,
isSpanMetricEnable,
topLevelOperationsRoute,
}),
[servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems],
);
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
});
const isQueryEnabled =
!topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0;
@@ -88,15 +85,23 @@ function ServiceOverview({
</Button>
<Card data-testid="service_latency">
<GraphContainer>
<Graph
name="service_latency"
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
fillSpans={false}
version={ENTITY_VERSION_V4}
/>
{topLevelOperationsIsLoading && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
{!topLevelOperationsIsLoading && (
<Graph
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
/>
)}
</GraphContainer>
</Card>
</>

View File

@@ -1,4 +1,4 @@
import { Typography } from 'antd';
import { Skeleton, Typography } from 'antd';
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -27,15 +27,23 @@ function TopLevelOperation({
</Typography>
) : (
<GraphContainer>
<Graph
fillSpans={false}
name={name}
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
/>
{topLevelOperationsIsLoading && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
{!topLevelOperationsIsLoading && (
<Graph
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
/>
)}
</GraphContainer>
)}
</Card>

View File

@@ -1,3 +1,5 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { DownloadOptions } from 'container/Download/Download.types';
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
@@ -20,7 +22,7 @@ export enum FORMULA {
ERROR_PERCENTAGE = 'A*100/B',
DATABASE_CALLS_AVG_DURATION = 'A/B',
APDEX_TRACES = '((B + C)/2)/A',
APDEX_DELTA_SPAN_METRICS = '(B + C/2)/A',
APDEX_DELTA_SPAN_METRICS = '((B + C)/2)/A',
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
}

View File

@@ -13,7 +13,7 @@ export const Card = styled(CardComponent)`
}
.ant-card-body {
height: calc(100% - 40px);
height: 100%;
padding: 0;
}
`;
@@ -40,7 +40,7 @@ export const ColErrorContainer = styled(ColComponent)`
export const GraphContainer = styled.div`
min-height: calc(40vh - 40px);
height: calc(100% - 40px);
height: 100%;
`;
export const GraphTitle = styled(Typography)`

View File

@@ -10,6 +10,7 @@ export interface GetWidgetQueryBuilderProps {
panelTypes: Widgets['panelTypes'];
yAxisUnit?: Widgets['yAxisUnit'];
id?: Widgets['id'];
fillSpans?: Widgets['fillSpans'];
}
export interface NavigateToTraceProps {

View File

@@ -33,6 +33,8 @@ export const getNearestHighestBucketValue = (
value: number,
buckets: number[],
): string => {
// sort the buckets
buckets.sort((a, b) => a - b);
const nearestBucket = buckets.find((bucket) => bucket >= value);
return nearestBucket?.toString() || '+Inf';
};

View File

@@ -10,6 +10,7 @@ export const PANEL_TYPES_INITIAL_QUERY = {
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
};

View File

@@ -1,6 +1,13 @@
import { Color } from '@signozhq/design-tokens';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { BarChart3, LineChart, List, SigmaSquare, Table } from 'lucide-react';
import {
BarChart3,
LineChart,
List,
PieChart,
SigmaSquare,
Table,
} from 'lucide-react';
const Items: ItemsProps[] = [
{
@@ -28,9 +35,14 @@ const Items: ItemsProps[] = [
icon: <BarChart3 size={32} color={Color.BG_ROBIN_400} />,
display: 'Bar',
},
{
name: PANEL_TYPES.PIE,
icon: <PieChart size={32} color={Color.BG_ROBIN_400} />,
display: 'Pie',
},
];
interface ItemsProps {
export interface ItemsProps {
name: PANEL_TYPES;
icon: JSX.Element;
display: string;

View File

@@ -1,9 +1,9 @@
import { Row } from 'antd';
import { isNull } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useState } from 'react';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { convertVariablesToDbFormat } from './util';
import VariableItem from './VariableItem';
function DashboardVariableSelection(): JSX.Element | null {
@@ -11,15 +11,14 @@ function DashboardVariableSelection(): JSX.Element | null {
selectedDashboard,
setSelectedDashboard,
updateLocalStorageDashboardVariables,
variablesToGetUpdated,
setVariablesToGetUpdated,
} = useDashboard();
const { data } = selectedDashboard || {};
const { variables } = data || {};
const [update, setUpdate] = useState<boolean>(false);
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const [variablesTableData, setVariablesTableData] = useState<any>([]);
useEffect(() => {
@@ -45,8 +44,27 @@ function DashboardVariableSelection(): JSX.Element | null {
}, [variables]);
const onVarChanged = (name: string): void => {
setLastUpdatedVar(name);
setUpdate(!update);
/**
* this function takes care of adding the dependent variables to current update queue and removing
* the updated variable name from the queue
*/
const dependentVariables = variablesTableData
?.map((variable: any) => {
if (variable.type === 'QUERY') {
const re = new RegExp(`\\{\\{\\s*?\\.${name}\\s*?\\}\\}`); // regex for `{{.var}}`
const queryValue = variable.queryValue || '';
const dependVarReMatch = queryValue.match(re);
if (dependVarReMatch !== null && dependVarReMatch.length > 0) {
return variable.name;
}
}
return null;
})
.filter((val: string | null) => !isNull(val));
setVariablesToGetUpdated((prev) => [
...prev.filter((v) => v !== name),
...dependentVariables,
]);
};
const onValueUpdate = (
@@ -56,37 +74,31 @@ function DashboardVariableSelection(): JSX.Element | null {
allSelected: boolean,
): void => {
if (id) {
const newVariablesArr = variablesTableData.map(
(variable: IDashboardVariable) => {
const variableCopy = { ...variable };
if (variableCopy.id === id) {
variableCopy.selectedValue = value;
variableCopy.allSelected = allSelected;
}
return variableCopy;
},
);
updateLocalStorageDashboardVariables(name, value, allSelected);
const variables = convertVariablesToDbFormat(newVariablesArr);
if (selectedDashboard) {
setSelectedDashboard({
...selectedDashboard,
data: {
...selectedDashboard?.data,
variables: {
...variables,
},
},
setSelectedDashboard((prev) => {
if (prev) {
return {
...prev,
data: {
...prev?.data,
variables: {
...prev?.data.variables,
[id]: {
...prev.data.variables[id],
selectedValue: value,
allSelected,
},
},
},
};
}
return prev;
});
}
onVarChanged(name);
setUpdate(!update);
}
};
@@ -107,13 +119,12 @@ function DashboardVariableSelection(): JSX.Element | null {
<VariableItem
key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={variables}
lastUpdatedVar={lastUpdatedVar}
variableData={{
name: variable.name,
...variable,
change: update,
}}
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
/>
))}
</Row>

View File

@@ -53,7 +53,7 @@ describe('VariableItem', () => {
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -68,7 +68,7 @@ describe('VariableItem', () => {
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -82,7 +82,7 @@ describe('VariableItem', () => {
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -110,7 +110,7 @@ describe('VariableItem', () => {
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -131,7 +131,7 @@ describe('VariableItem', () => {
variableData={customVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -146,7 +146,7 @@ describe('VariableItem', () => {
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);

View File

@@ -32,7 +32,7 @@ interface VariableItemProps {
arg1: IDashboardVariable['selectedValue'],
allSelected: boolean,
) => void;
lastUpdatedVar: string;
variablesToGetUpdated: string[];
}
const getSelectValue = (
@@ -49,7 +49,7 @@ function VariableItem({
variableData,
existingVariables,
onValueUpdate,
lastUpdatedVar,
variablesToGetUpdated,
}: VariableItemProps): JSX.Element {
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
@@ -108,16 +108,10 @@ function VariableItem({
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
/* eslint-disable no-useless-escape */
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
// If the variable is dependent on the last updated variable
// and contains the last updated variable in its query (of the form `{{.var}}`)
// then we need to update the value of the variable
const queryValue = variableData.queryValue || '';
const dependVarReMatch = queryValue.match(re);
if (
variableData.type === 'QUERY' &&
dependVarReMatch !== null &&
dependVarReMatch.length > 0
variableData.name &&
variablesToGetUpdated.includes(variableData.name)
) {
let value = variableData.selectedValue;
let allSelected = false;

View File

@@ -2,14 +2,11 @@ import './QuerySection.styles.scss';
import { Button, Tabs, Tooltip, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import { WidgetGraphProps } from 'container/NewWidget/types';
import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
@@ -22,9 +19,12 @@ import {
getSelectedWidgetIndex,
} from 'providers/Dashboard/util';
import { useCallback, useEffect, useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
@@ -35,7 +35,7 @@ import PromQLQueryContainer from './QueryBuilder/promQL';
function QuerySection({
selectedGraph,
selectedTime,
queryResponse,
}: QueryProps): JSX.Element {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const urlQuery = useUrlQuery();
@@ -51,14 +51,6 @@ function QuerySection({
const { selectedDashboard, setSelectedDashboard } = useDashboard();
const getWidgetQueryRange = useGetWidgetQueryRange(
{
graphType: selectedGraph,
selectedTime: selectedTime.enum,
},
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
);
const { widgets } = selectedDashboard?.data || {};
const getWidget = useCallback(() => {
@@ -233,7 +225,7 @@ function QuerySection({
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<Button
loading={getWidgetQueryRange.isFetching}
loading={queryResponse.isFetching}
type="primary"
onClick={handleRunQuery}
className="stage-run-query"
@@ -251,7 +243,10 @@ function QuerySection({
interface QueryProps {
selectedGraph: PANEL_TYPES;
selectedTime: WidgetGraphProps['selectedTime'];
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
}
export default QuerySection;

View File

@@ -1,12 +1,9 @@
import { Card, Typography } from 'antd';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { WidgetGraphProps } from 'container/NewWidget/types';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import useUrlQuery from 'hooks/useUrlQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { getGraphType } from 'utils/getGraphType';
import { WidgetGraphContainerProps } from 'container/NewWidget/types';
// import useUrlQuery from 'hooks/useUrlQuery';
// import { useDashboard } from 'providers/Dashboard/Dashboard';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { NotFoundContainer } from './styles';
@@ -14,58 +11,36 @@ import WidgetGraph from './WidgetGraphs';
function WidgetGraphContainer({
selectedGraph,
yAxisUnit,
selectedTime,
thresholds,
fillSpans = false,
softMax,
softMin,
selectedLogFields,
selectedTracesFields,
}: WidgetGraphProps): JSX.Element {
const { selectedDashboard } = useDashboard();
const { widgets = [] } = selectedDashboard?.data || {};
const params = useUrlQuery();
const widgetId = params.get('widgetId');
const selectedWidget = widgets.find((e) => e.id === widgetId);
const getWidgetQueryRange = useGetWidgetQueryRange(
{
graphType: getGraphType(selectedGraph),
selectedTime: selectedTime.enum,
},
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
);
if (getWidgetQueryRange.data && selectedGraph === PANEL_TYPES.BAR) {
queryResponse,
setRequestData,
selectedWidget,
}: WidgetGraphContainerProps): JSX.Element {
if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
getWidgetQueryRange.data?.payload.data.result,
queryResponse.data?.payload.data.result,
);
getWidgetQueryRange.data.payload.data.result = sortedSeriesData;
// eslint-disable-next-line no-param-reassign
queryResponse.data.payload.data.result = sortedSeriesData;
}
if (selectedWidget === undefined) {
return <Card>Invalid widget</Card>;
}
if (getWidgetQueryRange.error) {
if (queryResponse?.error) {
return (
<NotFoundContainer>
<Typography>{getWidgetQueryRange.error.message}</Typography>
<Typography>{queryResponse.error.message}</Typography>
</NotFoundContainer>
);
}
if (getWidgetQueryRange.isLoading) {
if (queryResponse.isLoading && selectedGraph !== PANEL_TYPES.LIST) {
return <Spinner size="large" tip="Loading..." />;
}
if (
selectedGraph !== PANEL_TYPES.LIST &&
getWidgetQueryRange.data?.payload.data.result.length === 0
queryResponse.data?.payload.data.result.length === 0
) {
return (
<NotFoundContainer>
@@ -75,7 +50,7 @@ function WidgetGraphContainer({
}
if (
selectedGraph === PANEL_TYPES.LIST &&
getWidgetQueryRange.data?.payload.data.newResult.data.result.length === 0
queryResponse.data?.payload.data.newResult.data.result.length === 0
) {
return (
<NotFoundContainer>
@@ -86,16 +61,9 @@ function WidgetGraphContainer({
return (
<WidgetGraph
yAxisUnit={yAxisUnit || ''}
getWidgetQueryRange={getWidgetQueryRange}
selectedWidget={selectedWidget}
thresholds={thresholds}
fillSpans={fillSpans}
softMax={softMax}
softMin={softMin}
selectedLogFields={selectedLogFields}
selectedTracesFields={selectedTracesFields}
selectedTime={selectedTime}
queryResponse={queryResponse}
setRequestData={setRequestData}
selectedGraph={selectedGraph}
/>
);

View File

@@ -1,98 +1,37 @@
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
} from 'react';
import { UseQueryResult } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getTimeRange } from 'utils/getTimeRange';
function WidgetGraph({
getWidgetQueryRange,
selectedWidget,
yAxisUnit,
thresholds,
fillSpans,
softMax,
softMin,
selectedLogFields,
selectedTracesFields,
selectedTime,
queryResponse,
setRequestData,
selectedGraph,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const location = useLocation();
useEffect((): void => {
const { startTime, endTime } = getTimeRange(getWidgetQueryRange);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [getWidgetQueryRange, maxTime, minTime, globalSelectedInterval]);
const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef);
const chartData = getUPlotChartData(
getWidgetQueryRange?.data?.payload,
fillSpans,
);
const isDarkMode = useIsDarkMode();
const params = useUrlQuery();
const widgetId = params.get('widgetId');
const dispatch = useDispatch();
const onDragSelect = useCallback(
(start: number, end: number): void => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
const { maxTime, minTime } = GetMinMax('custom', [
startTimestamp,
endTimestamp,
]);
params.set(QueryParams.startTime, minTime.toString());
params.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${params.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, params],
);
const urlQuery = useUrlQuery();
const location = useLocation();
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
@@ -114,6 +53,28 @@ function WidgetGraph({
}
};
const onDragSelect = useCallback(
(start: number, end: number): void => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
const { maxTime, minTime } = GetMinMax('custom', [
startTimestamp,
endTimestamp,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
useEffect(() => {
window.addEventListener('popstate', handleBackNavigation);
@@ -123,78 +84,26 @@ function WidgetGraph({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const options = useMemo(
() =>
getUPlotChartOptions({
id: widgetId || 'legend_widget',
yAxisUnit,
apiResponse: getWidgetQueryRange?.data?.payload,
dimensions: containerDimensions,
isDarkMode,
onDragSelect,
thresholds,
fillSpans,
minTimeScale,
maxTimeScale,
softMax,
softMin,
panelType: selectedGraph,
currentQuery,
}),
[
widgetId,
yAxisUnit,
getWidgetQueryRange?.data?.payload,
containerDimensions,
isDarkMode,
onDragSelect,
thresholds,
fillSpans,
minTimeScale,
maxTimeScale,
softMax,
softMin,
selectedGraph,
currentQuery,
],
);
return (
<div ref={graphRef} style={{ height: '100%' }}>
<GridPanelSwitch
data={chartData}
options={options}
panelType={selectedWidget.panelTypes}
name={widgetId || 'legend_widget'}
yAxisUnit={yAxisUnit}
panelData={
getWidgetQueryRange.data?.payload.data.newResult.data.result || []
}
query={stagedQuery || selectedWidget.query}
thresholds={thresholds}
selectedLogFields={selectedLogFields}
selectedTracesFields={selectedTracesFields}
dataSource={currentQuery.builder.queryData[0].dataSource}
selectedTime={selectedTime}
<PanelWrapper
widget={selectedWidget}
queryResponse={queryResponse}
setRequestData={setRequestData}
onDragSelect={onDragSelect}
selectedGraph={selectedGraph}
/>
</div>
);
}
interface WidgetGraphProps {
thresholds: ThresholdProps[];
yAxisUnit: string;
selectedWidget: Widgets;
fillSpans: boolean;
getWidgetQueryRange: UseQueryResult<
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
softMax: number | null;
softMin: number | null;
selectedLogFields: Widgets['selectedLogFields'];
selectedTracesFields: Widgets['selectedTracesFields'];
selectedTime: timePreferance;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
selectedGraph: PANEL_TYPES;
}

View File

@@ -1,47 +1,20 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { Card } from 'container/GridCardLayout/styles';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo } from 'react';
import { getGraphType } from 'utils/getGraphType';
import { WidgetGraphProps } from '../../types';
import { WidgetGraphContainerProps } from '../../types';
import PlotTag from './PlotTag';
import { AlertIconContainer, Container } from './styles';
import WidgetGraphComponent from './WidgetGraphContainer';
function WidgetGraph({
selectedGraph,
yAxisUnit,
selectedTime,
thresholds,
fillSpans,
softMax,
softMin,
selectedLogFields,
selectedTracesFields,
}: WidgetGraphProps): JSX.Element {
queryResponse,
setRequestData,
selectedWidget,
}: WidgetGraphContainerProps): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { selectedDashboard } = useDashboard();
const { widgets = [] } = selectedDashboard?.data || {};
const params = useUrlQuery();
const widgetId = params.get('widgetId');
const selectedWidget = widgets.find((e) => e.id === widgetId);
const getWidgetQueryRange = useGetWidgetQueryRange(
{
graphType: getGraphType(selectedGraph),
selectedTime: selectedTime.enum,
},
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
);
if (selectedWidget === undefined) {
return <Card $panelType={selectedGraph}>Invalid widget</Card>;
@@ -50,22 +23,17 @@ function WidgetGraph({
return (
<Container $panelType={selectedGraph}>
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
{getWidgetQueryRange.error && (
<AlertIconContainer color="red" title={getWidgetQueryRange.error.message}>
{queryResponse.error && (
<AlertIconContainer color="red" title={queryResponse.error.message}>
<InfoCircleOutlined />
</AlertIconContainer>
)}
<WidgetGraphComponent
thresholds={thresholds}
selectedTime={selectedTime}
selectedGraph={selectedGraph}
yAxisUnit={yAxisUnit}
fillSpans={fillSpans}
softMax={softMax}
softMin={softMin}
selectedLogFields={selectedLogFields}
selectedTracesFields={selectedTracesFields}
queryResponse={queryResponse}
setRequestData={setRequestData}
selectedWidget={selectedWidget}
/>
</Container>
);

View File

@@ -1,5 +1,16 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { memo } from 'react';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
import { WidgetGraphProps } from '../types';
import ExplorerColumnsRenderer from './ExplorerColumnsRenderer';
@@ -9,32 +20,84 @@ import WidgetGraph from './WidgetGraph';
function LeftContainer({
selectedGraph,
yAxisUnit,
selectedTime,
thresholds,
fillSpans,
softMax,
softMin,
selectedLogFields,
setSelectedLogFields,
selectedTracesFields,
setSelectedTracesFields,
selectedWidget,
selectedTime,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const { selectedDashboard } = useDashboard();
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (selectedWidget && selectedGraph !== PANEL_TYPES.LIST) {
return {
selectedTime: selectedWidget?.timePreferance,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: stagedQuery || initialQueriesMap.metrics,
globalSelectedInterval,
variables: getDashboardVariables(selectedDashboard?.data.variables),
};
}
const updatedQuery = { ...(stagedQuery || initialQueriesMap.metrics) };
updatedQuery.builder.queryData[0].pageSize = 10;
redirectWithQueryBuilderData(updatedQuery);
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: selectedTime.enum || 'GLOBAL_TIME',
globalSelectedInterval,
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
useEffect(() => {
if (stagedQuery) {
setRequestData((prev) => ({
...prev,
selectedTime: selectedTime.enum || prev.selectedTime,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: stagedQuery,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stagedQuery, selectedTime]);
const queryResponse = useGetQueryRange(
requestData,
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
{
enabled: !!stagedQuery,
retry: false,
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedInterval,
requestData,
],
},
);
return (
<>
<WidgetGraph
thresholds={thresholds}
selectedTime={selectedTime}
selectedGraph={selectedGraph}
yAxisUnit={yAxisUnit}
fillSpans={fillSpans}
softMax={softMax}
softMin={softMin}
selectedLogFields={selectedLogFields}
selectedTracesFields={selectedTracesFields}
queryResponse={queryResponse}
setRequestData={setRequestData}
selectedWidget={selectedWidget}
/>
<QueryContainer>
<QuerySection selectedTime={selectedTime} selectedGraph={selectedGraph} />
<QuerySection selectedGraph={selectedGraph} queryResponse={queryResponse} />
{selectedGraph === PANEL_TYPES.LIST && (
<ExplorerColumnsRenderer
selectedLogFields={selectedLogFields}

View File

@@ -26,6 +26,7 @@ export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -36,6 +37,7 @@ export const panelTypeVsSoftMinMax: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -45,6 +47,7 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.TIME_SERIES]: false,
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.TRACE]: false,
@@ -56,6 +59,7 @@ export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -66,6 +70,7 @@ export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -76,6 +81,7 @@ export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -88,6 +94,7 @@ export const panelTypeVsPanelTimePreferences: {
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: true,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,

View File

@@ -12,10 +12,20 @@ import {
import InputComponent from 'components/Input';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems';
import GraphTypes, {
ItemsProps,
} from 'container/NewDashboard/ComponentsSlider/menuItems';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { Dispatch, SetStateAction, useCallback } from 'react';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import { DataSource } from 'types/common/queryBuilder';
import {
panelTypeVsCreateAlert,
@@ -75,6 +85,24 @@ function RightContainer({
const allowPanelTimePreference =
panelTypeVsPanelTimePreferences[selectedGraph];
const { currentQuery } = useQueryBuilder();
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
useEffect(() => {
const queryContainsMetricsDataSource = currentQuery.builder.queryData.some(
(query) => query.dataSource === DataSource.METRICS,
);
if (queryContainsMetricsDataSource) {
setGraphTypes((prev) =>
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
);
} else {
setGraphTypes(GraphTypes);
}
}, [currentQuery]);
const softMinHandler = useCallback(
(value: number | null) => {
setSoftMin(value);
@@ -95,10 +123,9 @@ function RightContainer({
<Select
onChange={setGraphHandler}
value={selectedGraph}
disabled
style={{ width: '100%', marginBottom: 24 }}
>
{GraphTypes.map((item) => (
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
{item.display}
</Option>

View File

@@ -3,6 +3,7 @@ import { LockFilled, WarningOutlined } from '@ant-design/icons';
import { Button, Modal, Space, Tooltip, Typography } from 'antd';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DashboardShortcuts } from 'constants/shortcuts/DashboardShortcuts';
@@ -23,7 +24,7 @@ import {
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath, useLocation, useParams } from 'react-router-dom';
import { generatePath, useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { IField } from 'types/api/logs/fields';
@@ -44,7 +45,7 @@ import {
RightContainerWrapper,
} from './styles';
import { NewWidgetProps } from './types';
import { getIsQueryModified } from './utils';
import { getIsQueryModified, handleQueryChange } from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const {
@@ -57,7 +58,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { currentQuery, stagedQuery } = useQueryBuilder();
const {
currentQuery,
stagedQuery,
redirectWithQueryBuilderData,
supersetQuery,
} = useQueryBuilder();
const isQueryModified = useMemo(
() => getIsQueryModified(currentQuery, stagedQuery),
@@ -70,8 +76,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { widgets = [] } = selectedDashboard?.data || {};
const { search } = useLocation();
const query = useUrlQuery();
const { dashboardId } = useParams<DashboardWidgetPageParams>();
@@ -81,7 +85,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
return widgets?.find((e) => e.id === widgetId);
}, [query, widgets]);
const selectedWidget = getWidget();
const [selectedWidget, setSelectedWidget] = useState(getWidget());
const [title, setTitle] = useState<string>(
selectedWidget?.title?.toString() || '',
@@ -129,6 +133,44 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
: selectedWidget?.softMax || 0,
);
useEffect(() => {
setSelectedWidget((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
query: currentQuery,
title,
description,
isStacked: stacked,
opacity,
nullZeroValues: selectedNullZeroValue,
yAxisUnit,
thresholds,
softMin,
softMax,
fillSpans: isFillSpans,
selectedLogFields,
selectedTracesFields,
};
});
}, [
currentQuery,
description,
isFillSpans,
opacity,
selectedLogFields,
selectedNullZeroValue,
selectedTracesFields,
softMax,
softMin,
stacked,
thresholds,
title,
yAxisUnit,
]);
const closeModal = (): void => {
setSaveModal(false);
setDiscardModal(false);
@@ -194,21 +236,21 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
...preWidgets,
{
...(selectedWidget || ({} as Widgets)),
description,
description: selectedWidget?.description || '',
timePreferance: selectedTime.enum,
isStacked: stacked,
opacity,
nullZeroValues: selectedNullZeroValue,
title,
yAxisUnit,
isStacked: selectedWidget?.isStacked || false,
opacity: selectedWidget?.opacity || '1',
nullZeroValues: selectedWidget?.nullZeroValues || 'zero',
title: selectedWidget?.title,
yAxisUnit: selectedWidget?.yAxisUnit,
panelTypes: graphType,
query: currentQuery,
thresholds,
softMin,
softMax,
fillSpans: isFillSpans,
selectedLogFields,
selectedTracesFields,
thresholds: selectedWidget?.thresholds,
softMin: selectedWidget?.softMin || 0,
softMax: selectedWidget?.softMax || 0,
fillSpans: selectedWidget?.fillSpans,
selectedLogFields: selectedWidget?.selectedLogFields || [],
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
},
...afterWidgets,
],
@@ -234,21 +276,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedDashboard,
preWidgets,
selectedWidget,
description,
selectedTime.enum,
stacked,
opacity,
selectedNullZeroValue,
title,
yAxisUnit,
graphType,
currentQuery,
thresholds,
softMin,
softMax,
isFillSpans,
selectedLogFields,
selectedTracesFields,
afterWidgets,
updateDashboardMutation,
setSelectedDashboard,
@@ -271,9 +301,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
}, [dashboardId]);
const setGraphHandler = (type: PANEL_TYPES): void => {
const params = new URLSearchParams(search);
params.set('graphType', type);
const updatedQuery = handleQueryChange(type as any, supersetQuery);
setGraphType(type);
redirectWithQueryBuilderData(
updatedQuery,
{ [QueryParams.graphType]: type },
undefined,
true,
);
};
const onSaveDashboard = useCallback((): void => {
@@ -358,19 +393,17 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
<PanelContainer>
<LeftContainerWrapper flex={5}>
<LeftContainer
selectedTime={selectedTime}
selectedGraph={graphType}
yAxisUnit={yAxisUnit}
thresholds={thresholds}
fillSpans={isFillSpans}
softMax={softMax}
softMin={softMin}
selectedLogFields={selectedLogFields}
setSelectedLogFields={setSelectedLogFields}
selectedTracesFields={selectedTracesFields}
setSelectedTracesFields={setSelectedTracesFields}
/>
{selectedWidget && (
<LeftContainer
selectedGraph={graphType}
selectedLogFields={selectedLogFields}
setSelectedLogFields={setSelectedLogFields}
selectedTracesFields={selectedTracesFields}
setSelectedTracesFields={setSelectedTracesFields}
selectedWidget={selectedWidget}
selectedTime={selectedTime}
/>
)}
</LeftContainerWrapper>
<RightContainerWrapper flex={1}>

View File

@@ -1,8 +1,11 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { ThresholdProps } from './RightContainer/Threshold/types';
import { timePreferance } from './RightContainer/timeItems';
export interface NewWidgetProps {
@@ -11,15 +14,24 @@ export interface NewWidgetProps {
fillSpans: Widgets['fillSpans'];
}
export interface WidgetGraphProps extends NewWidgetProps {
selectedTime: timePreferance;
thresholds: ThresholdProps[];
softMin: number | null;
softMax: number | null;
export interface WidgetGraphProps {
selectedLogFields: Widgets['selectedLogFields'];
setSelectedLogFields?: Dispatch<SetStateAction<Widgets['selectedLogFields']>>;
selectedTracesFields: Widgets['selectedTracesFields'];
setSelectedTracesFields?: Dispatch<
SetStateAction<Widgets['selectedTracesFields']>
>;
selectedWidget: Widgets;
selectedGraph: PANEL_TYPES;
selectedTime: timePreferance;
}
export type WidgetGraphContainerProps = {
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
selectedGraph: PANEL_TYPES;
selectedWidget: Widgets;
};

View File

@@ -1,6 +1,11 @@
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import { isEqual } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { isEqual, set, unset } from 'lodash-es';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
export const getIsQueryModified = (
currentQuery: Query,
@@ -13,3 +18,287 @@ export const getIsQueryModified = (
const omitIdFromCurrentQuery = omitIdFromQuery(currentQuery);
return !isEqual(omitIdFromStageQuery, omitIdFromCurrentQuery);
};
export type PartialPanelTypes = {
[PANEL_TYPES.BAR]: 'bar';
[PANEL_TYPES.LIST]: 'list';
[PANEL_TYPES.TABLE]: 'table';
[PANEL_TYPES.TIME_SERIES]: 'graph';
[PANEL_TYPES.VALUE]: 'value';
[PANEL_TYPES.PIE]: 'pie';
};
export const panelTypeDataSourceFormValuesMap: Record<
keyof PartialPanelTypes,
Record<DataSource, any>
> = {
[PANEL_TYPES.BAR]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
],
},
},
},
[PANEL_TYPES.TIME_SERIES]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
],
},
},
},
[PANEL_TYPES.TABLE]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
],
},
},
},
[PANEL_TYPES.PIE]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
],
},
},
},
[PANEL_TYPES.LIST]: {
[DataSource.LOGS]: {
builder: {
queryData: ['filters', 'limit', 'orderBy'],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [],
},
},
[DataSource.TRACES]: {
builder: {
queryData: ['filters', 'limit', 'orderBy'],
},
},
},
[PANEL_TYPES.VALUE]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'reduceTo',
'having',
'functions',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'having',
'reduceTo',
'functions',
'spaceAggregation',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
],
},
},
},
};
export function handleQueryChange(
newPanelType: keyof PartialPanelTypes,
supersetQuery: Query,
): Query {
return {
...supersetQuery,
builder: {
...supersetQuery.builder,
queryData: supersetQuery.builder.queryData.map((query, index) => {
const { dataSource } = query;
const tempQuery = { ...initialQueryBuilderFormValuesMap[dataSource] };
const fieldsToSelect =
panelTypeDataSourceFormValuesMap[newPanelType][dataSource].builder
.queryData;
fieldsToSelect.forEach((field: keyof IBuilderQuery) => {
set(tempQuery, field, supersetQuery.builder.queryData[index][field]);
});
if (newPanelType === PANEL_TYPES.LIST) {
set(tempQuery, 'aggregateOperator', 'noop');
set(tempQuery, 'offset', 0);
set(tempQuery, 'pageSize', 10);
} else if (tempQuery.aggregateOperator === 'noop') {
set(tempQuery, 'aggregateOperator', 'count');
unset(tempQuery, 'offset');
unset(tempQuery, 'pageSize');
}
return tempQuery;
}),
},
};
}

View File

@@ -0,0 +1,106 @@
### Step 1: Install OpenTelemetry Dependencies
Dependencies related to OpenTelemetry exporter and SDK have to be installed first.
Run the below commands after navigating to the application source folder:
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
//sigNoz Cloud Endpoint
otlpOptions.Endpoint = new Uri("https://ingest.{{REGION}}.signoz.cloud:443");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
//SigNoz Cloud account Ingestion key
string headerKey = "signoz-access-token";
string headerValue = "{{SIGNOZ_INGESTION_KEY}}";
string formattedHeader = $"{headerKey}={headerValue}";
otlpOptions.Headers = formattedHeader;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.
### Step 3: Dockerize your application
Since the environment variables like SIGNOZ_INGESTION_KEY, Ingestion Endpoint and Service name are set in the `program.cs` file, you don't need to add any additional steps in your Dockerfile.
An **example** of a Dockerfile could look like this:
```bash
# Use the Microsoft official .NET SDK image to build the application
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /app
# Copy the CSPROJ file and restore any dependencies (via NUGET)
COPY *.csproj ./
RUN dotnet restore
# Copy the rest of the project files and build the application
COPY . ./
RUN dotnet publish -c Release -o out
# Generate the runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build-env /app/out .
# Expose port 5145 for the application
EXPOSE 5145
# Set the ASPNETCORE_URLS environment variable to listen on port 5145
ENV ASPNETCORE_URLS=http://+:5145
ENTRYPOINT ["dotnet", "YOUR-APPLICATION.dll"]
```

View File

@@ -0,0 +1,21 @@
Once you update your Dockerfile, you can build and run it using the commands below.
&nbsp;
### Step 1: Build your dockerfile
Build your docker image
```bash
docker build -t <your-image-name> .
```
- `<your-image-name>` is the name of your Docker Image
&nbsp;
### Step 2: Run your docker image
```bash
docker run <your-image-name>
```

View File

@@ -0,0 +1,12 @@
## Setup OpenTelemetry Binary as an agent
&nbsp;
As a first step, you should install the OTel collector Binary according to the instructions provided on [this link](https://signoz.io/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/).
&nbsp;
Once you are done setting up the OTel collector binary, you can follow the next steps.
&nbsp;

View File

@@ -0,0 +1,101 @@
After setting up the Otel collector agent, follow the steps below to instrument your .NET Application
&nbsp;
&nbsp;
### Step 1: Install OpenTelemetry Dependencies
Install the following dependencies in your application.
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.Runtime
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.AutoInstrumentation
```
&nbsp;
### Step 2: Adding OpenTelemetry as a service and configuring exporter options
In your `Program.cs` file, add OpenTelemetry as a service. Here, we are configuring these variables:
`serviceName` - It is the name of your service.
`otlpOptions.Endpoint` - It is the endpoint for your OTel Collector agent.
&nbsp;
Heres a sample `Program.cs` file with the configured variables:
```bash
using System.Diagnostics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// Configure OpenTelemetry with tracing and auto-start.
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService(serviceName: "{{MYAPP}}"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri("http://localhost:4317");
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
}));
var app = builder.Build();
//The index route ("/") is set up to write out the OpenTelemetry trace information on the response:
app.MapGet("/", () => $"Hello World! OpenTelemetry Trace: {Activity.Current?.Id}");
app.Run();
```
&nbsp;
The OpenTelemetry.Exporter.Options get or set the target to which the exporter is going to send traces. Here, were configuring it to send traces to the OTel Collector agent. The target must be a valid Uri with the scheme (http or https) and host and may contain a port and a path.
This is done by configuring an OpenTelemetry [TracerProvider](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/customizing-the-sdk#readme) using extension methods and setting it to auto-start when the host is started.
&nbsp;
### Step 3: Dockerize your application
Since the crucial environment variables like SIGNOZ_INGESTION_KEY, Ingestion Endpoint and Service name are set in the `program.cs` file, you don't need to add any additional steps in your Dockerfile.
An **example** of a Dockerfile could look like this:
```bash
# Use the Microsoft official .NET SDK image to build the application
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /app
# Copy the CSPROJ file and restore any dependencies (via NUGET)
COPY *.csproj ./
RUN dotnet restore
# Copy the rest of the project files and build the application
COPY . ./
RUN dotnet publish -c Release -o out
# Generate the runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build-env /app/out .
# Expose port 5145 for the application
EXPOSE 5145
# Set the ASPNETCORE_URLS environment variable to listen on port 5145
ENV ASPNETCORE_URLS=http://+:5145
ENTRYPOINT ["dotnet", "YOUR-APPLICATION.dll"]
```

View File

@@ -0,0 +1,21 @@
Once you update your Dockerfile, you can build and run it using the commands below.
&nbsp;
### Step 1: Build your dockerfile
Build your docker image
```bash
docker build -t <your-image-name> .
```
- `<your-image-name>` is the name of your Docker Image
&nbsp;
### Step 2: Run your docker image
```bash
docker run <your-image-name>
```

View File

@@ -3,14 +3,14 @@
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_linux_amd64.tar.gz
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{OTEL_VERSION}}/otelcol-contrib_{{OTEL_VERSION}}_linux_amd64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_linux_amd64.tar.gz -C otelcol-contrib
mkdir otelcol-contrib && tar xvzf otelcol-contrib_{{OTEL_VERSION}}_linux_amd64.tar.gz -C otelcol-contrib
```
&nbsp;

View File

@@ -4,14 +4,14 @@
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_linux_arm64.tar.gz
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{OTEL_VERSION}}/otelcol-contrib_{{OTEL_VERSION}}_linux_arm64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_linux_arm64.tar.gz -C otelcol-contrib
mkdir otelcol-contrib && tar xvzf otelcol-contrib_{{OTEL_VERSION}}_linux_arm64.tar.gz -C otelcol-contrib
```
&nbsp;

View File

@@ -3,13 +3,13 @@
### Step 1: Download otel-collector tar.gz
```bash
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.79.0/otelcol-contrib_0.79.0_darwin_amd64.tar.gz
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v{{OTEL_VERSION}}/otelcol-contrib_{{OTEL_VERSION}}_darwin_amd64.tar.gz
```
&nbsp;
### Step 2: Extract otel-collector tar.gz to the `otelcol-contrib` folder
```bash
mkdir otelcol-contrib && tar xvzf otelcol-contrib_0.79.0_darwin_amd64.tar.gz -C otelcol-contrib
mkdir otelcol-contrib && tar xvzf otelcol-contrib_{{OTEL_VERSION}}_darwin_amd64.tar.gz -C otelcol-contrib
```
&nbsp;

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