Compare commits
101 Commits
v0.42.3-de
...
v0.44.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3272444e13 | ||
|
|
71b3e6d522 | ||
|
|
6cf7cc9f4f | ||
|
|
5ec2f17d09 | ||
|
|
a45fb8ec0c | ||
|
|
bd148bbd5a | ||
|
|
1306e99ca8 | ||
|
|
1a8f063b4b | ||
|
|
c7668b9a78 | ||
|
|
5e3dba2587 | ||
|
|
374f30e0cd | ||
|
|
38d2833931 | ||
|
|
731eacbbca | ||
|
|
a63bb139bf | ||
|
|
a140bef0e6 | ||
|
|
48e5436167 | ||
|
|
0fc664a387 | ||
|
|
bb318cf52a | ||
|
|
ec0185da61 | ||
|
|
fc2bdb610f | ||
|
|
a9464de62d | ||
|
|
57bfdedfe1 | ||
|
|
7bdc9c0cb0 | ||
|
|
0d5934d56b | ||
|
|
3a5a61aff9 | ||
|
|
a54b7baa7d | ||
|
|
cd63dd972d | ||
|
|
389058b9b4 | ||
|
|
27e412d1ee | ||
|
|
03dccb0101 | ||
|
|
25b74b48a5 | ||
|
|
6815a96d29 | ||
|
|
e9bb05cc5d | ||
|
|
31c0b94ae6 | ||
|
|
59c242961f | ||
|
|
872ed9e963 | ||
|
|
d6cd155988 | ||
|
|
7f4a61ffb1 | ||
|
|
7737d513a7 | ||
|
|
2bd666efae | ||
|
|
d98265f345 | ||
|
|
b480ff1e48 | ||
|
|
af353b9340 | ||
|
|
96e7505922 | ||
|
|
8f6f2f0018 | ||
|
|
1f25d386df | ||
|
|
2d7a3733da | ||
|
|
ff2a3bc4b0 | ||
|
|
33383a4503 | ||
|
|
f05b94c01e | ||
|
|
fd632f9952 | ||
|
|
fd84d7b492 | ||
|
|
e4808e585a | ||
|
|
5cfeb56f9c | ||
|
|
b947f823d7 | ||
|
|
1520c1c57d | ||
|
|
f8477981d8 | ||
|
|
9b1d596816 | ||
|
|
6a4aa9a956 | ||
|
|
a7b0ef55ad | ||
|
|
87534b6fb6 | ||
|
|
c76cef47ba | ||
|
|
3276dfa03e | ||
|
|
1a14cc305c | ||
|
|
0c7e63d735 | ||
|
|
eb74cb4c5e | ||
|
|
a47d3289d0 | ||
|
|
8ad827130e | ||
|
|
93bdfd3d83 | ||
|
|
22d8889a07 | ||
|
|
7c93944d40 | ||
|
|
ec9dbb6853 | ||
|
|
7a7d814288 | ||
|
|
3babce3ecf | ||
|
|
1610b95b84 | ||
|
|
8c02f8ec31 | ||
|
|
5e0e9da6c4 | ||
|
|
51abe71421 | ||
|
|
00d74bfebb | ||
|
|
39e0ef68ca | ||
|
|
cff20f88cd | ||
|
|
4fbb71484d | ||
|
|
f8e8132b58 | ||
|
|
a1dd170641 | ||
|
|
fe2ddf9d60 | ||
|
|
dfc99a7756 | ||
|
|
c2556facc2 | ||
|
|
2a7ad596a1 | ||
|
|
6c455ab5ce | ||
|
|
7c062163a1 | ||
|
|
d6a256247c | ||
|
|
0e2c699518 | ||
|
|
c04d0e9419 | ||
|
|
d0feff00a7 | ||
|
|
6c2a3d5d43 | ||
|
|
914b035b3f | ||
|
|
71c4fcc382 | ||
|
|
9af1c2320b | ||
|
|
cdabf9060e | ||
|
|
f069ecdb76 | ||
|
|
493aef0241 |
31
.github/workflows/jest-coverage-changes.yml
vendored
Normal file
31
.github/workflows/jest-coverage-changes.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Jest Coverage - changed files
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: "refs/heads/develop"
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication
|
||||||
|
|
||||||
|
- name: Fetch branch
|
||||||
|
run: git fetch origin ${{ github.event.pull_request.head.ref }}
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
git checkout ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd frontend && npm install -g yarn && yarn
|
||||||
|
|
||||||
|
- name: npm run test:changedsince
|
||||||
|
run: cd frontend && npm run i18n:generate-hash && npm run test:changedsince
|
||||||
@@ -22,7 +22,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
"wget",
|
"wget",
|
||||||
"--spider",
|
"--spider",
|
||||||
"-q",
|
"-q",
|
||||||
"localhost:8123/ping"
|
"0.0.0.0:8123/ping"
|
||||||
]
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
@@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.39.0
|
image: signoz/query-service:0.44.0
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
@@ -186,7 +186,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.39.0
|
image: signoz/frontend:0.44.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@@ -199,7 +199,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.88.12
|
image: signoz/signoz-otel-collector:0.88.21
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@@ -237,7 +237,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.88.12
|
image: signoz/signoz-schema-migrator:0.88.21
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -111,18 +111,18 @@ processors:
|
|||||||
|
|
||||||
exporters:
|
exporters:
|
||||||
clickhousetraces:
|
clickhousetraces:
|
||||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
clickhousemetricswrite:
|
clickhousemetricswrite:
|
||||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
resource_to_telemetry_conversion:
|
resource_to_telemetry_conversion:
|
||||||
enabled: true
|
enabled: true
|
||||||
clickhousemetricswrite/prometheus:
|
clickhousemetricswrite/prometheus:
|
||||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
# logging: {}
|
# logging: {}
|
||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/
|
dsn: tcp://clickhouse:9000/signoz_logs
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
extensions:
|
extensions:
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ rule_files:
|
|||||||
scrape_configs: []
|
scrape_configs: []
|
||||||
|
|
||||||
remote_read:
|
remote_read:
|
||||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
- url: tcp://clickhouse:9000/signoz_metrics
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ services:
|
|||||||
"wget",
|
"wget",
|
||||||
"--spider",
|
"--spider",
|
||||||
"-q",
|
"-q",
|
||||||
"localhost:8123/ping"
|
"0.0.0.0:8123/ping"
|
||||||
]
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.21}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--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`
|
# 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:
|
otel-collector:
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.88.12
|
image: signoz/signoz-otel-collector:0.88.21
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
|||||||
"wget",
|
"wget",
|
||||||
"--spider",
|
"--spider",
|
||||||
"-q",
|
"-q",
|
||||||
"localhost:8123/ping"
|
"0.0.0.0:8123/ping"
|
||||||
]
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
@@ -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`
|
# 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:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.39.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.44.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@@ -203,7 +203,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.39.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.44.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -215,7 +215,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.21}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -229,7 +229,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.12}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.21}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -122,21 +122,20 @@ extensions:
|
|||||||
|
|
||||||
exporters:
|
exporters:
|
||||||
clickhousetraces:
|
clickhousetraces:
|
||||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
clickhousemetricswrite:
|
clickhousemetricswrite:
|
||||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
resource_to_telemetry_conversion:
|
resource_to_telemetry_conversion:
|
||||||
enabled: true
|
enabled: true
|
||||||
clickhousemetricswrite/prometheus:
|
clickhousemetricswrite/prometheus:
|
||||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
# logging: {}
|
|
||||||
|
|
||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/
|
dsn: tcp://clickhouse:9000/signoz_logs
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
# logging: {}
|
||||||
|
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ rule_files:
|
|||||||
scrape_configs: []
|
scrape_configs: []
|
||||||
|
|
||||||
remote_read:
|
remote_read:
|
||||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
- url: tcp://clickhouse:9000/signoz_metrics
|
||||||
|
|||||||
@@ -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/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).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/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
|
// PAT APIs
|
||||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof" // http profiler
|
_ "net/http/pprof" // http profiler
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
@@ -328,7 +329,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
r.Use(loggingMiddleware)
|
r.Use(loggingMiddleware)
|
||||||
|
|
||||||
apiHandler.RegisterRoutes(r, am)
|
apiHandler.RegisterRoutes(r, am)
|
||||||
apiHandler.RegisterMetricsRoutes(r, am)
|
|
||||||
apiHandler.RegisterLogsRoutes(r, am)
|
apiHandler.RegisterLogsRoutes(r, am)
|
||||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||||
@@ -393,13 +393,14 @@ func (lrw *loggingResponseWriter) Flush() {
|
|||||||
lrw.ResponseWriter.(http.Flusher).Flush()
|
lrw.ResponseWriter.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface{}, bool) {
|
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
|
||||||
pathToExtractBodyFrom := "/api/v3/query_range"
|
pathToExtractBodyFromV3 := "/api/v3/query_range"
|
||||||
|
pathToExtractBodyFromV4 := "/api/v4/query_range"
|
||||||
|
|
||||||
data := map[string]interface{}{}
|
data := map[string]interface{}{}
|
||||||
var postData *v3.QueryRangeParamsV3
|
var postData *v3.QueryRangeParamsV3
|
||||||
|
|
||||||
if path == pathToExtractBodyFrom && (r.Method == "POST") {
|
if (r.Method == "POST") && ((path == pathToExtractBodyFromV3) || (path == pathToExtractBodyFromV4)) {
|
||||||
if r.Body != nil {
|
if r.Body != nil {
|
||||||
bodyBytes, err := io.ReadAll(r.Body)
|
bodyBytes, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -417,6 +418,25 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
|
|||||||
return nil, false
|
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
|
signozMetricsUsed := false
|
||||||
signozLogsUsed := false
|
signozLogsUsed := false
|
||||||
signozTracesUsed := false
|
signozTracesUsed := false
|
||||||
@@ -445,6 +465,20 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
|
|||||||
data["tracesUsed"] = signozTracesUsed
|
data["tracesUsed"] = signozTracesUsed
|
||||||
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
|
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
|
||||||
if err == nil {
|
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)
|
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)
|
route := mux.CurrentRoute(r)
|
||||||
path, _ := route.GetPathTemplate()
|
path, _ := route.GetPathTemplate()
|
||||||
|
|
||||||
queryRangeV3data, metadataExists := extractQueryRangeV3Data(path, r)
|
queryRangeData, metadataExists := extractQueryRangeData(path, r)
|
||||||
getActiveLogs(path, r)
|
getActiveLogs(path, r)
|
||||||
|
|
||||||
lrw := NewLoggingResponseWriter(w)
|
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}
|
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
|
||||||
if metadataExists {
|
if metadataExists {
|
||||||
for key, value := range queryRangeV3data {
|
for key, value := range queryRangeData {
|
||||||
data[key] = value
|
data[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
frontend/.gitignore
vendored
Normal file
3
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
# Sentry Config File
|
||||||
|
.env.sentry-build-plugin
|
||||||
@@ -35,6 +35,14 @@ const config: Config.InitialOptions = {
|
|||||||
browsers: ['chromium', 'firefox', 'webkit'],
|
browsers: ['chromium', 'firefox', 'webkit'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
statements: 80,
|
||||||
|
branches: 65,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
"playwright:codegen:local": "playwright codegen http://localhost:3301",
|
"playwright:codegen:local": "playwright codegen http://localhost:3301",
|
||||||
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
|
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
|
||||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||||
"commitlint": "commitlint --edit $1"
|
"commitlint": "commitlint --edit $1",
|
||||||
|
"test": "jest --coverage",
|
||||||
|
"test:changedsince": "jest --changedSince=develop --coverage --silent"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.15.0"
|
"node": ">=16.15.0"
|
||||||
@@ -41,9 +43,12 @@
|
|||||||
"@radix-ui/react-tabs": "1.0.4",
|
"@radix-ui/react-tabs": "1.0.4",
|
||||||
"@radix-ui/react-tooltip": "1.0.7",
|
"@radix-ui/react-tooltip": "1.0.7",
|
||||||
"@sentry/react": "7.102.1",
|
"@sentry/react": "7.102.1",
|
||||||
"@sentry/webpack-plugin": "2.14.2",
|
"@sentry/webpack-plugin": "2.16.0",
|
||||||
"@signozhq/design-tokens": "0.0.8",
|
"@signozhq/design-tokens": "0.0.8",
|
||||||
"@uiw/react-md-editor": "3.23.5",
|
"@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",
|
"@xstate/react": "^3.0.0",
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"antd": "5.11.0",
|
"antd": "5.11.0",
|
||||||
@@ -121,6 +126,7 @@
|
|||||||
"web-vitals": "^0.2.4",
|
"web-vitals": "^0.2.4",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.88.2",
|
||||||
"webpack-dev-server": "^4.15.1",
|
"webpack-dev-server": "^4.15.1",
|
||||||
|
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||||
"xstate": "^4.31.0"
|
"xstate": "^4.31.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"button_test_channel": "Test",
|
"button_test_channel": "Test",
|
||||||
"button_return": "Back",
|
"button_return": "Back",
|
||||||
"field_channel_name": "Name",
|
"field_channel_name": "Name",
|
||||||
|
"field_send_resolved": "Send resolved alerts",
|
||||||
"field_channel_type": "Type",
|
"field_channel_type": "Type",
|
||||||
"field_webhook_url": "Webhook URL",
|
"field_webhook_url": "Webhook URL",
|
||||||
"field_slack_recipient": "Recipient",
|
"field_slack_recipient": "Recipient",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"button_test_channel": "Test",
|
"button_test_channel": "Test",
|
||||||
"button_return": "Back",
|
"button_return": "Back",
|
||||||
"field_channel_name": "Name",
|
"field_channel_name": "Name",
|
||||||
|
"field_send_resolved": "Send resolved alerts",
|
||||||
"field_channel_type": "Type",
|
"field_channel_type": "Type",
|
||||||
"field_webhook_url": "Webhook URL",
|
"field_webhook_url": "Webhook URL",
|
||||||
"field_slack_recipient": "Recipient",
|
"field_slack_recipient": "Recipient",
|
||||||
|
|||||||
@@ -48,5 +48,5 @@
|
|||||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
||||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||||
"SHORTCUTS": "SigNoz | Shortcuts",
|
"SHORTCUTS": "SigNoz | Shortcuts",
|
||||||
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations"
|
"INTEGRATIONS": "SigNoz | Integrations"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
|
||||||
setRoutes(newRoutes);
|
setRoutes(newRoutes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,11 +197,3 @@ export const InstalledIntegrations = Loadable(
|
|||||||
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const IntegrationsMarketPlace = Loadable(
|
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
ErrorDetails,
|
ErrorDetails,
|
||||||
IngestionSettings,
|
IngestionSettings,
|
||||||
InstalledIntegrations,
|
InstalledIntegrations,
|
||||||
IntegrationsMarketPlace,
|
|
||||||
LicensePage,
|
LicensePage,
|
||||||
ListAllALertsPage,
|
ListAllALertsPage,
|
||||||
LiveLogs,
|
LiveLogs,
|
||||||
@@ -338,18 +337,11 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'SHORTCUTS',
|
key: 'SHORTCUTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.INTEGRATIONS_INSTALLED,
|
path: ROUTES.INTEGRATIONS,
|
||||||
exact: true,
|
exact: true,
|
||||||
component: InstalledIntegrations,
|
component: InstalledIntegrations,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'INTEGRATIONS_INSTALLED',
|
key: 'INTEGRATIONS',
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ROUTES.INTEGRATIONS_MARKETPLACE,
|
|
||||||
exact: true,
|
|
||||||
component: IntegrationsMarketPlace,
|
|
||||||
isPrivate: true,
|
|
||||||
key: 'INTEGRATIONS_MARKETPLACE',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
email_configs: [
|
email_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
to: props.to,
|
to: props.to,
|
||||||
html: props.html,
|
html: props.html,
|
||||||
headers: props.headers,
|
headers: props.headers,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
msteams_configs: [
|
msteams_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
webhook_url: props.webhook_url,
|
webhook_url: props.webhook_url,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
text: props.text,
|
text: props.text,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
pagerduty_configs: [
|
pagerduty_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
routing_key: props.routing_key,
|
routing_key: props.routing_key,
|
||||||
client: props.client,
|
client: props.client,
|
||||||
client_url: props.client_url,
|
client_url: props.client_url,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
slack_configs: [
|
slack_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
api_url: props.api_url,
|
api_url: props.api_url,
|
||||||
channel: props.channel,
|
channel: props.channel,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const create = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
webhook_configs: [
|
webhook_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
url: props.api_url,
|
url: props.api_url,
|
||||||
http_config: httpConfig,
|
http_config: httpConfig,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editEmail = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
email_configs: [
|
email_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
to: props.to,
|
to: props.to,
|
||||||
html: props.html,
|
html: props.html,
|
||||||
headers: props.headers,
|
headers: props.headers,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editMsTeams = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
msteams_configs: [
|
msteams_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
webhook_url: props.webhook_url,
|
webhook_url: props.webhook_url,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
text: props.text,
|
text: props.text,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editOpsgenie = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
opsgenie_configs: [
|
opsgenie_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
api_key: props.api_key,
|
api_key: props.api_key,
|
||||||
description: props.description,
|
description: props.description,
|
||||||
priority: props.priority,
|
priority: props.priority,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editPager = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
pagerduty_configs: [
|
pagerduty_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
routing_key: props.routing_key,
|
routing_key: props.routing_key,
|
||||||
client: props.client,
|
client: props.client,
|
||||||
client_url: props.client_url,
|
client_url: props.client_url,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const editSlack = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
slack_configs: [
|
slack_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
api_url: props.api_url,
|
api_url: props.api_url,
|
||||||
channel: props.channel,
|
channel: props.channel,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const editWebhook = async (
|
|||||||
name: props.name,
|
name: props.name,
|
||||||
webhook_configs: [
|
webhook_configs: [
|
||||||
{
|
{
|
||||||
send_resolved: true,
|
send_resolved: props.send_resolved,
|
||||||
url: props.api_url,
|
url: props.api_url,
|
||||||
http_config: httpConfig,
|
http_config: httpConfig,
|
||||||
},
|
},
|
||||||
|
|||||||
28
frontend/src/api/common/logEvent.ts
Normal file
28
frontend/src/api/common/logEvent.ts
Normal 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;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from 'api';
|
import { ApiV4Instance } from 'api';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { MetricMetaProps } from 'types/api/metrics/getApDex';
|
import { MetricMetaProps } from 'types/api/metrics/getApDex';
|
||||||
|
|
||||||
@@ -6,4 +6,6 @@ export const getMetricMeta = (
|
|||||||
metricName: string,
|
metricName: string,
|
||||||
servicename: string,
|
servicename: string,
|
||||||
): Promise<AxiosResponse<MetricMetaProps>> =>
|
): Promise<AxiosResponse<MetricMetaProps>> =>
|
||||||
axios.get(`/metric_meta?metricName=${metricName}&serviceName=${servicename}`);
|
ApiV4Instance.get(
|
||||||
|
`/metric/metric_metadata?metricName=${metricName}&serviceName=${servicename}`,
|
||||||
|
);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ApiV2Instance as axios } from 'api';
|
import { ApiV3Instance as axios } from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
TagKeyProps,
|
TagKeyProps,
|
||||||
@@ -8,15 +9,19 @@ import {
|
|||||||
TagValueProps,
|
TagValueProps,
|
||||||
TagValuesPayloadProps,
|
TagValuesPayloadProps,
|
||||||
} from 'types/api/metrics/getResourceAttributes';
|
} from 'types/api/metrics/getResourceAttributes';
|
||||||
|
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export const getResourceAttributesTagKeys = async (
|
export const getResourceAttributesTagKeys = async (
|
||||||
props: TagKeyProps,
|
props: TagKeyProps,
|
||||||
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/metrics/autocomplete/tagKey?metricName=${props.metricName}${
|
`/autocomplete/attribute_keys?${createQueryParams({
|
||||||
props.match ? `&match=${props.match}` : ''
|
aggregateOperator: MetricAggregateOperator.RATE,
|
||||||
}`,
|
searchText: props.match,
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
aggregateAttribute: props.metricName,
|
||||||
|
})}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -35,7 +40,13 @@ export const getResourceAttributesTagValues = async (
|
|||||||
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
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 {
|
return {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ComponentType, lazy, LazyExoticComponent } from 'react';
|
import { ComponentType, lazy, LazyExoticComponent } from 'react';
|
||||||
|
import { lazyRetry } from 'utils/lazyWithRetries';
|
||||||
|
|
||||||
function Loadable(importPath: {
|
function Loadable(importPath: {
|
||||||
(): LoadableProps;
|
(): LoadableProps;
|
||||||
}): LazyExoticComponent<LazyComponent> {
|
}): LazyExoticComponent<LazyComponent> {
|
||||||
return lazy(() => importPath());
|
return lazy(() => lazyRetry(() => importPath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
type LazyComponent = ComponentType<Record<string, unknown>>;
|
type LazyComponent = ComponentType<Record<string, unknown>>;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import './DynamicColumnTable.syles.scss';
|
import './DynamicColumnTable.syles.scss';
|
||||||
|
|
||||||
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
import { SlidersHorizontal } from 'lucide-react';
|
import { SlidersHorizontal } from 'lucide-react';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
@@ -20,6 +21,7 @@ function DynamicColumnTable({
|
|||||||
columns,
|
columns,
|
||||||
dynamicColumns,
|
dynamicColumns,
|
||||||
onDragColumn,
|
onDragColumn,
|
||||||
|
facingIssueBtn,
|
||||||
...restProps
|
...restProps
|
||||||
}: DynamicColumnTableProps): JSX.Element {
|
}: DynamicColumnTableProps): JSX.Element {
|
||||||
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
||||||
@@ -83,19 +85,22 @@ function DynamicColumnTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="DynamicColumnTable">
|
<div className="DynamicColumnTable">
|
||||||
{dynamicColumns && (
|
<Flex justify="flex-end" align="center" gap={8}>
|
||||||
<Dropdown
|
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
|
||||||
getPopupContainer={popupContainer}
|
{dynamicColumns && (
|
||||||
menu={{ items }}
|
<Dropdown
|
||||||
trigger={['click']}
|
getPopupContainer={popupContainer}
|
||||||
>
|
menu={{ items }}
|
||||||
<Button
|
trigger={['click']}
|
||||||
className="dynamicColumnTable-button filter-btn"
|
>
|
||||||
size="middle"
|
<Button
|
||||||
icon={<SlidersHorizontal size={14} />}
|
className="dynamicColumnTable-button filter-btn"
|
||||||
/>
|
size="middle"
|
||||||
</Dropdown>
|
icon={<SlidersHorizontal size={14} />}
|
||||||
)}
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columnsData}
|
columns={columnsData}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { TableProps } from 'antd';
|
import { TableProps } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
|
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
|
||||||
|
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
|
|
||||||
import { TableDataSource } from './contants';
|
import { TableDataSource } from './contants';
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
|
|||||||
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
||||||
dynamicColumns: TableProps<any>['columns'];
|
dynamicColumns: TableProps<any>['columns'];
|
||||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||||
|
facingIssueBtn?: FacingIssueBtnProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetVisibleColumnsFunction = (
|
export type GetVisibleColumnsFunction = (
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import './LabelColumn.styles.scss';
|
import './LabelColumn.styles.scss';
|
||||||
|
|
||||||
import { Popover, Tag } from 'antd';
|
import { Popover, Tag } from 'antd';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
|
||||||
|
|
||||||
import { LabelColumnProps } from './TableRenderer.types';
|
import { LabelColumnProps } from './TableRenderer.types';
|
||||||
import TagWithToolTip from './TagWithToolTip';
|
import TagWithToolTip from './TagWithToolTip';
|
||||||
|
import { getLabelAndValueContent } from './utils';
|
||||||
|
|
||||||
function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
|
function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
|
||||||
const newLabels = labels.length > 3 ? labels.slice(0, 3) : labels;
|
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 && (
|
{remainingLabels.length > 0 && (
|
||||||
<Popover
|
<Popover
|
||||||
getPopupContainer={popupContainer}
|
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
showArrow={false}
|
showArrow={false}
|
||||||
content={
|
content={
|
||||||
<div>
|
<div>
|
||||||
{labels.map(
|
{labels.map(
|
||||||
(label: string): JSX.Element => (
|
(label: string): JSX.Element => (
|
||||||
<TagWithToolTip
|
<div key={label}>
|
||||||
key={label}
|
<Tag className="label-column--tag" color={color}>
|
||||||
label={label}
|
{getLabelAndValueContent(label, value && value[label])}
|
||||||
color={color}
|
</Tag>
|
||||||
value={value}
|
</div>
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ export const getLabelRenderingValue = (
|
|||||||
return label;
|
return label;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getLabelAndValueContent = (
|
||||||
|
label: string,
|
||||||
|
value?: string,
|
||||||
|
): string => {
|
||||||
|
if (value) {
|
||||||
|
return `${label}: ${value}`;
|
||||||
|
}
|
||||||
|
return `${label}`;
|
||||||
|
};
|
||||||
|
|
||||||
interface GeneratorResizeTableColumnsProp<T> {
|
interface GeneratorResizeTableColumnsProp<T> {
|
||||||
baseColumnOptions: ColumnsType<T>;
|
baseColumnOptions: ColumnsType<T>;
|
||||||
dynamicColumnOption: { key: string; columnOption: ColumnType<T> }[];
|
dynamicColumnOption: { key: string; columnOption: ColumnType<T> }[];
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.facing-issue-button {
|
||||||
|
color: var(--bg-amber-500);
|
||||||
|
border-color: var(--bg-amber-500);
|
||||||
|
|
||||||
|
.ant-btn:hover {
|
||||||
|
color: var(--bg-amber-400) !important;
|
||||||
|
border-color: var(--bg-amber-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx
Normal file
57
frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import './FacingIssueBtn.style.scss';
|
||||||
|
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { FeatureKeys } from 'constants/features';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
|
import { defaultTo } from 'lodash-es';
|
||||||
|
import { HelpCircle } from 'lucide-react';
|
||||||
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
|
export interface FacingIssueBtnProps {
|
||||||
|
eventName: string;
|
||||||
|
attributes: Record<string, unknown>;
|
||||||
|
message?: string;
|
||||||
|
buttonText?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FacingIssueBtn({
|
||||||
|
attributes,
|
||||||
|
eventName,
|
||||||
|
message = '',
|
||||||
|
buttonText = '',
|
||||||
|
className = '',
|
||||||
|
}: FacingIssueBtnProps): JSX.Element | null {
|
||||||
|
const handleFacingIssuesClick = (): void => {
|
||||||
|
logEvent(eventName, attributes);
|
||||||
|
|
||||||
|
if (window.Intercom) {
|
||||||
|
window.Intercom('showNewMessage', defaultTo(message, ''));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
|
||||||
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
|
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
|
||||||
|
<div className="facing-issue-button">
|
||||||
|
<Button
|
||||||
|
className={cx('periscope-btn', 'facing-issue-button', className)}
|
||||||
|
onClick={handleFacingIssuesClick}
|
||||||
|
icon={<HelpCircle size={14} />}
|
||||||
|
>
|
||||||
|
{buttonText || 'Facing issues?'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FacingIssueBtn.defaultProps = {
|
||||||
|
message: '',
|
||||||
|
buttonText: '',
|
||||||
|
className: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FacingIssueBtn;
|
||||||
@@ -29,6 +29,7 @@ export const getComponentForPanelType = (
|
|||||||
[PANEL_TYPES.LIST]:
|
[PANEL_TYPES.LIST]:
|
||||||
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
|
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
|
||||||
[PANEL_TYPES.BAR]: Uplot,
|
[PANEL_TYPES.BAR]: Uplot,
|
||||||
|
[PANEL_TYPES.PIE]: null,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ export enum PANEL_TYPES {
|
|||||||
LIST = 'list',
|
LIST = 'list',
|
||||||
TRACE = 'trace',
|
TRACE = 'trace',
|
||||||
BAR = 'bar',
|
BAR = 'bar',
|
||||||
|
PIE = 'pie',
|
||||||
EMPTY_WIDGET = 'EMPTY_WIDGET',
|
EMPTY_WIDGET = 'EMPTY_WIDGET',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
|
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
export const queryFunctionOptions: SelectOption<string, string>[] = [
|
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
||||||
{
|
{
|
||||||
value: QueryFunctionsTypes.CUTOFF_MIN,
|
value: QueryFunctionsTypes.CUTOFF_MIN,
|
||||||
label: 'Cut Off 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 {
|
interface QueryFunctionConfigType {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
showInput: boolean;
|
showInput: boolean;
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ const ROUTES = {
|
|||||||
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||||
WORKSPACE_LOCKED: '/workspace-locked',
|
WORKSPACE_LOCKED: '/workspace-locked',
|
||||||
SHORTCUTS: '/shortcuts',
|
SHORTCUTS: '/shortcuts',
|
||||||
INTEGRATIONS_BASE: '/integrations',
|
INTEGRATIONS: '/integrations',
|
||||||
INTEGRATIONS_INSTALLED: '/integrations/installed',
|
|
||||||
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default ROUTES;
|
export default ROUTES;
|
||||||
|
|||||||
3
frontend/src/constants/sessionStorage.ts
Normal file
3
frontend/src/constants/sessionStorage.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export enum SESSIONSTORAGE {
|
||||||
|
RETRY_LAZY_REFRESHED = 'retry-lazy-refreshed',
|
||||||
|
}
|
||||||
@@ -9,9 +9,10 @@ export const DashboardShortcuts = {
|
|||||||
|
|
||||||
export const DashboardShortcutsName = {
|
export const DashboardShortcutsName = {
|
||||||
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
|
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
|
||||||
|
DiscardChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+d`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DashboardShortcutsDescription = {
|
export const DashboardShortcutsDescription = {
|
||||||
SaveChanges: 'Save Changes',
|
SaveChanges: 'Save Changes for panel',
|
||||||
DiscardChanges: 'Discard Changes',
|
DiscardChanges: 'Discard Changes for panel',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ export const QBShortcutsName = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const QBShortcutsDescription = {
|
export const QBShortcutsDescription = {
|
||||||
StageAndRunQuery: 'Stage and Run the query',
|
StageAndRunQuery: 'Stage and Run the current query',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
width: 100%;
|
width: calc(100% - 64px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -16,6 +17,12 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.docked {
|
||||||
|
.app-content {
|
||||||
|
width: calc(100% - 240px);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.isDarkMode {
|
.isDarkMode {
|
||||||
|
|||||||
@@ -311,7 +311,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
<Flex
|
||||||
|
className={cx(
|
||||||
|
'app-layout',
|
||||||
|
isDarkMode ? 'darkMode' : 'lightMode',
|
||||||
|
!collapsed && !renderFullScreen ? 'docked' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{isToDisplayLayout && !renderFullScreen && (
|
{isToDisplayLayout && !renderFullScreen && (
|
||||||
<SideNav
|
<SideNav
|
||||||
licenseData={licenseData}
|
licenseData={licenseData}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.billing-container {
|
.billing-container {
|
||||||
|
margin-bottom: 40px;
|
||||||
padding-top: 36px;
|
padding-top: 36px;
|
||||||
width: 65%;
|
width: 65%;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Spinner from 'components/Spinner';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { lazy, Suspense, useMemo } from 'react';
|
import { lazy, Suspense, useMemo } from 'react';
|
||||||
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||||
|
import { lazyRetry } from 'utils/lazyWithRetries';
|
||||||
|
|
||||||
import ErrorLink from './ErrorLink';
|
import ErrorLink from './ErrorLink';
|
||||||
import LinkContainer from './Link';
|
import LinkContainer from './Link';
|
||||||
@@ -17,8 +18,9 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
|||||||
|
|
||||||
const items = sortedConfig.map((item) => {
|
const items = sortedConfig.map((item) => {
|
||||||
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
|
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 {
|
return {
|
||||||
key: item.text + item.href,
|
key: item.text + item.href,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function CreateAlertChannels({
|
|||||||
EmailChannel
|
EmailChannel
|
||||||
>
|
>
|
||||||
>({
|
>({
|
||||||
|
send_resolved: true,
|
||||||
text: `{{ range .Alerts -}}
|
text: `{{ range .Alerts -}}
|
||||||
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ function CreateAlertChannels({
|
|||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
channel: selectedConfig?.channel || '',
|
channel: selectedConfig?.channel || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
}),
|
}),
|
||||||
@@ -158,7 +159,7 @@ function CreateAlertChannels({
|
|||||||
let request: WebhookChannel = {
|
let request: WebhookChannel = {
|
||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
|
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
|
||||||
@@ -226,7 +227,7 @@ function CreateAlertChannels({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
routing_key: selectedConfig?.routing_key || '',
|
routing_key: selectedConfig?.routing_key || '',
|
||||||
client: selectedConfig?.client || '',
|
client: selectedConfig?.client || '',
|
||||||
client_url: selectedConfig?.client_url || '',
|
client_url: selectedConfig?.client_url || '',
|
||||||
@@ -274,7 +275,7 @@ function CreateAlertChannels({
|
|||||||
() => ({
|
() => ({
|
||||||
api_key: selectedConfig?.api_key || '',
|
api_key: selectedConfig?.api_key || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
description: selectedConfig?.description || '',
|
description: selectedConfig?.description || '',
|
||||||
message: selectedConfig?.message || '',
|
message: selectedConfig?.message || '',
|
||||||
priority: selectedConfig?.priority || '',
|
priority: selectedConfig?.priority || '',
|
||||||
@@ -312,7 +313,7 @@ function CreateAlertChannels({
|
|||||||
const prepareEmailRequest = useCallback(
|
const prepareEmailRequest = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
to: selectedConfig?.to || '',
|
to: selectedConfig?.to || '',
|
||||||
html: selectedConfig?.html || '',
|
html: selectedConfig?.html || '',
|
||||||
headers: selectedConfig?.headers || {},
|
headers: selectedConfig?.headers || {},
|
||||||
@@ -350,7 +351,7 @@ function CreateAlertChannels({
|
|||||||
() => ({
|
() => ({
|
||||||
webhook_url: selectedConfig?.webhook_url || '',
|
webhook_url: selectedConfig?.webhook_url || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Form, Row } from 'antd';
|
|||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
import FormAlertRules from 'container/FormAlertRules';
|
||||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
@@ -18,9 +19,7 @@ import SelectAlertType from './SelectAlertType';
|
|||||||
|
|
||||||
function CreateRules(): JSX.Element {
|
function CreateRules(): JSX.Element {
|
||||||
const [initValues, setInitValues] = useState<AlertDef | null>(null);
|
const [initValues, setInitValues] = useState<AlertDef | null>(null);
|
||||||
const [alertType, setAlertType] = useState<AlertTypes>(
|
const [alertType, setAlertType] = useState<AlertTypes>();
|
||||||
AlertTypes.METRICS_BASED_ALERT,
|
|
||||||
);
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
@@ -56,10 +55,10 @@ function CreateRules(): JSX.Element {
|
|||||||
}
|
}
|
||||||
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
|
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
|
||||||
|
|
||||||
const alertType = ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
const alertTypeFromQuery = ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
||||||
|
|
||||||
if (alertType) {
|
if (alertTypeFromQuery && !isEqual(alertType, alertTypeFromQuery)) {
|
||||||
onSelectType(alertType);
|
onSelectType(alertTypeFromQuery);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [compositeQuery]);
|
}, [compositeQuery]);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function EditAlertChannels({
|
|||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
channel: selectedConfig?.channel || '',
|
channel: selectedConfig?.channel || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
id,
|
id,
|
||||||
@@ -115,7 +115,7 @@ function EditAlertChannels({
|
|||||||
return {
|
return {
|
||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
name: name || '',
|
name: name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
id,
|
id,
|
||||||
@@ -284,7 +284,7 @@ function EditAlertChannels({
|
|||||||
() => ({
|
() => ({
|
||||||
webhook_url: selectedConfig?.webhook_url || '',
|
webhook_url: selectedConfig?.webhook_url || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: selectedConfig?.send_resolved || false,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.hide-update {
|
.hide-update {
|
||||||
left: calc(50% - 41px) !important;
|
left: calc(50% - 72px) !important;
|
||||||
}
|
}
|
||||||
.explorer-update {
|
.explorer-update {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
left: calc(50% - 250px);
|
left: calc(50% - 352px);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.explorer-options {
|
.explorer-options {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
@@ -78,11 +77,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
border: none;
|
||||||
|
|
||||||
border: 1px solid #1d2023;
|
border: 1px solid #1d2023;
|
||||||
color: #c0c1c3;
|
|
||||||
background-color: #161922;
|
|
||||||
|
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
|||||||
@@ -373,34 +373,42 @@ function ExplorerOptions({
|
|||||||
onClick={handleSaveViewModalToggle}
|
onClick={handleSaveViewModalToggle}
|
||||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||||
disabled={viewsIsLoading || isRefetching}
|
disabled={viewsIsLoading || isRefetching}
|
||||||
|
icon={<Disc3 size={16} />}
|
||||||
>
|
>
|
||||||
<Disc3 size={16} /> Save this view
|
Save this view
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
||||||
|
|
||||||
<div className={cx('actions', 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
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
shape="circle"
|
shape="circle"
|
||||||
onClick={onCreateAlertsHandler}
|
onClick={hideToolbar}
|
||||||
>
|
icon={<PanelBottomClose size={16} />}
|
||||||
<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>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -413,6 +421,7 @@ function ExplorerOptions({
|
|||||||
isQueryUpdated={isQueryUpdated}
|
isQueryUpdated={isQueryUpdated}
|
||||||
handleClearSelect={handleClearSelect}
|
handleClearSelect={handleClearSelect}
|
||||||
onUpdateQueryHandler={onUpdateQueryHandler}
|
onUpdateQueryHandler={onUpdateQueryHandler}
|
||||||
|
isEditDeleteSupported={isEditDeleteSupported}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface DroppableAreaProps {
|
|||||||
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
||||||
handleClearSelect: () => void;
|
handleClearSelect: () => void;
|
||||||
onUpdateQueryHandler: () => void;
|
onUpdateQueryHandler: () => void;
|
||||||
|
isEditDeleteSupported: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExplorerOptionsHideArea({
|
function ExplorerOptionsHideArea({
|
||||||
@@ -25,6 +26,7 @@ function ExplorerOptionsHideArea({
|
|||||||
setIsExplorerOptionHidden,
|
setIsExplorerOptionHidden,
|
||||||
handleClearSelect,
|
handleClearSelect,
|
||||||
onUpdateQueryHandler,
|
onUpdateQueryHandler,
|
||||||
|
isEditDeleteSupported,
|
||||||
}: DroppableAreaProps): JSX.Element {
|
}: DroppableAreaProps): JSX.Element {
|
||||||
const handleShowExplorerOption = (): void => {
|
const handleShowExplorerOption = (): void => {
|
||||||
if (setIsExplorerOptionHidden) {
|
if (setIsExplorerOptionHidden) {
|
||||||
@@ -47,14 +49,16 @@ function ExplorerOptionsHideArea({
|
|||||||
icon={<X size={14} color={Color.BG_INK_500} />}
|
icon={<X size={14} color={Color.BG_INK_500} />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Update this View">
|
{isEditDeleteSupported && (
|
||||||
<Button
|
<Tooltip title="Update this View">
|
||||||
onClick={onUpdateQueryHandler}
|
<Button
|
||||||
className="action-btn"
|
onClick={onUpdateQueryHandler}
|
||||||
style={{ background: Color.BG_ROBIN_500 }}
|
className="action-btn"
|
||||||
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
|
style={{ background: Color.BG_ROBIN_500 }}
|
||||||
/>
|
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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 { Store } from 'antd/lib/form/interface';
|
||||||
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
|
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
@@ -95,6 +95,22 @@ function FormAlertChannels({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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">
|
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
|
||||||
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
|
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
|
||||||
<Select.Option value="slack" key="slack">
|
<Select.Option value="slack" key="slack">
|
||||||
|
|||||||
@@ -1,45 +1,50 @@
|
|||||||
.create-alert-modal {
|
.create-alert-modal {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
background-color: var(--bg-ink-300);
|
background-color: var(--bg-ink-300);
|
||||||
.ant-modal-confirm-title {
|
.ant-modal-confirm-title {
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-content {
|
.ant-modal-confirm-content {
|
||||||
.ant-typography {
|
.ant-typography {
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-btns {
|
.ant-modal-confirm-btns {
|
||||||
button:nth-of-type(1) {
|
button:nth-of-type(1) {
|
||||||
background-color: var(--bg-slate-400);
|
background-color: var(--bg-slate-400);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
background-color: var(--bg-vanilla-100);
|
background-color: var(--bg-vanilla-100);
|
||||||
.ant-modal-confirm-title {
|
.ant-modal-confirm-title {
|
||||||
color: var(--bg-ink-500);
|
color: var(--bg-ink-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-content {
|
.ant-modal-confirm-content {
|
||||||
.ant-typography {
|
.ant-typography {
|
||||||
color: var(--bg-ink-500);
|
color: var(--bg-ink-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-confirm-btns {
|
.ant-modal-confirm-btns {
|
||||||
button:nth-of-type(1) {
|
button:nth-of-type(1) {
|
||||||
background-color: var(--bg-vanilla-300);
|
background-color: var(--bg-vanilla-300);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--bg-ink-500);
|
color: var(--bg-ink-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.facing-issue-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ function QuerySection({
|
|||||||
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||||
}}
|
}}
|
||||||
showFunctions={
|
showFunctions={
|
||||||
alertType === AlertTypes.METRICS_BASED_ALERT &&
|
(alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||||
alertDef.version === ENTITY_VERSION_V4
|
alertDef.version === ENTITY_VERSION_V4) ||
|
||||||
|
alertType === AlertTypes.LOGS_BASED_ALERT
|
||||||
}
|
}
|
||||||
version={alertDef.version || 'v3'}
|
version={alertDef.version || 'v3'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import testAlertApi from 'api/alerts/testAlert';
|
import testAlertApi from 'api/alerts/testAlert';
|
||||||
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -138,15 +139,21 @@ function FormAlertRules({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set selectedQueryName based on the length of queryOptions
|
// Set selectedQueryName based on the length of queryOptions
|
||||||
setAlertDef((def) => ({
|
const selectedQueryName = alertDef?.condition?.selectedQueryName;
|
||||||
...def,
|
if (
|
||||||
condition: {
|
!selectedQueryName ||
|
||||||
...def.condition,
|
!queryOptions.some((option) => option.value === selectedQueryName)
|
||||||
selectedQueryName:
|
) {
|
||||||
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
|
setAlertDef((def) => ({
|
||||||
},
|
...def,
|
||||||
}));
|
condition: {
|
||||||
}, [currentQuery?.queryType, queryOptions]);
|
...def.condition,
|
||||||
|
selectedQueryName:
|
||||||
|
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [alertDef, currentQuery?.queryType, queryOptions]);
|
||||||
|
|
||||||
const onCancelHandler = useCallback(() => {
|
const onCancelHandler = useCallback(() => {
|
||||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||||
@@ -482,6 +489,8 @@ function FormAlertRules({
|
|||||||
alertDef?.broadcastToAll ||
|
alertDef?.broadcastToAll ||
|
||||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
||||||
|
|
||||||
|
const isRuleCreated = !ruleId || ruleId === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Element}
|
{Element}
|
||||||
@@ -563,6 +572,30 @@ function FormAlertRules({
|
|||||||
</StyledLeftContainer>
|
</StyledLeftContainer>
|
||||||
<Col flex="1 1 300px">
|
<Col flex="1 1 300px">
|
||||||
<UserGuide queryType={currentQuery.queryType} />
|
<UserGuide queryType={currentQuery.queryType} />
|
||||||
|
<FacingIssueBtn
|
||||||
|
attributes={{
|
||||||
|
alert: alertDef?.alert,
|
||||||
|
alertType: alertDef?.alertType,
|
||||||
|
id: ruleId,
|
||||||
|
ruleType: alertDef?.ruleType,
|
||||||
|
state: (alertDef as any)?.state,
|
||||||
|
panelType,
|
||||||
|
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
|
||||||
|
}}
|
||||||
|
className="facing-issue-btn"
|
||||||
|
eventName="Alert: Facing Issues in alert"
|
||||||
|
buttonText="Facing Issues in alert"
|
||||||
|
message={`Hi Team,
|
||||||
|
|
||||||
|
I am facing issues configuring alerts in SigNoz. Here are my alert rule details
|
||||||
|
|
||||||
|
Name: ${alertDef?.alert || ''}
|
||||||
|
Alert Type: ${alertDef?.alertType || ''}
|
||||||
|
State: ${(alertDef as any)?.state || ''}
|
||||||
|
Alert Id: ${ruleId}
|
||||||
|
|
||||||
|
Thanks`}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -26,5 +26,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
|
|||||||
LIST: false,
|
LIST: false,
|
||||||
TRACE: false,
|
TRACE: false,
|
||||||
BAR: true,
|
BAR: true,
|
||||||
|
PIE: false,
|
||||||
EMPTY_WIDGET: false,
|
EMPTY_WIDGET: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,62 +1,60 @@
|
|||||||
import './WidgetFullView.styles.scss';
|
import './WidgetFullView.styles.scss';
|
||||||
|
|
||||||
import { SyncOutlined } from '@ant-design/icons';
|
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
import { Button } from 'antd';
|
import { Button, Spin } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
|
||||||
import {
|
import {
|
||||||
timeItems,
|
timeItems,
|
||||||
timePreferance,
|
timePreferance,
|
||||||
} from 'container/NewWidget/RightContainer/timeItems';
|
} from 'container/NewWidget/RightContainer/timeItems';
|
||||||
|
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import { useChartMutable } from 'hooks/useChartMutable';
|
import { useChartMutable } from 'hooks/useChartMutable';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import GetMinMax from 'lib/getMinMax';
|
||||||
|
import history from 'lib/history';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
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 { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import uPlot from 'uplot';
|
import { getGraphType } from 'utils/getGraphType';
|
||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
|
||||||
|
|
||||||
import { getLocalStorageGraphVisibilityState } from '../utils';
|
import { getLocalStorageGraphVisibilityState } from '../utils';
|
||||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||||
import GraphManager from './GraphManager';
|
|
||||||
import { GraphContainer, TimeContainer } from './styles';
|
import { GraphContainer, TimeContainer } from './styles';
|
||||||
import { FullViewProps } from './types';
|
import { FullViewProps } from './types';
|
||||||
|
|
||||||
function FullView({
|
function FullView({
|
||||||
widget,
|
widget,
|
||||||
fullViewOptions = true,
|
fullViewOptions = true,
|
||||||
onClickHandler,
|
|
||||||
name,
|
|
||||||
version,
|
version,
|
||||||
originalName,
|
originalName,
|
||||||
yAxisUnit,
|
|
||||||
onDragSelect,
|
|
||||||
isDependedDataLoaded = false,
|
isDependedDataLoaded = false,
|
||||||
onToggleModelHandler,
|
onToggleModelHandler,
|
||||||
parentChartRef,
|
|
||||||
}: FullViewProps): JSX.Element {
|
}: FullViewProps): JSX.Element {
|
||||||
const { selectedTime: globalSelectedTime } = useSelector<
|
const { selectedTime: globalSelectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const fullViewRef = useRef<HTMLDivElement>(null);
|
const fullViewRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
|
|
||||||
|
|
||||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const getSelectedTime = useCallback(
|
const getSelectedTime = useCallback(
|
||||||
@@ -74,24 +72,70 @@ function FullView({
|
|||||||
|
|
||||||
const updatedQuery = useStepInterval(widget?.query);
|
const updatedQuery = useStepInterval(widget?.query);
|
||||||
|
|
||||||
const response = useGetQueryRange(
|
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||||
{
|
if (widget.panelTypes !== PANEL_TYPES.LIST) {
|
||||||
selectedTime: selectedTime.enum,
|
return {
|
||||||
graphType:
|
selectedTime: selectedTime.enum,
|
||||||
widget.panelTypes === PANEL_TYPES.BAR
|
graphType: getGraphType(widget.panelTypes),
|
||||||
? PANEL_TYPES.TIME_SERIES
|
query: updatedQuery,
|
||||||
: widget.panelTypes,
|
globalSelectedInterval: globalSelectedTime,
|
||||||
|
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||||
|
return {
|
||||||
query: updatedQuery,
|
query: updatedQuery,
|
||||||
|
graphType: PANEL_TYPES.LIST,
|
||||||
|
selectedTime: widget?.timePreferance || 'GLOBAL_TIME',
|
||||||
globalSelectedInterval: globalSelectedTime,
|
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,
|
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
|
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
|
||||||
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
|
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<
|
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
|
||||||
boolean[]
|
boolean[]
|
||||||
>(Array(response.data?.payload.data.result.length).fill(true));
|
>(Array(response.data?.payload.data.result.length).fill(true));
|
||||||
@@ -118,60 +162,6 @@ function FullView({
|
|||||||
response.data.payload.data.result = sortedSeriesData;
|
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(() => {
|
useEffect(() => {
|
||||||
graphsVisibilityStates?.forEach((e, i) => {
|
graphsVisibilityStates?.forEach((e, i) => {
|
||||||
fullViewChartRef?.current?.toggleGraph(i, e);
|
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||||
@@ -180,7 +170,7 @@ function FullView({
|
|||||||
|
|
||||||
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
|
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..." />;
|
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +179,9 @@ function FullView({
|
|||||||
<div className="full-view-header-container">
|
<div className="full-view-header-container">
|
||||||
{fullViewOptions && (
|
{fullViewOptions && (
|
||||||
<TimeContainer $panelType={widget.panelTypes}>
|
<TimeContainer $panelType={widget.panelTypes}>
|
||||||
|
{response.isFetching && (
|
||||||
|
<Spin spinning indicator={<LoadingOutlined spin />} />
|
||||||
|
)}
|
||||||
<TimePreference
|
<TimePreference
|
||||||
selectedTime={selectedTime}
|
selectedTime={selectedTime}
|
||||||
setSelectedTime={setSelectedTime}
|
setSelectedTime={setSelectedTime}
|
||||||
@@ -214,47 +207,24 @@ function FullView({
|
|||||||
})}
|
})}
|
||||||
ref={fullViewRef}
|
ref={fullViewRef}
|
||||||
>
|
>
|
||||||
{chartOptions && (
|
<GraphContainer
|
||||||
<GraphContainer
|
style={{
|
||||||
style={{
|
height: isListView ? '100%' : '90%',
|
||||||
height: isListView ? '100%' : '90%',
|
}}
|
||||||
}}
|
isGraphLegendToggleAvailable={canModifyChart}
|
||||||
isGraphLegendToggleAvailable={canModifyChart}
|
>
|
||||||
>
|
<PanelWrapper
|
||||||
<GridPanelSwitch
|
queryResponse={response}
|
||||||
panelType={widget.panelTypes}
|
widget={widget}
|
||||||
data={chartData}
|
setRequestData={setRequestData}
|
||||||
options={chartOptions}
|
isFullViewMode
|
||||||
onClickHandler={onClickHandler}
|
onToggleModelHandler={onToggleModelHandler}
|
||||||
name={name}
|
setGraphVisibility={setGraphsVisibilityStates}
|
||||||
yAxisUnit={yAxisUnit}
|
graphVisibility={graphsVisibilityStates}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
panelData={response.data?.payload.data.newResult.data.result || []}
|
/>
|
||||||
query={widget.query}
|
</GraphContainer>
|
||||||
ref={fullViewChartRef}
|
|
||||||
thresholds={widget.thresholds}
|
|
||||||
selectedLogFields={widget.selectedLogFields}
|
|
||||||
dataSource={widget.query.builder.queryData[0].dataSource}
|
|
||||||
selectedTracesFields={widget.selectedTracesFields}
|
|
||||||
selectedTime={selectedTime}
|
|
||||||
/>
|
|
||||||
</GraphContainer>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canModifyChart && chartOptions && !isDashboardLocked && (
|
|
||||||
<GraphManager
|
|
||||||
data={chartData}
|
|
||||||
name={originalName}
|
|
||||||
options={chartOptions}
|
|
||||||
yAxisUnit={yAxisUnit}
|
|
||||||
onToggleModelHandler={onToggleModelHandler}
|
|
||||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
|
||||||
graphsVisibilityStates={graphsVisibilityStates}
|
|
||||||
lineChartRef={fullViewChartRef}
|
|
||||||
parentChartRef={parentChartRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const NotFoundContainer = styled.div`
|
|||||||
export const TimeContainer = styled.div<Props>`
|
export const TimeContainer = styled.div<Props>`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
${({ $panelType }): FlattenSimpleInterpolation =>
|
${({ $panelType }): FlattenSimpleInterpolation =>
|
||||||
$panelType === PANEL_TYPES.TABLE
|
$panelType === PANEL_TYPES.TABLE
|
||||||
? css`
|
? css`
|
||||||
|
|||||||
@@ -53,10 +53,8 @@ export interface FullViewProps {
|
|||||||
version?: string;
|
version?: string;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
onDragSelect: (start: number, end: number) => void;
|
|
||||||
isDependedDataLoaded?: boolean;
|
isDependedDataLoaded?: boolean;
|
||||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||||
parentChartRef: GraphManagerProps['lineChartRef'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphManagerProps extends UplotProps {
|
export interface GraphManagerProps extends UplotProps {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
|
|||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
@@ -33,23 +33,20 @@ import FullView from './FullView';
|
|||||||
import { Modal } from './styles';
|
import { Modal } from './styles';
|
||||||
import { WidgetGraphComponentProps } from './types';
|
import { WidgetGraphComponentProps } from './types';
|
||||||
import { getLocalStorageGraphVisibilityState } from './utils';
|
import { getLocalStorageGraphVisibilityState } from './utils';
|
||||||
|
// import { getLocalStorageGraphVisibilityState } from './utils';
|
||||||
|
|
||||||
function WidgetGraphComponent({
|
function WidgetGraphComponent({
|
||||||
widget,
|
widget,
|
||||||
queryResponse,
|
queryResponse,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
name,
|
|
||||||
version,
|
version,
|
||||||
threshold,
|
threshold,
|
||||||
headerMenuList,
|
headerMenuList,
|
||||||
isWarning,
|
isWarning,
|
||||||
data,
|
isFetchingResponse,
|
||||||
options,
|
setRequestData,
|
||||||
graphVisibiltyState,
|
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
setGraphVisibility,
|
|
||||||
isFetchingResponse,
|
|
||||||
}: WidgetGraphComponentProps): JSX.Element {
|
}: WidgetGraphComponentProps): JSX.Element {
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
@@ -61,12 +58,15 @@ function WidgetGraphComponent({
|
|||||||
const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id;
|
const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id;
|
||||||
|
|
||||||
const lineChartRef = useRef<ToggleGraphProps>();
|
const lineChartRef = useRef<ToggleGraphProps>();
|
||||||
|
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
|
||||||
|
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
|
||||||
|
);
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lineChartRef.current) return;
|
if (!lineChartRef.current) return;
|
||||||
|
|
||||||
graphVisibiltyState.forEach((state, index) => {
|
graphVisibility.forEach((state, index) => {
|
||||||
lineChartRef.current?.toggleGraph(index, state);
|
lineChartRef.current?.toggleGraph(index, state);
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -210,7 +210,7 @@ function WidgetGraphComponent({
|
|||||||
graphVisibilityStates: localStoredVisibilityState,
|
graphVisibilityStates: localStoredVisibilityState,
|
||||||
} = getLocalStorageGraphVisibilityState({
|
} = getLocalStorageGraphVisibilityState({
|
||||||
apiResponse: queryResponse.data.payload.data.result,
|
apiResponse: queryResponse.data.payload.data.result,
|
||||||
name,
|
name: widget.id,
|
||||||
});
|
});
|
||||||
setGraphVisibility(localStoredVisibilityState);
|
setGraphVisibility(localStoredVisibilityState);
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@ function WidgetGraphComponent({
|
|||||||
onBlur={(): void => {
|
onBlur={(): void => {
|
||||||
setHovered(false);
|
setHovered(false);
|
||||||
}}
|
}}
|
||||||
id={name}
|
id={widget.id}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
@@ -278,14 +278,12 @@ function WidgetGraphComponent({
|
|||||||
className="widget-full-view"
|
className="widget-full-view"
|
||||||
>
|
>
|
||||||
<FullView
|
<FullView
|
||||||
name={`${name}expanded`}
|
name={`${widget.id}expanded`}
|
||||||
version={version}
|
version={version}
|
||||||
originalName={name}
|
originalName={widget.id}
|
||||||
widget={widget}
|
widget={widget}
|
||||||
yAxisUnit={widget.yAxisUnit}
|
yAxisUnit={widget.yAxisUnit}
|
||||||
onToggleModelHandler={onToggleModelHandler}
|
onToggleModelHandler={onToggleModelHandler}
|
||||||
parentChartRef={lineChartRef}
|
|
||||||
onDragSelect={onDragSelect}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -305,26 +303,22 @@ function WidgetGraphComponent({
|
|||||||
isFetchingResponse={isFetchingResponse}
|
isFetchingResponse={isFetchingResponse}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{queryResponse.isLoading && <Skeleton />}
|
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
|
||||||
|
<Skeleton />
|
||||||
|
)}
|
||||||
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
|
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
|
||||||
<div
|
<div
|
||||||
className={cx('widget-graph-container', widget.panelTypes)}
|
className={cx('widget-graph-container', widget.panelTypes)}
|
||||||
ref={graphRef}
|
ref={graphRef}
|
||||||
>
|
>
|
||||||
<GridPanelSwitch
|
<PanelWrapper
|
||||||
panelType={widget.panelTypes}
|
widget={widget}
|
||||||
data={data}
|
queryResponse={queryResponse}
|
||||||
name={name}
|
setRequestData={setRequestData}
|
||||||
ref={lineChartRef}
|
setGraphVisibility={setGraphVisibility}
|
||||||
options={options}
|
graphVisibility={graphVisibility}
|
||||||
yAxisUnit={widget.yAxisUnit}
|
|
||||||
onClickHandler={onClickHandler}
|
onClickHandler={onClickHandler}
|
||||||
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
|
onDragSelect={onDragSelect}
|
||||||
query={widget.query}
|
|
||||||
thresholds={widget.thresholds}
|
|
||||||
selectedLogFields={widget.selectedLogFields}
|
|
||||||
dataSource={widget.query.builder?.queryData[0]?.dataSource}
|
|
||||||
selectedTracesFields={widget.selectedTracesFields}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,80 +4,47 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
|||||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
|
||||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import GetMinMax from 'lib/getMinMax';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
import getTimeString from 'lib/getTimeString';
|
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 isEmpty from 'lodash-es/isEmpty';
|
||||||
import _noop from 'lodash-es/noop';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
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 { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { getGraphType } from 'utils/getGraphType';
|
import { getGraphType } from 'utils/getGraphType';
|
||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
|
||||||
|
|
||||||
import EmptyWidget from '../EmptyWidget';
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||||
import { GridCardGraphProps } from './types';
|
import { GridCardGraphProps } from './types';
|
||||||
import { getLocalStorageGraphVisibilityState } from './utils';
|
|
||||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||||
|
|
||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
widget,
|
widget,
|
||||||
name,
|
|
||||||
onClickHandler = _noop,
|
|
||||||
headerMenuList = [MenuItemKeys.View],
|
headerMenuList = [MenuItemKeys.View],
|
||||||
isQueryEnabled,
|
isQueryEnabled,
|
||||||
threshold,
|
threshold,
|
||||||
variables,
|
variables,
|
||||||
fillSpans = false,
|
|
||||||
version,
|
version,
|
||||||
|
onClickHandler,
|
||||||
|
onDragSelect,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
const {
|
||||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
toScrollWidgetId,
|
||||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
setToScrollWidgetId,
|
||||||
const urlQuery = useUrlQuery();
|
variablesToGetUpdated,
|
||||||
const location = useLocation();
|
} = useDashboard();
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((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 handleBackNavigation = (): void => {
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const startTime = searchParams.get(QueryParams.startTime);
|
const startTime = searchParams.get(QueryParams.startTime);
|
||||||
@@ -131,15 +98,39 @@ function GridCardGraph({
|
|||||||
isVisible &&
|
isVisible &&
|
||||||
!isEmptyWidget &&
|
!isEmptyWidget &&
|
||||||
isQueryEnabled &&
|
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(
|
const queryResponse = useGetQueryRange(
|
||||||
{
|
{
|
||||||
selectedTime: widget?.timePreferance,
|
...requestData,
|
||||||
graphType: getGraphType(widget.panelTypes),
|
|
||||||
query: updatedQuery,
|
|
||||||
globalSelectedInterval,
|
|
||||||
variables: getDashboardVariables(variables),
|
variables: getDashboardVariables(variables),
|
||||||
|
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
|
||||||
|
globalSelectedInterval,
|
||||||
},
|
},
|
||||||
version || DEFAULT_ENTITY_VERSION,
|
version || DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
@@ -151,6 +142,7 @@ function GridCardGraph({
|
|||||||
widget?.query,
|
widget?.query,
|
||||||
widget?.panelTypes,
|
widget?.panelTypes,
|
||||||
widget.timePreferance,
|
widget.timePreferance,
|
||||||
|
requestData,
|
||||||
],
|
],
|
||||||
retry(failureCount, error): boolean {
|
retry(failureCount, error): boolean {
|
||||||
if (
|
if (
|
||||||
@@ -173,15 +165,6 @@ function GridCardGraph({
|
|||||||
|
|
||||||
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
|
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) {
|
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
|
||||||
const sortedSeriesData = getSortedSeriesData(
|
const sortedSeriesData = getSortedSeriesData(
|
||||||
queryResponse.data?.payload.data.result,
|
queryResponse.data?.payload.data.result,
|
||||||
@@ -189,89 +172,30 @@ function GridCardGraph({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
|
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
|
||||||
|
|
||||||
const menuList =
|
const menuList =
|
||||||
widget.panelTypes === PANEL_TYPES.TABLE ||
|
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.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
|
||||||
: headerMenuList;
|
: 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 (
|
return (
|
||||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||||
{isEmptyLayout ? (
|
{isEmptyLayout ? (
|
||||||
<EmptyWidget />
|
<EmptyWidget />
|
||||||
) : (
|
) : (
|
||||||
<WidgetGraphComponent
|
<WidgetGraphComponent
|
||||||
data={chartData}
|
|
||||||
options={options}
|
|
||||||
widget={widget}
|
widget={widget}
|
||||||
queryResponse={queryResponse}
|
queryResponse={queryResponse}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
isWarning={false}
|
isWarning={false}
|
||||||
name={name}
|
|
||||||
version={version}
|
version={version}
|
||||||
onDragSelect={onDragSelect}
|
|
||||||
threshold={threshold}
|
threshold={threshold}
|
||||||
headerMenuList={menuList}
|
headerMenuList={menuList}
|
||||||
onClickHandler={onClickHandler}
|
|
||||||
graphVisibiltyState={graphVisibility}
|
|
||||||
setGraphVisibility={setGraphVisibility}
|
|
||||||
isFetchingResponse={queryResponse.isFetching}
|
isFetchingResponse={queryResponse.isFetching}
|
||||||
|
setRequestData={setRequestData}
|
||||||
|
onClickHandler={onClickHandler}
|
||||||
|
onDragSelect={onDragSelect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
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 { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
|
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
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 { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
@@ -16,35 +16,32 @@ export interface GraphVisibilityLegendEntryProps {
|
|||||||
legendEntry: LegendEntryProps[];
|
legendEntry: LegendEntryProps[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WidgetGraphComponentProps extends UplotProps {
|
export interface WidgetGraphComponentProps {
|
||||||
widget: Widgets;
|
widget: Widgets;
|
||||||
queryResponse: UseQueryResult<
|
queryResponse: UseQueryResult<
|
||||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||||
|
Error
|
||||||
>;
|
>;
|
||||||
errorMessage: string | undefined;
|
errorMessage: string | undefined;
|
||||||
name: string;
|
|
||||||
version?: string;
|
version?: string;
|
||||||
onDragSelect: (start: number, end: number) => void;
|
|
||||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
|
||||||
threshold?: ReactNode;
|
threshold?: ReactNode;
|
||||||
headerMenuList: MenuItemKeys[];
|
headerMenuList: MenuItemKeys[];
|
||||||
isWarning: boolean;
|
isWarning: boolean;
|
||||||
graphVisibiltyState: boolean[];
|
|
||||||
setGraphVisibility: Dispatch<SetStateAction<boolean[]>>;
|
|
||||||
isFetchingResponse: boolean;
|
isFetchingResponse: boolean;
|
||||||
|
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||||
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
|
onDragSelect: (start: number, end: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridCardGraphProps {
|
export interface GridCardGraphProps {
|
||||||
widget: Widgets;
|
widget: Widgets;
|
||||||
name: string;
|
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
|
||||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
|
||||||
threshold?: ReactNode;
|
threshold?: ReactNode;
|
||||||
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
||||||
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
isQueryEnabled: boolean;
|
isQueryEnabled: boolean;
|
||||||
variables?: Dashboard['data']['variables'];
|
variables?: Dashboard['data']['variables'];
|
||||||
fillSpans?: boolean;
|
|
||||||
version?: string;
|
version?: string;
|
||||||
|
onDragSelect: (start: number, end: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
.react-grid-layout {
|
.react-grid-layout {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
|
.widget-graph-container {
|
||||||
|
&.graph {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +29,7 @@
|
|||||||
.ant-modal-header {
|
.ant-modal-header {
|
||||||
background-color: var(--bg-ink-400);
|
background-color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import './GridCardLayout.styles.scss';
|
import './GridCardLayout.styles.scss';
|
||||||
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Tooltip } from 'antd';
|
import { Flex, Tooltip } from 'antd';
|
||||||
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import history from 'lib/history';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { FullscreenIcon } from 'lucide-react';
|
import { FullscreenIcon } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
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 { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { AppState } from 'store/reducers';
|
||||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
@@ -45,6 +51,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
const { data } = selectedDashboard || {};
|
const { data } = selectedDashboard || {};
|
||||||
const handle = useFullScreenHandle();
|
const handle = useFullScreenHandle();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { widgets, variables } = data || {};
|
const { widgets, variables } = data || {};
|
||||||
|
|
||||||
@@ -61,6 +69,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
const updateDashboardMutation = useUpdateDashboard();
|
const updateDashboardMutation = useUpdateDashboard();
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
|
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
|
||||||
|
|
||||||
@@ -126,6 +135,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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
dashboardLayout &&
|
dashboardLayout &&
|
||||||
@@ -144,28 +170,47 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonContainer>
|
<Flex justify="flex-end" gap={8} align="center">
|
||||||
<Tooltip title="Open in Full Screen">
|
<FacingIssueBtn
|
||||||
<Button
|
attributes={{
|
||||||
className="periscope-btn"
|
uuid: selectedDashboard?.uuid,
|
||||||
loading={updateDashboardMutation.isLoading}
|
title: data?.title,
|
||||||
onClick={handle.enter}
|
screen: 'Dashboard Details',
|
||||||
icon={<FullscreenIcon size={16} />}
|
}}
|
||||||
disabled={updateDashboardMutation.isLoading}
|
eventName="Dashboard: Facing Issues in dashboard"
|
||||||
/>
|
buttonText="Facing Issues in dashboard"
|
||||||
</Tooltip>
|
message={`Hi Team,
|
||||||
|
|
||||||
{!isDashboardLocked && addPanelPermission && (
|
I am facing issues configuring dashboard in SigNoz. Here are my dashboard details
|
||||||
<Button
|
|
||||||
className="periscope-btn"
|
Name: ${data?.title || ''}
|
||||||
onClick={onAddPanelHandler}
|
Dashboard Id: ${selectedDashboard?.uuid || ''}
|
||||||
icon={<PlusOutlined />}
|
|
||||||
data-testid="add-panel"
|
Thanks`}
|
||||||
>
|
/>
|
||||||
{t('dashboard:add_panel')}
|
<ButtonContainer>
|
||||||
</Button>
|
<Tooltip title="Open in Full Screen">
|
||||||
)}
|
<Button
|
||||||
</ButtonContainer>
|
className="periscope-btn"
|
||||||
|
loading={updateDashboardMutation.isLoading}
|
||||||
|
onClick={handle.enter}
|
||||||
|
icon={<FullscreenIcon size={16} />}
|
||||||
|
disabled={updateDashboardMutation.isLoading}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{!isDashboardLocked && addPanelPermission && (
|
||||||
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
|
onClick={onAddPanelHandler}
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
data-testid="add-panel"
|
||||||
|
>
|
||||||
|
{t('dashboard:add_panel')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonContainer>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<FullScreen handle={handle} className="fullscreen-grid-container">
|
<FullScreen handle={handle} className="fullscreen-grid-container">
|
||||||
<ReactGridLayout
|
<ReactGridLayout
|
||||||
@@ -200,11 +245,10 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
>
|
>
|
||||||
<GridCard
|
<GridCard
|
||||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
||||||
name={currentWidget?.id || ''}
|
|
||||||
headerMenuList={widgetActions}
|
headerMenuList={widgetActions}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
fillSpans={currentWidget?.fillSpans}
|
|
||||||
version={selectedDashboard?.data?.version}
|
version={selectedDashboard?.data?.version}
|
||||||
|
onDragSelect={onDragSelect}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
|
||||||
import { getComponentForPanelType } from 'constants/panelTypes';
|
import { getComponentForPanelType } from 'constants/panelTypes';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||||
import { FC, forwardRef, memo, useMemo } from 'react';
|
import { FC, forwardRef, memo, useMemo } from 'react';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
|
||||||
|
|
||||||
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
|
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
|
||||||
|
|
||||||
@@ -21,10 +19,7 @@ const GridPanelSwitch = forwardRef<
|
|||||||
query,
|
query,
|
||||||
options,
|
options,
|
||||||
thresholds,
|
thresholds,
|
||||||
selectedLogFields,
|
|
||||||
selectedTracesFields,
|
|
||||||
dataSource,
|
dataSource,
|
||||||
selectedTime,
|
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
): JSX.Element | null => {
|
): JSX.Element | null => {
|
||||||
@@ -46,20 +41,8 @@ const GridPanelSwitch = forwardRef<
|
|||||||
query,
|
query,
|
||||||
thresholds,
|
thresholds,
|
||||||
},
|
},
|
||||||
[PANEL_TYPES.LIST]:
|
[PANEL_TYPES.LIST]: null,
|
||||||
dataSource === DataSource.LOGS
|
[PANEL_TYPES.PIE]: null,
|
||||||
? {
|
|
||||||
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.TRACE]: null,
|
[PANEL_TYPES.TRACE]: null,
|
||||||
[PANEL_TYPES.BAR]: {
|
[PANEL_TYPES.BAR]: {
|
||||||
data,
|
data,
|
||||||
@@ -70,19 +53,7 @@ const GridPanelSwitch = forwardRef<
|
|||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [
|
}, [data, options, ref, yAxisUnit, thresholds, panelData, query]);
|
||||||
data,
|
|
||||||
options,
|
|
||||||
ref,
|
|
||||||
yAxisUnit,
|
|
||||||
thresholds,
|
|
||||||
panelData,
|
|
||||||
query,
|
|
||||||
dataSource,
|
|
||||||
selectedLogFields,
|
|
||||||
selectedTime,
|
|
||||||
selectedTracesFields,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const Component = getComponentForPanelType(panelType, dataSource) as FC<
|
const Component = getComponentForPanelType(panelType, dataSource) as FC<
|
||||||
PropsTypePropsMap[typeof panelType]
|
PropsTypePropsMap[typeof panelType]
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
|
|||||||
import { UplotProps } from 'components/Uplot/Uplot';
|
import { UplotProps } from 'components/Uplot/Uplot';
|
||||||
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
||||||
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
||||||
import { LogsPanelComponentProps } from 'container/LogsPanelTable/LogsPanelComponent';
|
|
||||||
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { TracesTableComponentProps } from 'container/TracesTableComponent/TracesTableComponent';
|
|
||||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { ForwardedRef } from 'react';
|
import { ForwardedRef } from 'react';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
@@ -40,7 +38,8 @@ export type PropsTypePropsMap = {
|
|||||||
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
||||||
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
||||||
[PANEL_TYPES.TRACE]: null;
|
[PANEL_TYPES.TRACE]: null;
|
||||||
[PANEL_TYPES.LIST]: LogsPanelComponentProps | TracesTableComponentProps;
|
[PANEL_TYPES.PIE]: null;
|
||||||
|
[PANEL_TYPES.LIST]: null;
|
||||||
[PANEL_TYPES.BAR]: UplotProps & {
|
[PANEL_TYPES.BAR]: UplotProps & {
|
||||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -358,6 +358,18 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
pagination={{
|
pagination={{
|
||||||
defaultCurrent: Number(paginationParam) || 1,
|
defaultCurrent: Number(paginationParam) || 1,
|
||||||
}}
|
}}
|
||||||
|
facingIssueBtn={{
|
||||||
|
attributes: {
|
||||||
|
screen: 'Alert list page',
|
||||||
|
},
|
||||||
|
eventName: 'Alert: Facing Issues in alert',
|
||||||
|
buttonText: 'Facing Issues in alert',
|
||||||
|
message: `Hi Team,
|
||||||
|
|
||||||
|
I am facing issues with alerts.
|
||||||
|
|
||||||
|
Thanks`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -385,6 +385,18 @@ function DashboardsList(): JSX.Element {
|
|||||||
dataSource={data}
|
dataSource={data}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
showSorterTooltip
|
showSorterTooltip
|
||||||
|
facingIssueBtn={{
|
||||||
|
attributes: {
|
||||||
|
screen: 'Dashboard list page',
|
||||||
|
},
|
||||||
|
eventName: 'Dashboard: Facing Issues in dashboard',
|
||||||
|
buttonText: 'Facing Issues in dashboard',
|
||||||
|
message: `Hi Team,
|
||||||
|
|
||||||
|
I am facing issues with dashboards.
|
||||||
|
|
||||||
|
Thanks`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -608,6 +608,7 @@ function LogsExplorerViews({
|
|||||||
className="periscope-btn"
|
className="periscope-btn"
|
||||||
onClick={handleToggleShowFormatOptions}
|
onClick={handleToggleShowFormatOptions}
|
||||||
icon={<Sliders size={14} />}
|
icon={<Sliders size={14} />}
|
||||||
|
data-testid="periscope-btn"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showFormatMenuItems && (
|
{showFormatMenuItems && (
|
||||||
|
|||||||
@@ -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 bit’s enough ⎯ we’re 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
51
frontend/src/container/LogsExplorerViews/tests/mock.ts
Normal file
51
frontend/src/container/LogsExplorerViews/tests/mock.ts
Normal 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: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,82 +4,53 @@ import { Table } from 'antd';
|
|||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
|
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
|
||||||
import Controls from 'container/Controls';
|
import Controls from 'container/Controls';
|
||||||
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
|
||||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||||
import { tableStyles } from 'container/TracesExplorer/ListView/styles';
|
import { tableStyles } from 'container/TracesExplorer/ListView/styles';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
|
||||||
import { Pagination } from 'hooks/queryPagination';
|
import { Pagination } from 'hooks/queryPagination';
|
||||||
import { useLogsData } from 'hooks/useLogsData';
|
import { useLogsData } from 'hooks/useLogsData';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
|
||||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import {
|
import {
|
||||||
|
Dispatch,
|
||||||
HTMLAttributes,
|
HTMLAttributes,
|
||||||
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { AppState } from 'store/reducers';
|
import { SuccessResponse } from 'types/api';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import { getLogPanelColumnsList } from './utils';
|
import { getLogPanelColumnsList, getNextOrPreviousItems } from './utils';
|
||||||
|
|
||||||
function LogsPanelComponent({
|
function LogsPanelComponent({
|
||||||
selectedLogsFields,
|
widget,
|
||||||
query,
|
setRequestData,
|
||||||
selectedTime,
|
queryResponse,
|
||||||
}: LogsPanelComponentProps): JSX.Element {
|
}: LogsPanelComponentProps): JSX.Element {
|
||||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
|
||||||
AppState,
|
|
||||||
GlobalReducer
|
|
||||||
>((state) => state.globalTime);
|
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<Pagination>({
|
const [pagination, setPagination] = useState<Pagination>({
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: query.builder.queryData[0].limit || 0,
|
limit: widget.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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRequestData({
|
setRequestData((prev) => ({
|
||||||
...requestData,
|
...prev,
|
||||||
globalSelectedInterval: globalSelectedTime,
|
|
||||||
tableParams: {
|
tableParams: {
|
||||||
pagination,
|
pagination,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [pagination, setRequestData]);
|
||||||
}, [pagination]);
|
|
||||||
|
|
||||||
const [pageSize, setPageSize] = useState<number>(10);
|
const [pageSize, setPageSize] = useState<number>(10);
|
||||||
const { selectedDashboard } = useDashboard();
|
|
||||||
|
|
||||||
const handleChangePageSize = (value: number): void => {
|
const handleChangePageSize = (value: number): void => {
|
||||||
setPagination({
|
setPagination({
|
||||||
@@ -88,53 +59,35 @@ function LogsPanelComponent({
|
|||||||
offset: value,
|
offset: value,
|
||||||
});
|
});
|
||||||
setPageSize(value);
|
setPageSize(value);
|
||||||
const newQueryData = { ...requestData.query };
|
setRequestData((prev) => {
|
||||||
newQueryData.builder.queryData[0].pageSize = value;
|
const newQueryData = { ...prev.query };
|
||||||
const newRequestData = {
|
newQueryData.builder.queryData[0].pageSize = value;
|
||||||
...requestData,
|
return {
|
||||||
query: newQueryData,
|
...prev,
|
||||||
tableParams: {
|
query: newQueryData,
|
||||||
pagination,
|
tableParams: {
|
||||||
},
|
pagination: {
|
||||||
};
|
limit: 0,
|
||||||
setRequestData(newRequestData);
|
offset: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, isFetching, isError } = useGetQueryRange(
|
const columns = getLogPanelColumnsList(widget.selectedLogFields);
|
||||||
{
|
|
||||||
...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 dataLength =
|
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 totalCount = useMemo(() => dataLength || 0, [dataLength]);
|
||||||
|
|
||||||
const [firstLog, setFirstLog] = useState<ILog>();
|
const [firstLog, setFirstLog] = useState<ILog>();
|
||||||
const [lastLog, setLastLog] = useState<ILog>();
|
const [lastLog, setLastLog] = useState<ILog>();
|
||||||
|
|
||||||
const { logs } = useLogsData({
|
const { logs } = useLogsData({
|
||||||
result: data?.payload.data.newResult.data.result,
|
result: queryResponse.data?.payload.data.newResult.data.result,
|
||||||
panelType: PANEL_TYPES.LIST,
|
panelType: PANEL_TYPES.LIST,
|
||||||
stagedQuery: query,
|
stagedQuery: widget.query,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -167,92 +120,86 @@ function LogsPanelComponent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isOrderByTimeStamp =
|
const isOrderByTimeStamp =
|
||||||
query.builder.queryData[0].orderBy.length > 0 &&
|
widget.query.builder.queryData[0].orderBy.length > 0 &&
|
||||||
query.builder.queryData[0].orderBy[0].columnName === 'timestamp';
|
widget.query.builder.queryData[0].orderBy[0].columnName === 'timestamp';
|
||||||
|
|
||||||
const handlePreviousPagination = (): void => {
|
const handlePreviousPagination = (): void => {
|
||||||
if (isOrderByTimeStamp) {
|
if (isOrderByTimeStamp) {
|
||||||
setRequestData({
|
setRequestData((prev) => ({
|
||||||
...requestData,
|
...prev,
|
||||||
query: {
|
query: {
|
||||||
...requestData.query,
|
...prev.query,
|
||||||
builder: {
|
builder: {
|
||||||
...requestData.query.builder,
|
...prev.query.builder,
|
||||||
queryData: [
|
queryData: [
|
||||||
{
|
{
|
||||||
...requestData.query.builder.queryData[0],
|
...prev.query.builder.queryData[0],
|
||||||
filters: {
|
filters: {
|
||||||
...requestData.query.builder.queryData[0].filters,
|
...prev.query.builder.queryData[0].filters,
|
||||||
items: [
|
items: [
|
||||||
{
|
...getNextOrPreviousItems(
|
||||||
id: uuid(),
|
prev.query.builder.queryData[0].filters.items,
|
||||||
key: {
|
'PREV',
|
||||||
key: 'id',
|
firstLog,
|
||||||
type: '',
|
),
|
||||||
dataType: DataTypes.String,
|
|
||||||
isColumn: true,
|
|
||||||
},
|
|
||||||
op: OPERATORS['>'],
|
|
||||||
value: firstLog?.id || '',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
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 => {
|
const handleNextPagination = (): void => {
|
||||||
if (isOrderByTimeStamp) {
|
if (isOrderByTimeStamp) {
|
||||||
setRequestData({
|
setRequestData((prev) => ({
|
||||||
...requestData,
|
...prev,
|
||||||
query: {
|
query: {
|
||||||
...requestData.query,
|
...prev.query,
|
||||||
builder: {
|
builder: {
|
||||||
...requestData.query.builder,
|
...prev.query.builder,
|
||||||
queryData: [
|
queryData: [
|
||||||
{
|
{
|
||||||
...requestData.query.builder.queryData[0],
|
...prev.query.builder.queryData[0],
|
||||||
filters: {
|
filters: {
|
||||||
...requestData.query.builder.queryData[0].filters,
|
...prev.query.builder.queryData[0].filters,
|
||||||
items: [
|
items: [
|
||||||
{
|
...getNextOrPreviousItems(
|
||||||
id: uuid(),
|
prev.query.builder.queryData[0].filters.items,
|
||||||
key: {
|
'NEXT',
|
||||||
key: 'id',
|
lastLog,
|
||||||
type: '',
|
),
|
||||||
dataType: DataTypes.String,
|
|
||||||
isColumn: true,
|
|
||||||
},
|
|
||||||
op: OPERATORS['<'],
|
|
||||||
value: lastLog?.id || '',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
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>;
|
return <div>{SOMETHING_WENT_WRONG}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,19 +212,19 @@ function LogsPanelComponent({
|
|||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
scroll={{ x: `calc(50vw - 10px)` }}
|
scroll={{ x: `calc(50vw - 10px)` }}
|
||||||
sticky
|
sticky
|
||||||
loading={isFetching}
|
loading={queryResponse.isFetching}
|
||||||
style={tableStyles}
|
style={tableStyles}
|
||||||
dataSource={flattenLogData}
|
dataSource={flattenLogData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onRow={handleRow}
|
onRow={handleRow}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!query.builder.queryData[0].limit && (
|
{!widget.query.builder.queryData[0].limit && (
|
||||||
<div className="controller">
|
<div className="controller">
|
||||||
<Controls
|
<Controls
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
perPageOptions={PER_PAGE_OPTIONS}
|
perPageOptions={PER_PAGE_OPTIONS}
|
||||||
isLoading={isFetching}
|
isLoading={queryResponse.isFetching}
|
||||||
offset={pagination.offset}
|
offset={pagination.offset}
|
||||||
countPerPage={pageSize}
|
countPerPage={pageSize}
|
||||||
handleNavigatePrevious={handlePreviousPagination}
|
handleNavigatePrevious={handlePreviousPagination}
|
||||||
@@ -301,13 +248,12 @@ function LogsPanelComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type LogsPanelComponentProps = {
|
export type LogsPanelComponentProps = {
|
||||||
selectedLogsFields: Widgets['selectedLogFields'];
|
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||||
query: Query;
|
queryResponse: UseQueryResult<
|
||||||
selectedTime?: timePreferance;
|
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||||
};
|
Error
|
||||||
|
>;
|
||||||
LogsPanelComponent.defaultProps = {
|
widget: Widgets;
|
||||||
selectedTime: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LogsPanelComponent;
|
export default LogsPanelComponent;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import { Typography } from 'antd/lib';
|
import { Typography } from 'antd/lib';
|
||||||
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
// import Typography from 'antd/es/typography/Typography';
|
// import Typography from 'antd/es/typography/Typography';
|
||||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { IField } from 'types/api/logs/fields';
|
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 = (
|
export const getLogPanelColumnsList = (
|
||||||
selectedLogFields: Widgets['selectedLogFields'],
|
selectedLogFields: Widgets['selectedLogFields'],
|
||||||
@@ -36,3 +41,49 @@ export const getLogPanelColumnsList = (
|
|||||||
|
|
||||||
return [...initialColumns, ...columns];
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const getWidgetQueryBuilder = ({
|
|||||||
title = '',
|
title = '',
|
||||||
panelTypes,
|
panelTypes,
|
||||||
yAxisUnit = '',
|
yAxisUnit = '',
|
||||||
|
fillSpans = false,
|
||||||
id,
|
id,
|
||||||
}: GetWidgetQueryBuilderProps): Widgets => ({
|
}: GetWidgetQueryBuilderProps): Widgets => ({
|
||||||
description: '',
|
description: '',
|
||||||
@@ -24,4 +25,5 @@ export const getWidgetQueryBuilder = ({
|
|||||||
softMin: null,
|
softMin: null,
|
||||||
selectedLogFields: [],
|
selectedLogFields: [],
|
||||||
selectedTracesFields: [],
|
selectedTracesFields: [],
|
||||||
|
fillSpans,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ function DBCall(): JSX.Element {
|
|||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'reqps',
|
yAxisUnit: 'reqps',
|
||||||
id: SERVICE_CHART_ID.dbCallsRPS,
|
id: SERVICE_CHART_ID.dbCallsRPS,
|
||||||
|
fillSpans: false,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@@ -89,7 +90,8 @@ function DBCall(): JSX.Element {
|
|||||||
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
|
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'ms',
|
yAxisUnit: 'ms',
|
||||||
id: SERVICE_CHART_ID.dbCallsAvgDuration,
|
id: GraphTitle.DATABASE_CALLS_AVG_DURATION,
|
||||||
|
fillSpans: true,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@@ -112,8 +114,6 @@ function DBCall(): JSX.Element {
|
|||||||
<Card data-testid="database_call_rps">
|
<Card data-testid="database_call_rps">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
fillSpans={false}
|
|
||||||
name="database_call_rps"
|
|
||||||
widget={databaseCallsRPSWidget}
|
widget={databaseCallsRPSWidget}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
@@ -147,8 +147,6 @@ function DBCall(): JSX.Element {
|
|||||||
<Card data-testid="database_call_avg_duration">
|
<Card data-testid="database_call_avg_duration">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
fillSpans
|
|
||||||
name="database_call_avg_duration"
|
|
||||||
widget={databaseCallsAverageDurationWidget}
|
widget={databaseCallsAverageDurationWidget}
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { v4 as uuid } from 'uuid';
|
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 { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import { Card, GraphContainer, Row } from '../styles';
|
import { Card, GraphContainer, Row } from '../styles';
|
||||||
import { Button } from './styles';
|
import { Button } from './styles';
|
||||||
@@ -60,7 +60,7 @@ function External(): JSX.Element {
|
|||||||
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
|
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: '%',
|
yAxisUnit: '%',
|
||||||
id: SERVICE_CHART_ID.externalCallErrorPercentage,
|
id: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@@ -86,7 +86,8 @@ function External(): JSX.Element {
|
|||||||
title: GraphTitle.EXTERNAL_CALL_DURATION,
|
title: GraphTitle.EXTERNAL_CALL_DURATION,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'ms',
|
yAxisUnit: 'ms',
|
||||||
id: SERVICE_CHART_ID.externalCallDuration,
|
id: GraphTitle.EXTERNAL_CALL_DURATION,
|
||||||
|
fillSpans: true,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@@ -108,7 +109,8 @@ function External(): JSX.Element {
|
|||||||
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
|
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'reqps',
|
yAxisUnit: 'reqps',
|
||||||
id: SERVICE_CHART_ID.externalCallRPSByAddress,
|
id: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
|
||||||
|
fillSpans: true,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@@ -130,7 +132,8 @@ function External(): JSX.Element {
|
|||||||
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
|
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||||
yAxisUnit: 'ms',
|
yAxisUnit: 'ms',
|
||||||
id: SERVICE_CHART_ID.externalCallDurationByAddress,
|
id: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
|
||||||
|
fillSpans: true,
|
||||||
}),
|
}),
|
||||||
[servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
@@ -155,9 +158,7 @@ function External(): JSX.Element {
|
|||||||
<Card data-testid="external_call_error_percentage">
|
<Card data-testid="external_call_error_percentage">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
fillSpans={false}
|
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
name="external_call_error_percentage"
|
|
||||||
widget={externalCallErrorWidget}
|
widget={externalCallErrorWidget}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
@@ -192,8 +193,6 @@ function External(): JSX.Element {
|
|||||||
<Card data-testid="external_call_duration">
|
<Card data-testid="external_call_duration">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
fillSpans
|
|
||||||
name="external_call_duration"
|
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
widget={externalCallDurationWidget}
|
widget={externalCallDurationWidget}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
@@ -230,8 +229,6 @@ function External(): JSX.Element {
|
|||||||
<Card data-testid="external_call_rps_by_address">
|
<Card data-testid="external_call_rps_by_address">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
fillSpans
|
|
||||||
name="external_call_rps_by_address"
|
|
||||||
widget={externalCallRPSWidget}
|
widget={externalCallRPSWidget}
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
|
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
|
||||||
@@ -267,10 +264,8 @@ function External(): JSX.Element {
|
|||||||
<Card data-testid="external_call_duration_by_address">
|
<Card data-testid="external_call_duration_by_address">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
name="external_call_duration_by_address"
|
|
||||||
widget={externalCallDurationAddressWidget}
|
widget={externalCallDurationAddressWidget}
|
||||||
headerMenuList={MENU_ITEMS}
|
headerMenuList={MENU_ITEMS}
|
||||||
fillSpans
|
|
||||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
xValue,
|
xValue,
|
||||||
|
|||||||
@@ -19,13 +19,10 @@ import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
|||||||
import { defaultTo } from 'lodash-es';
|
import { defaultTo } from 'lodash-es';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
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 { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
|
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
|
||||||
@@ -49,9 +46,6 @@ import {
|
|||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
function Application(): JSX.Element {
|
function Application(): JSX.Element {
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
|
||||||
(state) => state.globalTime,
|
|
||||||
);
|
|
||||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||||
const servicename = decodeURIComponent(encodedServiceName);
|
const servicename = decodeURIComponent(encodedServiceName);
|
||||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||||
@@ -59,10 +53,6 @@ function Application(): JSX.Element {
|
|||||||
const { queries } = useResourceAttribute();
|
const { queries } = useResourceAttribute();
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
const selectedTags = useMemo(
|
|
||||||
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
|
|
||||||
[queries],
|
|
||||||
);
|
|
||||||
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
|
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
|
||||||
?.active;
|
?.active;
|
||||||
|
|
||||||
@@ -94,7 +84,7 @@ function Application(): JSX.Element {
|
|||||||
isLoading: topLevelOperationsIsLoading,
|
isLoading: topLevelOperationsIsLoading,
|
||||||
isError: topLevelOperationsIsError,
|
isError: topLevelOperationsIsError,
|
||||||
} = useQuery<ServiceDataProps>({
|
} = useQuery<ServiceDataProps>({
|
||||||
queryKey: [servicename, minTime, maxTime, selectedTags],
|
queryKey: [servicename],
|
||||||
queryFn: getTopLevelOperations,
|
queryFn: getTopLevelOperations,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,49 +106,41 @@ function Application(): JSX.Element {
|
|||||||
[servicename, topLevelOperations],
|
[servicename, topLevelOperations],
|
||||||
);
|
);
|
||||||
|
|
||||||
const operationPerSecWidget = useMemo(
|
const operationPerSecWidget = getWidgetQueryBuilder({
|
||||||
() =>
|
query: {
|
||||||
getWidgetQueryBuilder({
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
query: {
|
promql: [],
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
builder: operationPerSec({
|
||||||
promql: [],
|
servicename,
|
||||||
builder: operationPerSec({
|
tagFilterItems,
|
||||||
servicename,
|
topLevelOperations: topLevelOperationsRoute,
|
||||||
tagFilterItems,
|
|
||||||
topLevelOperations: topLevelOperationsRoute,
|
|
||||||
}),
|
|
||||||
clickhouse_sql: [],
|
|
||||||
id: uuid(),
|
|
||||||
},
|
|
||||||
title: GraphTitle.RATE_PER_OPS,
|
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
|
||||||
yAxisUnit: 'ops',
|
|
||||||
id: SERVICE_CHART_ID.rps,
|
|
||||||
}),
|
}),
|
||||||
[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(
|
const errorPercentageWidget = getWidgetQueryBuilder({
|
||||||
() =>
|
query: {
|
||||||
getWidgetQueryBuilder({
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
query: {
|
promql: [],
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
builder: errorPercentage({
|
||||||
promql: [],
|
servicename,
|
||||||
builder: errorPercentage({
|
tagFilterItems,
|
||||||
servicename,
|
topLevelOperations: topLevelOperationsRoute,
|
||||||
tagFilterItems,
|
|
||||||
topLevelOperations: topLevelOperationsRoute,
|
|
||||||
}),
|
|
||||||
clickhouse_sql: [],
|
|
||||||
id: uuid(),
|
|
||||||
},
|
|
||||||
title: GraphTitle.ERROR_PERCENTAGE,
|
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
|
||||||
yAxisUnit: '%',
|
|
||||||
id: SERVICE_CHART_ID.errorPercentage,
|
|
||||||
}),
|
}),
|
||||||
[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(
|
const onDragSelect = useCallback(
|
||||||
(start: number, end: number) => {
|
(start: number, end: number) => {
|
||||||
|
|||||||
@@ -89,8 +89,6 @@ function ApDexMetrics({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Graph
|
<Graph
|
||||||
name="apdex"
|
|
||||||
fillSpans={false}
|
|
||||||
widget={apDexMetricsWidget}
|
widget={apDexMetricsWidget}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
onClickHandler={handleGraphClick('ApDex')}
|
onClickHandler={handleGraphClick('ApDex')}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ function ApDexTraces({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Graph
|
<Graph
|
||||||
name="apdex"
|
|
||||||
widget={apDexTracesWidget}
|
widget={apDexTracesWidget}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
onClickHandler={handleGraphClick('ApDex')}
|
onClickHandler={handleGraphClick('ApDex')}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Skeleton } from 'antd';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
@@ -46,28 +47,24 @@ function ServiceOverview({
|
|||||||
[isSpanMetricEnable, queries],
|
[isSpanMetricEnable, queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const latencyWidget = useMemo(
|
const latencyWidget = getWidgetQueryBuilder({
|
||||||
() =>
|
query: {
|
||||||
getWidgetQueryBuilder({
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
query: {
|
promql: [],
|
||||||
queryType: EQueryType.QUERY_BUILDER,
|
builder: latency({
|
||||||
promql: [],
|
servicename,
|
||||||
builder: latency({
|
tagFilterItems,
|
||||||
servicename,
|
isSpanMetricEnable,
|
||||||
tagFilterItems,
|
topLevelOperationsRoute,
|
||||||
isSpanMetricEnable,
|
|
||||||
topLevelOperationsRoute,
|
|
||||||
}),
|
|
||||||
clickhouse_sql: [],
|
|
||||||
id: uuid(),
|
|
||||||
},
|
|
||||||
title: GraphTitle.LATENCY,
|
|
||||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
|
||||||
yAxisUnit: 'ns',
|
|
||||||
id: SERVICE_CHART_ID.latency,
|
|
||||||
}),
|
}),
|
||||||
[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 =
|
const isQueryEnabled =
|
||||||
!topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0;
|
!topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0;
|
||||||
@@ -88,15 +85,23 @@ function ServiceOverview({
|
|||||||
</Button>
|
</Button>
|
||||||
<Card data-testid="service_latency">
|
<Card data-testid="service_latency">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
{topLevelOperationsIsLoading && (
|
||||||
name="service_latency"
|
<Skeleton
|
||||||
onDragSelect={onDragSelect}
|
style={{
|
||||||
widget={latencyWidget}
|
height: '100%',
|
||||||
onClickHandler={handleGraphClick('Service')}
|
padding: '16px',
|
||||||
isQueryEnabled={isQueryEnabled}
|
}}
|
||||||
fillSpans={false}
|
/>
|
||||||
version={ENTITY_VERSION_V4}
|
)}
|
||||||
/>
|
{!topLevelOperationsIsLoading && (
|
||||||
|
<Graph
|
||||||
|
onDragSelect={onDragSelect}
|
||||||
|
widget={latencyWidget}
|
||||||
|
onClickHandler={handleGraphClick('Service')}
|
||||||
|
isQueryEnabled={isQueryEnabled}
|
||||||
|
version={ENTITY_VERSION_V4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Typography } from 'antd';
|
import { Skeleton, Typography } from 'antd';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
@@ -27,15 +27,23 @@ function TopLevelOperation({
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
{topLevelOperationsIsLoading && (
|
||||||
fillSpans={false}
|
<Skeleton
|
||||||
name={name}
|
style={{
|
||||||
widget={widget}
|
height: '100%',
|
||||||
onClickHandler={handleGraphClick(opName)}
|
padding: '16px',
|
||||||
onDragSelect={onDragSelect}
|
}}
|
||||||
isQueryEnabled={!topLevelOperationsIsLoading}
|
/>
|
||||||
version={ENTITY_VERSION_V4}
|
)}
|
||||||
/>
|
{!topLevelOperationsIsLoading && (
|
||||||
|
<Graph
|
||||||
|
widget={widget}
|
||||||
|
onClickHandler={handleGraphClick(opName)}
|
||||||
|
onDragSelect={onDragSelect}
|
||||||
|
isQueryEnabled={!topLevelOperationsIsLoading}
|
||||||
|
version={ENTITY_VERSION_V4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
|
||||||
import { DownloadOptions } from 'container/Download/Download.types';
|
import { DownloadOptions } from 'container/Download/Download.types';
|
||||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ export enum FORMULA {
|
|||||||
ERROR_PERCENTAGE = 'A*100/B',
|
ERROR_PERCENTAGE = 'A*100/B',
|
||||||
DATABASE_CALLS_AVG_DURATION = 'A/B',
|
DATABASE_CALLS_AVG_DURATION = 'A/B',
|
||||||
APDEX_TRACES = '((B + C)/2)/A',
|
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',
|
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const Card = styled(CardComponent)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
height: calc(100% - 40px);
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -40,7 +40,7 @@ export const ColErrorContainer = styled(ColComponent)`
|
|||||||
|
|
||||||
export const GraphContainer = styled.div`
|
export const GraphContainer = styled.div`
|
||||||
min-height: calc(40vh - 40px);
|
min-height: calc(40vh - 40px);
|
||||||
height: calc(100% - 40px);
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GraphTitle = styled(Typography)`
|
export const GraphTitle = styled(Typography)`
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface GetWidgetQueryBuilderProps {
|
|||||||
panelTypes: Widgets['panelTypes'];
|
panelTypes: Widgets['panelTypes'];
|
||||||
yAxisUnit?: Widgets['yAxisUnit'];
|
yAxisUnit?: Widgets['yAxisUnit'];
|
||||||
id?: Widgets['id'];
|
id?: Widgets['id'];
|
||||||
|
fillSpans?: Widgets['fillSpans'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigateToTraceProps {
|
export interface NavigateToTraceProps {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export const getNearestHighestBucketValue = (
|
|||||||
value: number,
|
value: number,
|
||||||
buckets: number[],
|
buckets: number[],
|
||||||
): string => {
|
): string => {
|
||||||
|
// sort the buckets
|
||||||
|
buckets.sort((a, b) => a - b);
|
||||||
const nearestBucket = buckets.find((bucket) => bucket >= value);
|
const nearestBucket = buckets.find((bucket) => bucket >= value);
|
||||||
return nearestBucket?.toString() || '+Inf';
|
return nearestBucket?.toString() || '+Inf';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const PANEL_TYPES_INITIAL_QUERY = {
|
|||||||
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
|
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
|
||||||
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
||||||
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,146 +1,59 @@
|
|||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import {
|
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
|
||||||
listViewInitialLogQuery,
|
|
||||||
listViewInitialTraceQuery,
|
|
||||||
PANEL_TYPES_INITIAL_QUERY,
|
|
||||||
} from './constants';
|
|
||||||
import menuItems from './menuItems';
|
import menuItems from './menuItems';
|
||||||
import { Card, Container, Text } from './styles';
|
import { Card, Container, Text } from './styles';
|
||||||
|
|
||||||
function DashboardGraphSlider(): JSX.Element {
|
function DashboardGraphSlider(): JSX.Element {
|
||||||
const {
|
const { handleToggleDashboardSlider } = useDashboard();
|
||||||
handleToggleDashboardSlider,
|
|
||||||
layouts,
|
|
||||||
selectedDashboard,
|
|
||||||
} = useDashboard();
|
|
||||||
|
|
||||||
const { data } = selectedDashboard || {};
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const updateDashboardMutation = useUpdateDashboard();
|
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
|
handleToggleDashboardSlider(false);
|
||||||
updateDashboardMutation.mutateAsync(
|
const queryParamsLog = {
|
||||||
{
|
graphType: name,
|
||||||
uuid: selectedDashboard?.uuid || '',
|
widgetId: id,
|
||||||
data: {
|
[QueryParams.compositeQuery]: JSON.stringify({
|
||||||
title: data?.title || '',
|
...PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
variables: data?.variables || {},
|
builder: {
|
||||||
description: data?.description || '',
|
...PANEL_TYPES_INITIAL_QUERY[name].builder,
|
||||||
name: data?.name || '',
|
queryData: [
|
||||||
tags: data?.tags || [],
|
|
||||||
version: data?.version || 'v3',
|
|
||||||
layout: [
|
|
||||||
{
|
{
|
||||||
i: id,
|
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
|
||||||
w: 6,
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
x: 0,
|
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||||
h: 3,
|
offset: 0,
|
||||||
y: 0,
|
pageSize: 100,
|
||||||
},
|
|
||||||
...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) ||
|
|
||||||
[]),
|
|
||||||
],
|
|
||||||
widgets: [
|
|
||||||
...(data?.widgets || []),
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
isStacked: false,
|
|
||||||
nullZeroValues: '',
|
|
||||||
opacity: '',
|
|
||||||
panelTypes: name,
|
|
||||||
query:
|
|
||||||
name === PANEL_TYPES.LIST
|
|
||||||
? listViewInitialLogQuery
|
|
||||||
: PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
timePreferance: 'GLOBAL_TIME',
|
|
||||||
softMax: null,
|
|
||||||
softMin: null,
|
|
||||||
selectedLogFields: [
|
|
||||||
{
|
|
||||||
dataType: 'string',
|
|
||||||
type: '',
|
|
||||||
name: 'body',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataType: 'string',
|
|
||||||
type: '',
|
|
||||||
name: 'timestamp',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selectedTracesFields: [
|
|
||||||
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
{
|
};
|
||||||
onSuccess: (data) => {
|
|
||||||
if (data.payload) {
|
|
||||||
handleToggleDashboardSlider(false);
|
|
||||||
const queryParamsLog = {
|
|
||||||
graphType: name,
|
|
||||||
widgetId: id,
|
|
||||||
[QueryParams.compositeQuery]: JSON.stringify({
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
builder: {
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name].builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
|
|
||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
graphType: name,
|
graphType: name,
|
||||||
widgetId: id,
|
widgetId: id,
|
||||||
[QueryParams.compositeQuery]: JSON.stringify(
|
[QueryParams.compositeQuery]: JSON.stringify(
|
||||||
PANEL_TYPES_INITIAL_QUERY[name],
|
PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (name === PANEL_TYPES.LIST) {
|
if (name === PANEL_TYPES.LIST) {
|
||||||
history.push(
|
history.push(
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
history.push(
|
history.push(
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
notifications.success({
|
|
||||||
message: SOMETHING_WENT_WRONG,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
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[] = [
|
const Items: ItemsProps[] = [
|
||||||
{
|
{
|
||||||
@@ -28,9 +35,14 @@ const Items: ItemsProps[] = [
|
|||||||
icon: <BarChart3 size={32} color={Color.BG_ROBIN_400} />,
|
icon: <BarChart3 size={32} color={Color.BG_ROBIN_400} />,
|
||||||
display: 'Bar',
|
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;
|
name: PANEL_TYPES;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
display: string;
|
display: string;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Row } from 'antd';
|
import { Row } from 'antd';
|
||||||
|
import { isNull } from 'lodash-es';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
import { convertVariablesToDbFormat } from './util';
|
|
||||||
import VariableItem from './VariableItem';
|
import VariableItem from './VariableItem';
|
||||||
|
|
||||||
function DashboardVariableSelection(): JSX.Element | null {
|
function DashboardVariableSelection(): JSX.Element | null {
|
||||||
@@ -11,15 +11,14 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
setSelectedDashboard,
|
setSelectedDashboard,
|
||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
|
variablesToGetUpdated,
|
||||||
|
setVariablesToGetUpdated,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
const { data } = selectedDashboard || {};
|
const { data } = selectedDashboard || {};
|
||||||
|
|
||||||
const { variables } = data || {};
|
const { variables } = data || {};
|
||||||
|
|
||||||
const [update, setUpdate] = useState<boolean>(false);
|
|
||||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
|
||||||
|
|
||||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -45,8 +44,27 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
}, [variables]);
|
}, [variables]);
|
||||||
|
|
||||||
const onVarChanged = (name: string): void => {
|
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 = (
|
const onValueUpdate = (
|
||||||
@@ -54,39 +72,46 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
id: string,
|
id: string,
|
||||||
value: IDashboardVariable['selectedValue'],
|
value: IDashboardVariable['selectedValue'],
|
||||||
allSelected: boolean,
|
allSelected: boolean,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
): void => {
|
): void => {
|
||||||
if (id) {
|
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);
|
updateLocalStorageDashboardVariables(name, value, allSelected);
|
||||||
|
|
||||||
const variables = convertVariablesToDbFormat(newVariablesArr);
|
|
||||||
|
|
||||||
if (selectedDashboard) {
|
if (selectedDashboard) {
|
||||||
setSelectedDashboard({
|
setSelectedDashboard((prev) => {
|
||||||
...selectedDashboard,
|
if (prev) {
|
||||||
data: {
|
const oldVariables = prev?.data.variables;
|
||||||
...selectedDashboard?.data,
|
// this is added to handle case where we have two different
|
||||||
variables: {
|
// schemas for variable response
|
||||||
...variables,
|
if (oldVariables[id]) {
|
||||||
},
|
oldVariables[id] = {
|
||||||
},
|
...oldVariables[id],
|
||||||
|
selectedValue: value,
|
||||||
|
allSelected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (oldVariables[name]) {
|
||||||
|
oldVariables[name] = {
|
||||||
|
...oldVariables[name],
|
||||||
|
selectedValue: value,
|
||||||
|
allSelected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
data: {
|
||||||
|
...prev?.data,
|
||||||
|
variables: {
|
||||||
|
...oldVariables,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onVarChanged(name);
|
onVarChanged(name);
|
||||||
|
|
||||||
setUpdate(!update);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,13 +132,12 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
<VariableItem
|
<VariableItem
|
||||||
key={`${variable.name}${variable.id}}${variable.order}`}
|
key={`${variable.name}${variable.id}}${variable.order}`}
|
||||||
existingVariables={variables}
|
existingVariables={variables}
|
||||||
lastUpdatedVar={lastUpdatedVar}
|
|
||||||
variableData={{
|
variableData={{
|
||||||
name: variable.name,
|
name: variable.name,
|
||||||
...variable,
|
...variable,
|
||||||
change: update,
|
|
||||||
}}
|
}}
|
||||||
onValueUpdate={onValueUpdate}
|
onValueUpdate={onValueUpdate}
|
||||||
|
variablesToGetUpdated={variablesToGetUpdated}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -68,7 +68,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -82,7 +82,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockVariableData}
|
variableData={mockVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -110,7 +110,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockCustomVariableData}
|
variableData={mockCustomVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -131,7 +131,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={customVariableData}
|
variableData={customVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -146,7 +146,7 @@ describe('VariableItem', () => {
|
|||||||
variableData={mockCustomVariableData}
|
variableData={mockCustomVariableData}
|
||||||
existingVariables={{}}
|
existingVariables={{}}
|
||||||
onValueUpdate={mockOnValueUpdate}
|
onValueUpdate={mockOnValueUpdate}
|
||||||
lastUpdatedVar=""
|
variablesToGetUpdated={[]}
|
||||||
/>
|
/>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ interface VariableItemProps {
|
|||||||
arg1: IDashboardVariable['selectedValue'],
|
arg1: IDashboardVariable['selectedValue'],
|
||||||
allSelected: boolean,
|
allSelected: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
lastUpdatedVar: string;
|
variablesToGetUpdated: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSelectValue = (
|
const getSelectValue = (
|
||||||
@@ -49,7 +49,7 @@ function VariableItem({
|
|||||||
variableData,
|
variableData,
|
||||||
existingVariables,
|
existingVariables,
|
||||||
onValueUpdate,
|
onValueUpdate,
|
||||||
lastUpdatedVar,
|
variablesToGetUpdated,
|
||||||
}: VariableItemProps): JSX.Element {
|
}: VariableItemProps): JSX.Element {
|
||||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||||
[],
|
[],
|
||||||
@@ -108,16 +108,10 @@ function VariableItem({
|
|||||||
|
|
||||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||||
/* eslint-disable no-useless-escape */
|
/* 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 (
|
if (
|
||||||
variableData.type === 'QUERY' &&
|
variableData.type === 'QUERY' &&
|
||||||
dependVarReMatch !== null &&
|
variableData.name &&
|
||||||
dependVarReMatch.length > 0
|
variablesToGetUpdated.includes(variableData.name)
|
||||||
) {
|
) {
|
||||||
let value = variableData.selectedValue;
|
let value = variableData.selectedValue;
|
||||||
let allSelected = false;
|
let allSelected = false;
|
||||||
|
|||||||
@@ -2,18 +2,17 @@ import './QuerySection.styles.scss';
|
|||||||
|
|
||||||
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||||
import { WidgetGraphProps } from 'container/NewWidget/types';
|
import { getDefaultWidgetData } from 'container/NewWidget/utils';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import { defaultTo } from 'lodash-es';
|
||||||
import { Atom, Play, Terminal } from 'lucide-react';
|
import { Atom, Play, Terminal } from 'lucide-react';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
@@ -22,9 +21,12 @@ import {
|
|||||||
getSelectedWidgetIndex,
|
getSelectedWidgetIndex,
|
||||||
} from 'providers/Dashboard/util';
|
} from 'providers/Dashboard/util';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
@@ -35,7 +37,7 @@ import PromQLQueryContainer from './QueryBuilder/promQL';
|
|||||||
|
|
||||||
function QuerySection({
|
function QuerySection({
|
||||||
selectedGraph,
|
selectedGraph,
|
||||||
selectedTime,
|
queryResponse,
|
||||||
}: QueryProps): JSX.Element {
|
}: QueryProps): JSX.Element {
|
||||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
@@ -51,20 +53,15 @@ function QuerySection({
|
|||||||
|
|
||||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||||
|
|
||||||
const getWidgetQueryRange = useGetWidgetQueryRange(
|
|
||||||
{
|
|
||||||
graphType: selectedGraph,
|
|
||||||
selectedTime: selectedTime.enum,
|
|
||||||
},
|
|
||||||
selectedDashboard?.data?.version || DEFAULT_ENTITY_VERSION,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { widgets } = selectedDashboard?.data || {};
|
const { widgets } = selectedDashboard?.data || {};
|
||||||
|
|
||||||
const getWidget = useCallback(() => {
|
const getWidget = useCallback(() => {
|
||||||
const widgetId = urlQuery.get('widgetId');
|
const widgetId = urlQuery.get('widgetId');
|
||||||
return widgets?.find((e) => e.id === widgetId);
|
return defaultTo(
|
||||||
}, [widgets, urlQuery]);
|
widgets?.find((e) => e.id === widgetId),
|
||||||
|
getDefaultWidgetData(widgetId || '', selectedGraph),
|
||||||
|
);
|
||||||
|
}, [urlQuery, widgets, selectedGraph]);
|
||||||
|
|
||||||
const selectedWidget = getWidget() as Widgets;
|
const selectedWidget = getWidget() as Widgets;
|
||||||
|
|
||||||
@@ -233,7 +230,7 @@ function QuerySection({
|
|||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
<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" />
|
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||||
<Button
|
<Button
|
||||||
loading={getWidgetQueryRange.isFetching}
|
loading={queryResponse.isFetching}
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleRunQuery}
|
onClick={handleRunQuery}
|
||||||
className="stage-run-query"
|
className="stage-run-query"
|
||||||
@@ -251,7 +248,10 @@ function QuerySection({
|
|||||||
|
|
||||||
interface QueryProps {
|
interface QueryProps {
|
||||||
selectedGraph: PANEL_TYPES;
|
selectedGraph: PANEL_TYPES;
|
||||||
selectedTime: WidgetGraphProps['selectedTime'];
|
queryResponse: UseQueryResult<
|
||||||
|
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default QuerySection;
|
export default QuerySection;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user