mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-26 10:04:26 +08:00
commit
73fc262f04
@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.33.1
|
image: signoz/query-service:0.34.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.33.1
|
image: signoz/frontend:0.34.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.79.13
|
image: signoz/signoz-otel-collector:0.88.0
|
||||||
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.79.13
|
image: signoz/signoz-schema-migrator:0.88.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@ -250,7 +250,7 @@ services:
|
|||||||
# - clickhouse-3
|
# - clickhouse-3
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/signoz-otel-collector:0.79.13
|
image: signoz/signoz-otel-collector:0.88.0
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||||
|
@ -61,35 +61,6 @@ receivers:
|
|||||||
job_name: otel-collector
|
job_name: otel-collector
|
||||||
|
|
||||||
processors:
|
processors:
|
||||||
logstransform/internal:
|
|
||||||
operators:
|
|
||||||
- type: regex_parser
|
|
||||||
id: traceid
|
|
||||||
# https://regex101.com/r/MMfNjk/1
|
|
||||||
regex: '(?i)(trace(-|_||)id("|=| |-|:)*)(?P<trace_id>[A-Fa-f0-9]+)'
|
|
||||||
parse_from: body
|
|
||||||
parse_to: attributes.temp_trace
|
|
||||||
if: 'body matches "(?i)(trace(-|_||)id(\"|=| |-|:)*)(?P<trace_id>[A-Fa-f0-9]+)"'
|
|
||||||
output: spanid
|
|
||||||
- type: regex_parser
|
|
||||||
id: spanid
|
|
||||||
# https://regex101.com/r/uXSwLc/1
|
|
||||||
regex: '(?i)(span(-|_||)id("|=| |-|:)*)(?P<span_id>[A-Fa-f0-9]+)'
|
|
||||||
parse_from: body
|
|
||||||
parse_to: attributes.temp_trace
|
|
||||||
if: 'body matches "(?i)(span(-|_||)id(\"|=| |-|:)*)(?P<span_id>[A-Fa-f0-9]+)"'
|
|
||||||
output: trace_parser
|
|
||||||
- type: trace_parser
|
|
||||||
id: trace_parser
|
|
||||||
trace_id:
|
|
||||||
parse_from: attributes.temp_trace.trace_id
|
|
||||||
span_id:
|
|
||||||
parse_from: attributes.temp_trace.span_id
|
|
||||||
output: remove_temp
|
|
||||||
- type: remove
|
|
||||||
id: remove_temp
|
|
||||||
field: attributes.temp_trace
|
|
||||||
if: '"temp_trace" in attributes'
|
|
||||||
batch:
|
batch:
|
||||||
send_batch_size: 10000
|
send_batch_size: 10000
|
||||||
send_batch_max_size: 11000
|
send_batch_max_size: 11000
|
||||||
@ -188,5 +159,5 @@ service:
|
|||||||
exporters: [prometheus]
|
exporters: [prometheus]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp, tcplog/docker]
|
receivers: [otlp, tcplog/docker]
|
||||||
processors: [logstransform/internal, batch]
|
processors: [batch]
|
||||||
exporters: [clickhouselogsexporter]
|
exporters: [clickhouselogsexporter]
|
||||||
|
@ -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.79.13}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.0}
|
||||||
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.79.13
|
image: signoz/signoz-otel-collector:0.88.0
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@ -118,7 +118,7 @@ services:
|
|||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
container_name: signoz-otel-collector-metrics
|
container_name: signoz-otel-collector-metrics
|
||||||
image: signoz/signoz-otel-collector:0.79.13
|
image: signoz/signoz-otel-collector:0.88.0
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||||
|
@ -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.33.1}
|
image: signoz/query-service:${DOCKER_TAG:-0.34.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.33.1}
|
image: signoz/frontend:${DOCKER_TAG:-0.34.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.79.13}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.0}
|
||||||
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.79.13}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.0}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@ -269,7 +269,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.13}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.0}
|
||||||
container_name: signoz-otel-collector-metrics
|
container_name: signoz-otel-collector-metrics
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
@ -62,35 +62,6 @@ receivers:
|
|||||||
|
|
||||||
|
|
||||||
processors:
|
processors:
|
||||||
logstransform/internal:
|
|
||||||
operators:
|
|
||||||
- type: regex_parser
|
|
||||||
id: traceid
|
|
||||||
# https://regex101.com/r/MMfNjk/1
|
|
||||||
regex: '(?i)(trace(-|_||)id("|=| |-|:)*)(?P<trace_id>[A-Fa-f0-9]+)'
|
|
||||||
parse_from: body
|
|
||||||
parse_to: attributes.temp_trace
|
|
||||||
if: 'body matches "(?i)(trace(-|_||)id(\"|=| |-|:)*)(?P<trace_id>[A-Fa-f0-9]+)"'
|
|
||||||
output: spanid
|
|
||||||
- type: regex_parser
|
|
||||||
id: spanid
|
|
||||||
# https://regex101.com/r/uXSwLc/1
|
|
||||||
regex: '(?i)(span(-|_||)id("|=| |-|:)*)(?P<span_id>[A-Fa-f0-9]+)'
|
|
||||||
parse_from: body
|
|
||||||
parse_to: attributes.temp_trace
|
|
||||||
if: 'body matches "(?i)(span(-|_||)id(\"|=| |-|:)*)(?P<span_id>[A-Fa-f0-9]+)"'
|
|
||||||
output: trace_parser
|
|
||||||
- type: trace_parser
|
|
||||||
id: trace_parser
|
|
||||||
trace_id:
|
|
||||||
parse_from: attributes.temp_trace.trace_id
|
|
||||||
span_id:
|
|
||||||
parse_from: attributes.temp_trace.span_id
|
|
||||||
output: remove_temp
|
|
||||||
- type: remove
|
|
||||||
id: remove_temp
|
|
||||||
field: attributes.temp_trace
|
|
||||||
if: '"temp_trace" in attributes'
|
|
||||||
batch:
|
batch:
|
||||||
send_batch_size: 10000
|
send_batch_size: 10000
|
||||||
send_batch_max_size: 11000
|
send_batch_max_size: 11000
|
||||||
@ -193,5 +164,5 @@ service:
|
|||||||
exporters: [prometheus]
|
exporters: [prometheus]
|
||||||
logs:
|
logs:
|
||||||
receivers: [otlp, tcplog/docker]
|
receivers: [otlp, tcplog/docker]
|
||||||
processors: [logstransform/internal, batch]
|
processors: [batch]
|
||||||
exporters: [clickhouselogsexporter]
|
exporters: [clickhouselogsexporter]
|
@ -160,6 +160,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
|||||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
|
||||||
|
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
|
||||||
|
|
||||||
router.HandleFunc("/api/v2/licenses",
|
router.HandleFunc("/api/v2/licenses",
|
||||||
am.ViewAccess(ah.listLicensesV2)).
|
am.ViewAccess(ah.listLicensesV2)).
|
||||||
Methods(http.MethodGet)
|
Methods(http.MethodGet)
|
||||||
|
51
ee/query-service/app/api/dashboard.go
Normal file
51
ee/query-service/app/api/dashboard.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/common"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ah.lockUnlockDashboard(w, r, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ah.lockUnlockDashboard(w, r, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) {
|
||||||
|
// Locking can only be done by the owner of the dashboard
|
||||||
|
// or an admin
|
||||||
|
|
||||||
|
// - Fetch the dashboard
|
||||||
|
// - Check if the user is the owner or an admin
|
||||||
|
// - If yes, lock/unlock the dashboard
|
||||||
|
// - If no, return 403
|
||||||
|
|
||||||
|
// Get the dashboard UUID from the request
|
||||||
|
uuid := mux.Vars(r)["uuid"]
|
||||||
|
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
|
||||||
|
if err != nil {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := common.GetUserFromContext(r.Context())
|
||||||
|
if !auth.IsAdmin(user) && (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, "You are not authorized to lock/unlock this dashboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock/Unlock the dashboard
|
||||||
|
err = dashboards.LockUnlockDashboard(r.Context(), uuid, lock)
|
||||||
|
if err != nil {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ah.Respond(w, "Dashboard updated successfully")
|
||||||
|
}
|
@ -52,7 +52,6 @@ func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := context.Background()
|
|
||||||
var l model.License
|
var l model.License
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
|
||||||
@ -64,8 +63,7 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
|||||||
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
license, apiError := ah.LM().Activate(r.Context(), l.Key)
|
||||||
license, apiError := ah.LM().Activate(ctx, l.Key)
|
|
||||||
if apiError != nil {
|
if apiError != nil {
|
||||||
RespondError(w, apiError, nil)
|
RespondError(w, apiError, nil)
|
||||||
return
|
return
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"go.signoz.io/signoz/ee/query-service/constants"
|
"go.signoz.io/signoz/ee/query-service/constants"
|
||||||
"go.signoz.io/signoz/ee/query-service/dao"
|
"go.signoz.io/signoz/ee/query-service/dao"
|
||||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
baseInterface "go.signoz.io/signoz/pkg/query-service/interfaces"
|
baseInterface "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
|
||||||
@ -437,7 +438,10 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
|
|||||||
telemetry.GetInstance().AddActiveLogsUser()
|
telemetry.GetInstance().AddActiveLogsUser()
|
||||||
}
|
}
|
||||||
data["dataSources"] = dataSources
|
data["dataSources"] = dataSources
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, true)
|
userEmail, err := auth.GetEmailFromJwt(r.Context())
|
||||||
|
if err == nil {
|
||||||
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, userEmail, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return data, true
|
return data, true
|
||||||
}
|
}
|
||||||
@ -458,6 +462,8 @@ func getActiveLogs(path string, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := auth.AttachJwtToContext(r.Context(), r)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
route := mux.CurrentRoute(r)
|
route := mux.CurrentRoute(r)
|
||||||
path, _ := route.GetPathTemplate()
|
path, _ := route.GetPathTemplate()
|
||||||
|
|
||||||
@ -475,7 +481,10 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := telemetry.IgnoredPaths()[path]; !ok {
|
if _, ok := telemetry.IgnoredPaths()[path]; !ok {
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data)
|
userEmail, err := auth.GetEmailFromJwt(r.Context())
|
||||||
|
if err == nil {
|
||||||
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
|
|
||||||
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
|
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
|
||||||
@ -203,7 +204,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
|
|||||||
zap.S().Errorf("License validation completed with error", reterr)
|
zap.S().Errorf("License validation completed with error", reterr)
|
||||||
atomic.AddUint64(&lm.failedAttempts, 1)
|
atomic.AddUint64(&lm.failedAttempts, 1)
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
||||||
map[string]interface{}{"err": reterr.Error()})
|
map[string]interface{}{"err": reterr.Error()}, "")
|
||||||
} else {
|
} else {
|
||||||
zap.S().Info("License validation completed with no errors")
|
zap.S().Info("License validation completed with no errors")
|
||||||
}
|
}
|
||||||
@ -259,8 +260,11 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
|
|||||||
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
|
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if errResponse != nil {
|
if errResponse != nil {
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
userEmail, err := auth.GetEmailFromJwt(ctx)
|
||||||
map[string]interface{}{"err": errResponse.Err.Error()})
|
if err == nil {
|
||||||
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
||||||
|
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"@uiw/react-md-editor": "3.23.5",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
"@xstate/react": "^3.0.0",
|
"@xstate/react": "^3.0.0",
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"antd": "5.0.5",
|
"antd": "5.11.0",
|
||||||
"antd-table-saveas-excel": "2.2.1",
|
"antd-table-saveas-excel": "2.2.1",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
@ -80,11 +80,11 @@
|
|||||||
"react-dnd-html5-backend": "16.0.1",
|
"react-dnd-html5-backend": "16.0.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-drag-listview": "2.0.0",
|
"react-drag-listview": "2.0.0",
|
||||||
|
"react-error-boundary": "4.0.11",
|
||||||
"react-force-graph": "^1.43.0",
|
"react-force-graph": "^1.43.0",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-helmet-async": "1.3.0",
|
"react-helmet-async": "1.3.0",
|
||||||
"react-i18next": "^11.16.1",
|
"react-i18next": "^11.16.1",
|
||||||
"react-intersection-observer": "9.4.1",
|
|
||||||
"react-markdown": "8.0.7",
|
"react-markdown": "8.0.7",
|
||||||
"react-query": "^3.34.19",
|
"react-query": "^3.34.19",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
@ -102,6 +102,7 @@
|
|||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.0.5",
|
||||||
|
"uplot": "1.6.26",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web-vitals": "^0.2.4",
|
"web-vitals": "^0.2.4",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.88.2",
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
"button_returntorules": "Return to rules",
|
"button_returntorules": "Return to rules",
|
||||||
"button_cancelchanges": "Cancel",
|
"button_cancelchanges": "Cancel",
|
||||||
"button_discard": "Discard",
|
"button_discard": "Discard",
|
||||||
"text_condition1": "Send a notification when the metric is",
|
"text_condition1": "Send a notification when",
|
||||||
"text_condition2": "the threshold",
|
"text_condition2": "the threshold",
|
||||||
"text_condition3": "during the last",
|
"text_condition3": "during the last",
|
||||||
"option_5min": "5 mins",
|
"option_5min": "5 mins",
|
||||||
@ -109,5 +109,6 @@
|
|||||||
"traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.",
|
"traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.",
|
||||||
"exceptions_based_alert": "Exceptions-based Alert",
|
"exceptions_based_alert": "Exceptions-based Alert",
|
||||||
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
||||||
"field_unit": "Threshold unit"
|
"field_unit": "Threshold unit",
|
||||||
|
"selected_query_placeholder": "Select query"
|
||||||
}
|
}
|
||||||
|
@ -1,85 +1,85 @@
|
|||||||
{
|
{
|
||||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||||
"preview_chart_threshold_label": "Threshold",
|
"preview_chart_threshold_label": "Threshold",
|
||||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||||
"button_yes": "Yes",
|
"button_yes": "Yes",
|
||||||
"button_no": "No",
|
"button_no": "No",
|
||||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||||
"remove_label_success": "Labels cleared",
|
"remove_label_success": "Labels cleared",
|
||||||
"alert_form_step1": "Step 1 - Define the metric",
|
"alert_form_step1": "Step 1 - Define the metric",
|
||||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||||
"confirm_save_title": "Save Changes",
|
"confirm_save_title": "Save Changes",
|
||||||
"confirm_save_content_part1": "Your alert built with",
|
"confirm_save_content_part1": "Your alert built with",
|
||||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||||
"rule_created": "Rule created successfully",
|
"rule_created": "Rule created successfully",
|
||||||
"rule_edited": "Rule edited successfully",
|
"rule_edited": "Rule edited successfully",
|
||||||
"expression_missing": "expression is missing in {{where}}",
|
"expression_missing": "expression is missing in {{where}}",
|
||||||
"metricname_missing": "metric name is missing in {{where}}",
|
"metricname_missing": "metric name is missing in {{where}}",
|
||||||
"condition_required": "at least one metric condition is required",
|
"condition_required": "at least one metric condition is required",
|
||||||
"alertname_required": "alert name is required",
|
"alertname_required": "alert name is required",
|
||||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||||
"button_savechanges": "Save Rule",
|
"button_savechanges": "Save Rule",
|
||||||
"button_createrule": "Create Rule",
|
"button_createrule": "Create Rule",
|
||||||
"button_returntorules": "Return to rules",
|
"button_returntorules": "Return to rules",
|
||||||
"button_cancelchanges": "Cancel",
|
"button_cancelchanges": "Cancel",
|
||||||
"button_discard": "Discard",
|
"button_discard": "Discard",
|
||||||
"text_condition1": "Send a notification when the metric is",
|
"text_condition1": "Send a notification when",
|
||||||
"text_condition2": "the threshold",
|
"text_condition2": "the threshold",
|
||||||
"text_condition3": "during the last",
|
"text_condition3": "during the last",
|
||||||
"option_5min": "5 mins",
|
"option_5min": "5 mins",
|
||||||
"option_10min": "10 mins",
|
"option_10min": "10 mins",
|
||||||
"option_15min": "15 mins",
|
"option_15min": "15 mins",
|
||||||
"option_60min": "60 mins",
|
"option_60min": "60 mins",
|
||||||
"option_4hours": "4 hours",
|
"option_4hours": "4 hours",
|
||||||
"option_24hours": "24 hours",
|
"option_24hours": "24 hours",
|
||||||
"field_threshold": "Alert Threshold",
|
"field_threshold": "Alert Threshold",
|
||||||
"option_allthetimes": "all the times",
|
"option_allthetimes": "all the times",
|
||||||
"option_atleastonce": "at least once",
|
"option_atleastonce": "at least once",
|
||||||
"option_onaverage": "on average",
|
"option_onaverage": "on average",
|
||||||
"option_intotal": "in total",
|
"option_intotal": "in total",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
"button_formula": "Formula",
|
"button_formula": "Formula",
|
||||||
"tab_qb": "Query Builder",
|
"tab_qb": "Query Builder",
|
||||||
"tab_promql": "PromQL",
|
"tab_promql": "PromQL",
|
||||||
"title_confirm": "Confirm",
|
"title_confirm": "Confirm",
|
||||||
"button_ok": "Yes",
|
"button_ok": "Yes",
|
||||||
"button_cancel": "No",
|
"button_cancel": "No",
|
||||||
"field_promql_expr": "PromQL Expression",
|
"field_promql_expr": "PromQL Expression",
|
||||||
"field_alert_name": "Alert Name",
|
"field_alert_name": "Alert Name",
|
||||||
"field_alert_desc": "Alert Description",
|
"field_alert_desc": "Alert Description",
|
||||||
"field_labels": "Labels",
|
"field_labels": "Labels",
|
||||||
"field_severity": "Severity",
|
"field_severity": "Severity",
|
||||||
"option_critical": "Critical",
|
"option_critical": "Critical",
|
||||||
"option_error": "Error",
|
"option_error": "Error",
|
||||||
"option_warning": "Warning",
|
"option_warning": "Warning",
|
||||||
"option_info": "Info",
|
"option_info": "Info",
|
||||||
"user_guide_headline": "Steps to create an Alert",
|
"user_guide_headline": "Steps to create an Alert",
|
||||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||||
"user_tooltip_more_help": "More details on how to create alerts"
|
"user_tooltip_more_help": "More details on how to create alerts"
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
"button_returntorules": "Return to rules",
|
"button_returntorules": "Return to rules",
|
||||||
"button_cancelchanges": "Cancel",
|
"button_cancelchanges": "Cancel",
|
||||||
"button_discard": "Discard",
|
"button_discard": "Discard",
|
||||||
"text_condition1": "Send a notification when the metric is",
|
"text_condition1": "Send a notification when",
|
||||||
"text_condition2": "the threshold",
|
"text_condition2": "the threshold",
|
||||||
"text_condition3": "during the last",
|
"text_condition3": "during the last",
|
||||||
"option_5min": "5 mins",
|
"option_5min": "5 mins",
|
||||||
@ -109,5 +109,6 @@
|
|||||||
"traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.",
|
"traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.",
|
||||||
"exceptions_based_alert": "Exceptions-based Alert",
|
"exceptions_based_alert": "Exceptions-based Alert",
|
||||||
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
||||||
"field_unit": "Threshold unit"
|
"field_unit": "Threshold unit",
|
||||||
|
"selected_query_placeholder": "Select query"
|
||||||
}
|
}
|
||||||
|
@ -20,5 +20,7 @@
|
|||||||
"variable_updated_successfully": "Variable updated successfully",
|
"variable_updated_successfully": "Variable updated successfully",
|
||||||
"error_while_updating_variable": "Error while updating variable",
|
"error_while_updating_variable": "Error while updating variable",
|
||||||
"dashboard_has_been_updated": "Dashboard has been updated",
|
"dashboard_has_been_updated": "Dashboard has been updated",
|
||||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?"
|
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
|
||||||
|
"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.",
|
||||||
|
"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard."
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,7 @@
|
|||||||
"see_error_in_trace_graph": "See the error in trace graph",
|
"see_error_in_trace_graph": "See the error in trace graph",
|
||||||
"stack_trace": "Stacktrace",
|
"stack_trace": "Stacktrace",
|
||||||
"older": "Older",
|
"older": "Older",
|
||||||
"newer": "Newer"
|
"newer": "Newer",
|
||||||
|
"something_went_wrong": "Oops !!! Something went wrong",
|
||||||
|
"contact_if_issue_exists": "Don't worry, our team is here to help. Please contact support if the issue persists."
|
||||||
}
|
}
|
||||||
|
@ -1,85 +1,85 @@
|
|||||||
{
|
{
|
||||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||||
"preview_chart_threshold_label": "Threshold",
|
"preview_chart_threshold_label": "Threshold",
|
||||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||||
"button_yes": "Yes",
|
"button_yes": "Yes",
|
||||||
"button_no": "No",
|
"button_no": "No",
|
||||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||||
"remove_label_success": "Labels cleared",
|
"remove_label_success": "Labels cleared",
|
||||||
"alert_form_step1": "Step 1 - Define the metric",
|
"alert_form_step1": "Step 1 - Define the metric",
|
||||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||||
"confirm_save_title": "Save Changes",
|
"confirm_save_title": "Save Changes",
|
||||||
"confirm_save_content_part1": "Your alert built with",
|
"confirm_save_content_part1": "Your alert built with",
|
||||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||||
"rule_created": "Rule created successfully",
|
"rule_created": "Rule created successfully",
|
||||||
"rule_edited": "Rule edited successfully",
|
"rule_edited": "Rule edited successfully",
|
||||||
"expression_missing": "expression is missing in {{where}}",
|
"expression_missing": "expression is missing in {{where}}",
|
||||||
"metricname_missing": "metric name is missing in {{where}}",
|
"metricname_missing": "metric name is missing in {{where}}",
|
||||||
"condition_required": "at least one metric condition is required",
|
"condition_required": "at least one metric condition is required",
|
||||||
"alertname_required": "alert name is required",
|
"alertname_required": "alert name is required",
|
||||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||||
"button_savechanges": "Save Rule",
|
"button_savechanges": "Save Rule",
|
||||||
"button_createrule": "Create Rule",
|
"button_createrule": "Create Rule",
|
||||||
"button_returntorules": "Return to rules",
|
"button_returntorules": "Return to rules",
|
||||||
"button_cancelchanges": "Cancel",
|
"button_cancelchanges": "Cancel",
|
||||||
"button_discard": "Discard",
|
"button_discard": "Discard",
|
||||||
"text_condition1": "Send a notification when the metric is",
|
"text_condition1": "Send a notification when",
|
||||||
"text_condition2": "the threshold",
|
"text_condition2": "the threshold",
|
||||||
"text_condition3": "during the last",
|
"text_condition3": "during the last",
|
||||||
"option_5min": "5 mins",
|
"option_5min": "5 mins",
|
||||||
"option_10min": "10 mins",
|
"option_10min": "10 mins",
|
||||||
"option_15min": "15 mins",
|
"option_15min": "15 mins",
|
||||||
"option_60min": "60 mins",
|
"option_60min": "60 mins",
|
||||||
"option_4hours": "4 hours",
|
"option_4hours": "4 hours",
|
||||||
"option_24hours": "24 hours",
|
"option_24hours": "24 hours",
|
||||||
"field_threshold": "Alert Threshold",
|
"field_threshold": "Alert Threshold",
|
||||||
"option_allthetimes": "all the times",
|
"option_allthetimes": "all the times",
|
||||||
"option_atleastonce": "at least once",
|
"option_atleastonce": "at least once",
|
||||||
"option_onaverage": "on average",
|
"option_onaverage": "on average",
|
||||||
"option_intotal": "in total",
|
"option_intotal": "in total",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
"button_formula": "Formula",
|
"button_formula": "Formula",
|
||||||
"tab_qb": "Query Builder",
|
"tab_qb": "Query Builder",
|
||||||
"tab_promql": "PromQL",
|
"tab_promql": "PromQL",
|
||||||
"title_confirm": "Confirm",
|
"title_confirm": "Confirm",
|
||||||
"button_ok": "Yes",
|
"button_ok": "Yes",
|
||||||
"button_cancel": "No",
|
"button_cancel": "No",
|
||||||
"field_promql_expr": "PromQL Expression",
|
"field_promql_expr": "PromQL Expression",
|
||||||
"field_alert_name": "Alert Name",
|
"field_alert_name": "Alert Name",
|
||||||
"field_alert_desc": "Alert Description",
|
"field_alert_desc": "Alert Description",
|
||||||
"field_labels": "Labels",
|
"field_labels": "Labels",
|
||||||
"field_severity": "Severity",
|
"field_severity": "Severity",
|
||||||
"option_critical": "Critical",
|
"option_critical": "Critical",
|
||||||
"option_error": "Error",
|
"option_error": "Error",
|
||||||
"option_warning": "Warning",
|
"option_warning": "Warning",
|
||||||
"option_info": "Info",
|
"option_info": "Info",
|
||||||
"user_guide_headline": "Steps to create an Alert",
|
"user_guide_headline": "Steps to create an Alert",
|
||||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||||
"user_tooltip_more_help": "More details on how to create alerts"
|
"user_tooltip_more_help": "More details on how to create alerts"
|
||||||
}
|
}
|
||||||
|
3
frontend/public/locales/en/valueGraph.json
Normal file
3
frontend/public/locales/en/valueGraph.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"this_value_satisfies_multiple_thresholds": "This value satisfies multiple thresholds."
|
||||||
|
}
|
11
frontend/src/api/dashboard/lockDashboard.ts
Normal file
11
frontend/src/api/dashboard/lockDashboard.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
interface LockDashboardProps {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockDashboard = (props: LockDashboardProps): Promise<AxiosResponse> =>
|
||||||
|
axios.put(`/dashboards/${props.uuid}/lock`);
|
||||||
|
|
||||||
|
export default lockDashboard;
|
11
frontend/src/api/dashboard/unlockDashboard.ts
Normal file
11
frontend/src/api/dashboard/unlockDashboard.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
interface UnlockDashboardProps {
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlockDashboard = (props: UnlockDashboardProps): Promise<AxiosResponse> =>
|
||||||
|
axios.put(`/dashboards/${props.uuid}/unlock`);
|
||||||
|
|
||||||
|
export default unlockDashboard;
|
@ -3,10 +3,10 @@
|
|||||||
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="ant-table-wrapper css-dev-only-do-not-override-1i536d8"
|
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-spin-nested-loading css-dev-only-do-not-override-1i536d8"
|
class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-spin-container"
|
class="ant-spin-container"
|
||||||
@ -28,7 +28,7 @@ exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
|||||||
class="ant-table-thead"
|
class="ant-table-thead"
|
||||||
>
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<td
|
||||||
class="ant-table-cell"
|
class="ant-table-cell"
|
||||||
/>
|
/>
|
||||||
</tr>
|
</tr>
|
||||||
@ -43,7 +43,7 @@ exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
|||||||
class="ant-table-cell"
|
class="ant-table-cell"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-1i536d8 ant-empty ant-empty-normal"
|
class="css-dev-only-do-not-override-2i2tap ant-empty ant-empty-normal"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-empty-image"
|
class="ant-empty-image"
|
||||||
|
@ -51,11 +51,10 @@ function SaveViewWithName({
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Typography>{t('name_of_the_view')}</Typography>
|
<Typography>{t('name_of_the_view')}</Typography>
|
||||||
<Form form={form} onFinish={onSaveHandler}>
|
<Form form={form} onFinish={onSaveHandler} requiredMark>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['viewName']}
|
name={['viewName']}
|
||||||
required
|
required
|
||||||
requiredMark
|
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -35,7 +35,7 @@ export type GraphOnClickHandler = (
|
|||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export type ToggleGraphProps = {
|
export type ToggleGraphProps = {
|
||||||
toggleGraph(graphIndex: number, isVisible: boolean): void;
|
toggleGraph(graphIndex: number, isVisible: boolean, reference?: string): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomChartOptions = ChartOptions & {
|
export type CustomChartOptions = ChartOptions & {
|
||||||
|
@ -46,7 +46,7 @@ export const getYAxisFormattedValue = (
|
|||||||
return `${parseFloat(value)}`;
|
return `${parseFloat(value)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getToolTipValue = (value: string, format: string): string => {
|
export const getToolTipValue = (value: string, format?: string): string => {
|
||||||
try {
|
try {
|
||||||
return formattedValueToString(
|
return formattedValueToString(
|
||||||
getValueFormat(format)(parseFloat(value), undefined, undefined, undefined),
|
getValueFormat(format)(parseFloat(value), undefined, undefined, undefined),
|
||||||
|
@ -9,7 +9,7 @@ exports[`MessageTip custom action 1`] = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="ant-alert ant-alert-info ant-alert-with-description c0 css-dev-only-do-not-override-1i536d8"
|
class="ant-alert ant-alert-info ant-alert-with-description c0 css-dev-only-do-not-override-2i2tap"
|
||||||
data-show="true"
|
data-show="true"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DownOutlined } from '@ant-design/icons';
|
||||||
import { Button, Dropdown } from 'antd';
|
import { Button, Dropdown } from 'antd';
|
||||||
import TimeItems, {
|
import TimeItems, {
|
||||||
timePreferance,
|
timePreferance,
|
||||||
@ -33,7 +34,9 @@ function TimePreference({
|
|||||||
return (
|
return (
|
||||||
<TextContainer noButtonMargin>
|
<TextContainer noButtonMargin>
|
||||||
<Dropdown menu={menu}>
|
<Dropdown menu={menu}>
|
||||||
<Button>{selectedTime.name}</Button>
|
<Button>
|
||||||
|
{selectedTime.name} <DownOutlined />
|
||||||
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</TextContainer>
|
</TextContainer>
|
||||||
);
|
);
|
||||||
|
141
frontend/src/components/Uplot/Uplot.tsx
Normal file
141
frontend/src/components/Uplot/Uplot.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import './uplot.scss';
|
||||||
|
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import UPlot from 'uplot';
|
||||||
|
|
||||||
|
import { dataMatch, optionsUpdateState } from './utils';
|
||||||
|
|
||||||
|
export interface UplotProps {
|
||||||
|
options: uPlot.Options;
|
||||||
|
data: uPlot.AlignedData;
|
||||||
|
onDelete?: (chart: uPlot) => void;
|
||||||
|
onCreate?: (chart: uPlot) => void;
|
||||||
|
resetScales?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||||
|
(
|
||||||
|
{ options, data, onDelete, onCreate, resetScales = true },
|
||||||
|
ref,
|
||||||
|
): JSX.Element | null => {
|
||||||
|
const chartRef = useRef<uPlot | null>(null);
|
||||||
|
const propOptionsRef = useRef(options);
|
||||||
|
const targetRef = useRef<HTMLDivElement>(null);
|
||||||
|
const propDataRef = useRef(data);
|
||||||
|
const onCreateRef = useRef(onCreate);
|
||||||
|
const onDeleteRef = useRef(onDelete);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
(): ToggleGraphProps => ({
|
||||||
|
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||||
|
chartRef.current?.setSeries(graphIndex, { show: isVisible });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onCreateRef.current = onCreate;
|
||||||
|
onDeleteRef.current = onDelete;
|
||||||
|
});
|
||||||
|
|
||||||
|
const destroy = useCallback((chart: uPlot | null) => {
|
||||||
|
if (chart) {
|
||||||
|
onDeleteRef.current?.(chart);
|
||||||
|
chart.destroy();
|
||||||
|
chartRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const create = useCallback(() => {
|
||||||
|
if (targetRef.current === null) return;
|
||||||
|
|
||||||
|
// If data is empty, hide cursor
|
||||||
|
if (data && data[0] && data[0]?.length === 0) {
|
||||||
|
propOptionsRef.current = {
|
||||||
|
...propOptionsRef.current,
|
||||||
|
cursor: { show: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChart = new UPlot(
|
||||||
|
propOptionsRef.current,
|
||||||
|
propDataRef.current,
|
||||||
|
targetRef.current,
|
||||||
|
);
|
||||||
|
|
||||||
|
chartRef.current = newChart;
|
||||||
|
onCreateRef.current?.(newChart);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
create();
|
||||||
|
return (): void => {
|
||||||
|
destroy(chartRef.current);
|
||||||
|
};
|
||||||
|
}, [create, destroy]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (propOptionsRef.current !== options) {
|
||||||
|
const optionsState = optionsUpdateState(propOptionsRef.current, options);
|
||||||
|
propOptionsRef.current = options;
|
||||||
|
if (!chartRef.current || optionsState === 'create') {
|
||||||
|
destroy(chartRef.current);
|
||||||
|
create();
|
||||||
|
} else if (optionsState === 'update') {
|
||||||
|
chartRef.current.setSize({
|
||||||
|
width: options.width,
|
||||||
|
height: options.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [options, create, destroy]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (propDataRef.current !== data) {
|
||||||
|
if (!chartRef.current) {
|
||||||
|
propDataRef.current = data;
|
||||||
|
create();
|
||||||
|
} else if (!dataMatch(propDataRef.current, data)) {
|
||||||
|
if (resetScales) {
|
||||||
|
chartRef.current.setData(data, true);
|
||||||
|
} else {
|
||||||
|
chartRef.current.setData(data, false);
|
||||||
|
chartRef.current.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
propDataRef.current = data;
|
||||||
|
}
|
||||||
|
}, [data, resetScales, create]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="uplot-graph-container" ref={targetRef}>
|
||||||
|
{data && data[0] && data[0]?.length === 0 ? (
|
||||||
|
<div className="not-found">
|
||||||
|
<Typography>No Data</Typography>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Uplot.displayName = 'Uplot';
|
||||||
|
|
||||||
|
Uplot.defaultProps = {
|
||||||
|
onDelete: undefined,
|
||||||
|
onCreate: undefined,
|
||||||
|
resetScales: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Uplot);
|
3
frontend/src/components/Uplot/index.ts
Normal file
3
frontend/src/components/Uplot/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Uplot from './Uplot';
|
||||||
|
|
||||||
|
export default Uplot;
|
15
frontend/src/components/Uplot/uplot.scss
Normal file
15
frontend/src/components/Uplot/uplot.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.not-found {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 0;
|
||||||
|
height: 85%;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot-graph-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
48
frontend/src/components/Uplot/utils.ts
Normal file
48
frontend/src/components/Uplot/utils.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
|
type OptionsUpdateState = 'keep' | 'update' | 'create';
|
||||||
|
|
||||||
|
export const optionsUpdateState = (
|
||||||
|
_lhs: uPlot.Options,
|
||||||
|
_rhs: uPlot.Options,
|
||||||
|
): OptionsUpdateState => {
|
||||||
|
const { width: lhsWidth, height: lhsHeight, ...lhs } = _lhs;
|
||||||
|
const { width: rhsWidth, height: rhsHeight, ...rhs } = _rhs;
|
||||||
|
|
||||||
|
let state: OptionsUpdateState = 'keep';
|
||||||
|
|
||||||
|
if (lhsHeight !== rhsHeight || lhsWidth !== rhsWidth) {
|
||||||
|
state = 'update';
|
||||||
|
}
|
||||||
|
if (Object.keys(lhs).length !== Object.keys(rhs).length) {
|
||||||
|
return 'create';
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const k of Object.keys(lhs)) {
|
||||||
|
if (!Object.is((lhs as any)[k], (rhs as any)[k])) {
|
||||||
|
state = 'create';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dataMatch = (
|
||||||
|
lhs: uPlot.AlignedData,
|
||||||
|
rhs: uPlot.AlignedData,
|
||||||
|
): boolean => {
|
||||||
|
if (lhs.length !== rhs.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return lhs.every((lhsOneSeries, seriesIdx) => {
|
||||||
|
const rhsOneSeries = rhs[seriesIdx];
|
||||||
|
if (lhsOneSeries.length !== rhsOneSeries.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare each value in the series
|
||||||
|
return (lhsOneSeries as number[])?.every(
|
||||||
|
(value, valueIdx) => value === rhsOneSeries[valueIdx],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
31
frontend/src/components/ValueGraph/ValueGraph.styles.scss
Normal file
31
frontend/src/components/ValueGraph/ValueGraph.styles.scss
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.value-graph-container {
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.value-graph-text {
|
||||||
|
font-size: 2.5vw;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-graph-bgconflict {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-graph-textconflict {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-graph-icon {
|
||||||
|
color: #E89A3C;
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,66 @@
|
|||||||
import { Value } from './styles';
|
import './ValueGraph.styles.scss';
|
||||||
|
|
||||||
function ValueGraph({ value }: ValueGraphProps): JSX.Element {
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
return <Value>{value}</Value>;
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { getBackgroundColorAndThresholdCheck } from './utils';
|
||||||
|
|
||||||
|
function ValueGraph({
|
||||||
|
value,
|
||||||
|
rawValue,
|
||||||
|
thresholds,
|
||||||
|
}: ValueGraphProps): JSX.Element {
|
||||||
|
const { t } = useTranslation(['valueGraph']);
|
||||||
|
|
||||||
|
const {
|
||||||
|
threshold,
|
||||||
|
isConflictingThresholds,
|
||||||
|
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="value-graph-container"
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
threshold.thresholdFormat === 'Background'
|
||||||
|
? threshold.thresholdColor
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Text
|
||||||
|
className="value-graph-text"
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
threshold.thresholdFormat === 'Text'
|
||||||
|
? threshold.thresholdColor
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography.Text>
|
||||||
|
{isConflictingThresholds && (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
threshold.thresholdFormat === 'Background'
|
||||||
|
? 'value-graph-bgconflict'
|
||||||
|
: 'value-graph-textconflict'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||||
|
<ExclamationCircleFilled className="value-graph-icon" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValueGraphProps {
|
interface ValueGraphProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
rawValue: number;
|
||||||
|
thresholds: ThresholdProps[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ValueGraph;
|
export default ValueGraph;
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { Typography } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Value = styled(Typography)`
|
|
||||||
font-size: 2.5vw;
|
|
||||||
text-align: center;
|
|
||||||
`;
|
|
97
frontend/src/components/ValueGraph/utils.ts
Normal file
97
frontend/src/components/ValueGraph/utils.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
|
|
||||||
|
function compareThreshold(
|
||||||
|
rawValue: number,
|
||||||
|
threshold: ThresholdProps,
|
||||||
|
): boolean {
|
||||||
|
if (
|
||||||
|
threshold.thresholdOperator === undefined ||
|
||||||
|
threshold.thresholdValue === undefined
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (threshold.thresholdOperator) {
|
||||||
|
case '>':
|
||||||
|
return rawValue > threshold.thresholdValue;
|
||||||
|
case '>=':
|
||||||
|
return rawValue >= threshold.thresholdValue;
|
||||||
|
case '<':
|
||||||
|
return rawValue < threshold.thresholdValue;
|
||||||
|
case '<=':
|
||||||
|
return rawValue <= threshold.thresholdValue;
|
||||||
|
case '=':
|
||||||
|
return rawValue === threshold.thresholdValue;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractNumbersFromString(inputString: string): number[] {
|
||||||
|
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||||
|
const matches = inputString.match(regex);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
return matches.map(Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHighestPrecedenceThreshold(
|
||||||
|
matchingThresholds: ThresholdProps[],
|
||||||
|
thresholds: ThresholdProps[],
|
||||||
|
): ThresholdProps | null {
|
||||||
|
if (matchingThresholds.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// whichever threshold from matchingThresholds is found first in thresholds array return the threshold from thresholds array
|
||||||
|
let highestPrecedenceThreshold = matchingThresholds[0];
|
||||||
|
for (let i = 1; i < matchingThresholds.length; i += 1) {
|
||||||
|
if (
|
||||||
|
thresholds.indexOf(matchingThresholds[i]) <
|
||||||
|
thresholds.indexOf(highestPrecedenceThreshold)
|
||||||
|
) {
|
||||||
|
highestPrecedenceThreshold = matchingThresholds[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return highestPrecedenceThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackgroundColorAndThresholdCheck(
|
||||||
|
thresholds: ThresholdProps[],
|
||||||
|
rawValue: number,
|
||||||
|
): {
|
||||||
|
threshold: ThresholdProps;
|
||||||
|
isConflictingThresholds: boolean;
|
||||||
|
} {
|
||||||
|
const matchingThresholds = thresholds.filter((threshold) =>
|
||||||
|
compareThreshold(
|
||||||
|
extractNumbersFromString(
|
||||||
|
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
|
||||||
|
)[0],
|
||||||
|
threshold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingThresholds.length === 0) {
|
||||||
|
return {
|
||||||
|
threshold: {} as ThresholdProps,
|
||||||
|
isConflictingThresholds: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const highestPrecedenceThreshold = getHighestPrecedenceThreshold(
|
||||||
|
matchingThresholds,
|
||||||
|
thresholds,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isConflictingThresholds = matchingThresholds.length > 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
threshold: highestPrecedenceThreshold || ({} as ThresholdProps),
|
||||||
|
isConflictingThresholds,
|
||||||
|
};
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
import Graph from 'components/Graph';
|
import Uplot from 'components/Uplot';
|
||||||
import GridTableComponent from 'container/GridTableComponent';
|
import GridTableComponent from 'container/GridTableComponent';
|
||||||
import GridValueComponent from 'container/GridValueComponent';
|
import GridValueComponent from 'container/GridValueComponent';
|
||||||
|
|
||||||
import { PANEL_TYPES } from './queryBuilder';
|
import { PANEL_TYPES } from './queryBuilder';
|
||||||
|
|
||||||
export const PANEL_TYPES_COMPONENT_MAP = {
|
export const PANEL_TYPES_COMPONENT_MAP = {
|
||||||
[PANEL_TYPES.TIME_SERIES]: Graph,
|
[PANEL_TYPES.TIME_SERIES]: Uplot,
|
||||||
[PANEL_TYPES.VALUE]: GridValueComponent,
|
[PANEL_TYPES.VALUE]: GridValueComponent,
|
||||||
[PANEL_TYPES.TABLE]: GridTableComponent,
|
[PANEL_TYPES.TABLE]: GridTableComponent,
|
||||||
[PANEL_TYPES.TRACE]: null,
|
[PANEL_TYPES.TRACE]: null,
|
||||||
|
@ -9,7 +9,6 @@ const themeColors = {
|
|||||||
silver: '#BDBDBD',
|
silver: '#BDBDBD',
|
||||||
outrageousOrange: '#FF6633',
|
outrageousOrange: '#FF6633',
|
||||||
roseBud: '#FFB399',
|
roseBud: '#FFB399',
|
||||||
magentaPink: '#FF33FF',
|
|
||||||
canary: '#FFFF99',
|
canary: '#FFFF99',
|
||||||
deepSkyBlue: '#00B3E6',
|
deepSkyBlue: '#00B3E6',
|
||||||
goldTips: '#E6B333',
|
goldTips: '#E6B333',
|
||||||
|
@ -6,7 +6,9 @@ import Header from 'container/Header';
|
|||||||
import SideNav from 'container/SideNav';
|
import SideNav from 'container/SideNav';
|
||||||
import TopNav from 'container/TopNav';
|
import TopNav from 'container/TopNav';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { ReactNode, useEffect, useMemo, useRef } from 'react';
|
import { ReactNode, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
@ -203,12 +205,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
{isToDisplayLayout && <Header />}
|
{isToDisplayLayout && <Header />}
|
||||||
<Layout>
|
<Layout>
|
||||||
{isToDisplayLayout && !renderFullScreen && <SideNav />}
|
{isToDisplayLayout && !renderFullScreen && <SideNav />}
|
||||||
<LayoutContent>
|
|
||||||
<ChildrenContainer>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
<LayoutContent>
|
||||||
{children}
|
<ChildrenContainer>
|
||||||
</ChildrenContainer>
|
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||||
</LayoutContent>
|
{children}
|
||||||
|
</ChildrenContainer>
|
||||||
|
</LayoutContent>
|
||||||
|
</ErrorBoundary>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { StaticLineProps } from 'components/Graph/types';
|
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import getChartData from 'lib/getChartData';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useMemo } from 'react';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -54,20 +56,6 @@ function ChartPreview({
|
|||||||
targetUnit: query?.unit,
|
targetUnit: query?.unit,
|
||||||
});
|
});
|
||||||
|
|
||||||
const staticLine: StaticLineProps | undefined =
|
|
||||||
threshold !== undefined
|
|
||||||
? {
|
|
||||||
yMin: thresholdValue,
|
|
||||||
yMax: thresholdValue,
|
|
||||||
borderColor: '#f14',
|
|
||||||
borderWidth: 1,
|
|
||||||
lineText: `${t('preview_chart_threshold_label')} (y=${thresholdValue} ${
|
|
||||||
query?.unit || ''
|
|
||||||
})`,
|
|
||||||
textColor: '#f14',
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const canQuery = useMemo((): boolean => {
|
const canQuery = useMemo((): boolean => {
|
||||||
if (!query || query == null) {
|
if (!query || query == null) {
|
||||||
return false;
|
return false;
|
||||||
@ -114,15 +102,44 @@ function ChartPreview({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartDataSet = queryResponse.isError
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
? null
|
|
||||||
: getChartData({
|
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||||
queryData: [
|
|
||||||
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
getUPlotChartOptions({
|
||||||
|
id: 'alert_legend_widget',
|
||||||
|
yAxisUnit: query?.unit,
|
||||||
|
apiResponse: queryResponse?.data?.payload,
|
||||||
|
dimensions: containerDimensions,
|
||||||
|
isDarkMode,
|
||||||
|
thresholds: [
|
||||||
{
|
{
|
||||||
queryData: queryResponse?.data?.payload?.data?.result ?? [],
|
index: '0', // no impact
|
||||||
|
keyIndex: 0,
|
||||||
|
moveThreshold: (): void => {},
|
||||||
|
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
|
||||||
|
thresholdValue,
|
||||||
|
thresholdLabel: `${t(
|
||||||
|
'preview_chart_threshold_label',
|
||||||
|
)} (y=${thresholdValue} ${query?.unit || ''})`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
}),
|
||||||
|
[
|
||||||
|
query?.unit,
|
||||||
|
queryResponse?.data?.payload,
|
||||||
|
containerDimensions,
|
||||||
|
isDarkMode,
|
||||||
|
t,
|
||||||
|
thresholdValue,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContainer>
|
<ChartContainer>
|
||||||
@ -136,18 +153,18 @@ function ChartPreview({
|
|||||||
{queryResponse.isLoading && (
|
{queryResponse.isLoading && (
|
||||||
<Spinner size="large" tip="Loading..." height="70vh" />
|
<Spinner size="large" tip="Loading..." height="70vh" />
|
||||||
)}
|
)}
|
||||||
{chartDataSet && !queryResponse.isError && (
|
{chartData && !queryResponse.isError && (
|
||||||
<GridPanelSwitch
|
<div ref={graphRef} style={{ height: '100%' }}>
|
||||||
panelType={graphType}
|
<GridPanelSwitch
|
||||||
title={name}
|
options={options}
|
||||||
data={chartDataSet.data}
|
panelType={graphType}
|
||||||
isStacked
|
data={chartData}
|
||||||
name={name || 'Chart Preview'}
|
name={name || 'Chart Preview'}
|
||||||
staticLine={staticLine}
|
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
||||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
query={query || initialQueriesMap.metrics}
|
||||||
query={query || initialQueriesMap.metrics}
|
yAxisUnit={query?.unit}
|
||||||
yAxisUnit={query?.unit}
|
/>
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
|
@ -5,12 +5,16 @@ function PromqlSection(): JSX.Element {
|
|||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PromQLQueryBuilder
|
<>
|
||||||
key="A"
|
{currentQuery.promql.map((query, index) => (
|
||||||
queryIndex={0}
|
<PromQLQueryBuilder
|
||||||
queryData={currentQuery.promql[0]}
|
key={query.name}
|
||||||
deletable={false}
|
queryIndex={index}
|
||||||
/>
|
queryData={query}
|
||||||
|
deletable={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import { DefaultOptionType } from 'antd/es/select';
|
||||||
import {
|
import {
|
||||||
getCategoryByOptionId,
|
getCategoryByOptionId,
|
||||||
getCategorySelectOptionByName,
|
getCategorySelectOptionByName,
|
||||||
@ -28,6 +29,7 @@ function RuleOptions({
|
|||||||
alertDef,
|
alertDef,
|
||||||
setAlertDef,
|
setAlertDef,
|
||||||
queryCategory,
|
queryCategory,
|
||||||
|
queryOptions,
|
||||||
}: RuleOptionsProps): JSX.Element {
|
}: RuleOptionsProps): JSX.Element {
|
||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
@ -44,6 +46,18 @@ function RuleOptions({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeSelectedQueryName = (value: string | unknown): void => {
|
||||||
|
if (typeof value !== 'string') return;
|
||||||
|
|
||||||
|
setAlertDef({
|
||||||
|
...alertDef,
|
||||||
|
condition: {
|
||||||
|
...alertDef.condition,
|
||||||
|
selectedQueryName: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderCompareOps = (): JSX.Element => (
|
const renderCompareOps = (): JSX.Element => (
|
||||||
<InlineSelect
|
<InlineSelect
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
@ -122,16 +136,38 @@ function RuleOptions({
|
|||||||
const renderThresholdRuleOpts = (): JSX.Element => (
|
const renderThresholdRuleOpts = (): JSX.Element => (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
{t('text_condition1')}
|
||||||
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
options={queryOptions}
|
||||||
|
placeholder={t('selected_query_placeholder')}
|
||||||
|
value={alertDef.condition.selectedQueryName}
|
||||||
|
onChange={onChangeSelectedQueryName}
|
||||||
|
/>
|
||||||
|
<Typography.Text>is</Typography.Text>
|
||||||
|
{renderCompareOps()} {t('text_condition2')} {renderThresholdMatchOpts()}{' '}
|
||||||
|
{t('text_condition3')} {renderEvalWindows()}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderPromRuleOptions = (): JSX.Element => (
|
const renderPromRuleOptions = (): JSX.Element => (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
{t('text_condition1')}
|
||||||
{renderPromMatchOpts()}
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
options={queryOptions}
|
||||||
|
placeholder={t('selected_query_placeholder')}
|
||||||
|
value={alertDef.condition.selectedQueryName}
|
||||||
|
onChange={onChangeSelectedQueryName}
|
||||||
|
/>
|
||||||
|
<Typography.Text>is</Typography.Text>
|
||||||
|
{renderCompareOps()} {t('text_condition2')} {renderPromMatchOpts()}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
@ -172,7 +208,7 @@ function RuleOptions({
|
|||||||
? renderPromRuleOptions()
|
? renderPromRuleOptions()
|
||||||
: renderThresholdRuleOpts()}
|
: renderThresholdRuleOpts()}
|
||||||
|
|
||||||
<Space align="start">
|
<Space direction="horizontal" align="center">
|
||||||
<Form.Item noStyle name={['condition', 'target']}>
|
<Form.Item noStyle name={['condition', 'target']}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
addonBefore={t('field_threshold')}
|
addonBefore={t('field_threshold')}
|
||||||
@ -183,7 +219,7 @@ function RuleOptions({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item noStyle>
|
||||||
<Select
|
<Select
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
allowClear
|
allowClear
|
||||||
@ -204,5 +240,6 @@ interface RuleOptionsProps {
|
|||||||
alertDef: AlertDef;
|
alertDef: AlertDef;
|
||||||
setAlertDef: (a: AlertDef) => void;
|
setAlertDef: (a: AlertDef) => void;
|
||||||
queryCategory: EQueryType;
|
queryCategory: EQueryType;
|
||||||
|
queryOptions: DefaultOptionType[];
|
||||||
}
|
}
|
||||||
export default RuleOptions;
|
export default RuleOptions;
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import { Col, FormInstance, Modal, Tooltip, Typography } from 'antd';
|
import {
|
||||||
|
Col,
|
||||||
|
FormInstance,
|
||||||
|
Modal,
|
||||||
|
SelectProps,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} 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 { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
@ -44,6 +51,7 @@ import {
|
|||||||
StyledLeftContainer,
|
StyledLeftContainer,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import UserGuide from './UserGuide';
|
import UserGuide from './UserGuide';
|
||||||
|
import { getSelectedQueryOptions } from './utils';
|
||||||
|
|
||||||
function FormAlertRules({
|
function FormAlertRules({
|
||||||
alertType,
|
alertType,
|
||||||
@ -80,6 +88,20 @@ function FormAlertRules({
|
|||||||
initialValue,
|
initialValue,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const queryOptions = useMemo(() => {
|
||||||
|
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
|
||||||
|
[EQueryType.QUERY_BUILDER]: () => [
|
||||||
|
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
|
||||||
|
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
|
||||||
|
],
|
||||||
|
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||||
|
[EQueryType.CLICKHOUSE]: () =>
|
||||||
|
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||||
|
};
|
||||||
|
|
||||||
|
return queryConfig[currentQuery.queryType]?.() || [];
|
||||||
|
}, [currentQuery]);
|
||||||
|
|
||||||
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
||||||
|
|
||||||
useShareBuilderUrl(sq);
|
useShareBuilderUrl(sq);
|
||||||
@ -88,6 +110,18 @@ function FormAlertRules({
|
|||||||
setAlertDef(initialValue);
|
setAlertDef(initialValue);
|
||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Set selectedQueryName based on the length of queryOptions
|
||||||
|
setAlertDef((def) => ({
|
||||||
|
...def,
|
||||||
|
condition: {
|
||||||
|
...def.condition,
|
||||||
|
selectedQueryName:
|
||||||
|
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [currentQuery?.queryType, queryOptions]);
|
||||||
|
|
||||||
const onCancelHandler = useCallback(() => {
|
const onCancelHandler = useCallback(() => {
|
||||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||||
}, []);
|
}, []);
|
||||||
@ -369,21 +403,7 @@ function FormAlertRules({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderPromChartPreview = (): JSX.Element => (
|
const renderPromAndChQueryChartPreview = (): JSX.Element => (
|
||||||
<ChartPreview
|
|
||||||
headline={
|
|
||||||
<PlotTag
|
|
||||||
queryType={currentQuery.queryType}
|
|
||||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="Chart Preview"
|
|
||||||
query={stagedQuery}
|
|
||||||
alertDef={alertDef}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderChQueryChartPreview = (): JSX.Element => (
|
|
||||||
<ChartPreview
|
<ChartPreview
|
||||||
headline={
|
headline={
|
||||||
<PlotTag
|
<PlotTag
|
||||||
@ -431,9 +451,10 @@ function FormAlertRules({
|
|||||||
>
|
>
|
||||||
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||||
renderQBChartPreview()}
|
renderQBChartPreview()}
|
||||||
{currentQuery.queryType === EQueryType.PROM && renderPromChartPreview()}
|
{currentQuery.queryType === EQueryType.PROM &&
|
||||||
|
renderPromAndChQueryChartPreview()}
|
||||||
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
|
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
|
||||||
renderChQueryChartPreview()}
|
renderPromAndChQueryChartPreview()}
|
||||||
|
|
||||||
<StepContainer>
|
<StepContainer>
|
||||||
<BuilderUnitsFilter onChange={onUnitChangeHandler} />
|
<BuilderUnitsFilter onChange={onUnitChangeHandler} />
|
||||||
@ -450,6 +471,7 @@ function FormAlertRules({
|
|||||||
queryCategory={currentQuery.queryType}
|
queryCategory={currentQuery.queryType}
|
||||||
alertDef={alertDef}
|
alertDef={alertDef}
|
||||||
setAlertDef={setAlertDef}
|
setAlertDef={setAlertDef}
|
||||||
|
queryOptions={queryOptions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{renderBasicInfo()}
|
{renderBasicInfo()}
|
||||||
|
@ -59,8 +59,8 @@ export const StepHeading = styled.p`
|
|||||||
export const InlineSelect = styled(Select)`
|
export const InlineSelect = styled(Select)`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 10% !important;
|
width: 10% !important;
|
||||||
margin-left: 0.2em;
|
margin-left: 0.3em;
|
||||||
margin-right: 0.2em;
|
margin-right: 0.3em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SeveritySelect = styled(Select)`
|
export const SeveritySelect = styled(Select)`
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
|
import { SelectProps } from 'antd';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
import getStep from 'lib/getStep';
|
import getStep from 'lib/getStep';
|
||||||
|
import {
|
||||||
|
IBuilderFormula,
|
||||||
|
IBuilderQuery,
|
||||||
|
IClickHouseQuery,
|
||||||
|
IPromQLQuery,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
// toChartInterval converts eval window to chart selection time interval
|
// toChartInterval converts eval window to chart selection time interval
|
||||||
export const toChartInterval = (evalWindow: string | undefined): Time => {
|
export const toChartInterval = (evalWindow: string | undefined): Time => {
|
||||||
@ -35,3 +42,15 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => {
|
|||||||
inputFormat: 'ns',
|
inputFormat: 'ns',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSelectedQueryOptions = (
|
||||||
|
queries: Array<
|
||||||
|
IBuilderQuery | IBuilderFormula | IClickHouseQuery | IPromQLQuery
|
||||||
|
>,
|
||||||
|
): SelectProps['options'] =>
|
||||||
|
queries
|
||||||
|
.filter((query) => !query.disabled)
|
||||||
|
.map((query) => ({
|
||||||
|
label: 'queryName' in query ? query.queryName : query.name,
|
||||||
|
value: 'queryName' in query ? query.queryName : query.name,
|
||||||
|
}));
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
.graph-manager-container {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
overflow-x: scroll;
|
|
||||||
|
|
||||||
.filter-table-container {
|
|
||||||
flex-basis: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-cancel-container {
|
|
||||||
flex-basis: 20%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-cancel-button {
|
|
||||||
margin: 0 0.313rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import './GraphManager.styles.scss';
|
import './WidgetFullView.styles.scss';
|
||||||
|
|
||||||
import { Button, Input } from 'antd';
|
import { Button, Input } from 'antd';
|
||||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
@ -19,12 +19,13 @@ function GraphManager({
|
|||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
onToggleModelHandler,
|
onToggleModelHandler,
|
||||||
setGraphsVisibilityStates,
|
setGraphsVisibilityStates,
|
||||||
graphsVisibilityStates = [],
|
graphsVisibilityStates = [], // not trimed
|
||||||
lineChartRef,
|
lineChartRef,
|
||||||
parentChartRef,
|
parentChartRef,
|
||||||
|
options,
|
||||||
}: GraphManagerProps): JSX.Element {
|
}: GraphManagerProps): JSX.Element {
|
||||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
|
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
|
||||||
getDefaultTableDataSet(data),
|
getDefaultTableDataSet(options, data),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@ -32,21 +33,22 @@ function GraphManager({
|
|||||||
const checkBoxOnChangeHandler = useCallback(
|
const checkBoxOnChangeHandler = useCallback(
|
||||||
(e: CheckboxChangeEvent, index: number): void => {
|
(e: CheckboxChangeEvent, index: number): void => {
|
||||||
const newStates = [...graphsVisibilityStates];
|
const newStates = [...graphsVisibilityStates];
|
||||||
|
|
||||||
newStates[index] = e.target.checked;
|
newStates[index] = e.target.checked;
|
||||||
|
|
||||||
lineChartRef?.current?.toggleGraph(index, e.target.checked);
|
lineChartRef?.current?.toggleGraph(index, e.target.checked);
|
||||||
|
parentChartRef?.current?.toggleGraph(index, e.target.checked);
|
||||||
setGraphsVisibilityStates([...newStates]);
|
setGraphsVisibilityStates([...newStates]);
|
||||||
},
|
},
|
||||||
[graphsVisibilityStates, setGraphsVisibilityStates, lineChartRef],
|
[
|
||||||
|
graphsVisibilityStates,
|
||||||
|
lineChartRef,
|
||||||
|
parentChartRef,
|
||||||
|
setGraphsVisibilityStates,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const labelClickedHandler = useCallback(
|
const labelClickedHandler = useCallback(
|
||||||
(labelIndex: number): void => {
|
(labelIndex: number): void => {
|
||||||
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
const newGraphVisibilityStates = Array<boolean>(data.length).fill(false);
|
||||||
false,
|
|
||||||
);
|
|
||||||
newGraphVisibilityStates[labelIndex] = true;
|
newGraphVisibilityStates[labelIndex] = true;
|
||||||
|
|
||||||
newGraphVisibilityStates.forEach((state, index) => {
|
newGraphVisibilityStates.forEach((state, index) => {
|
||||||
@ -55,18 +57,13 @@ function GraphManager({
|
|||||||
});
|
});
|
||||||
setGraphsVisibilityStates(newGraphVisibilityStates);
|
setGraphsVisibilityStates(newGraphVisibilityStates);
|
||||||
},
|
},
|
||||||
[
|
[data.length, lineChartRef, parentChartRef, setGraphsVisibilityStates],
|
||||||
data.datasets.length,
|
|
||||||
setGraphsVisibilityStates,
|
|
||||||
lineChartRef,
|
|
||||||
parentChartRef,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = getGraphManagerTableColumns({
|
const columns = getGraphManagerTableColumns({
|
||||||
data,
|
tableDataSet,
|
||||||
checkBoxOnChangeHandler,
|
checkBoxOnChangeHandler,
|
||||||
graphVisibilityState: graphsVisibilityStates || [],
|
graphVisibilityState: graphsVisibilityStates,
|
||||||
labelClickedHandler,
|
labelClickedHandler,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
});
|
});
|
||||||
@ -87,7 +84,7 @@ function GraphManager({
|
|||||||
|
|
||||||
const saveHandler = useCallback((): void => {
|
const saveHandler = useCallback((): void => {
|
||||||
saveLegendEntriesToLocalStorage({
|
saveLegendEntriesToLocalStorage({
|
||||||
data,
|
options,
|
||||||
graphVisibilityState: graphsVisibilityStates || [],
|
graphVisibilityState: graphsVisibilityStates || [],
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
@ -97,34 +94,49 @@ function GraphManager({
|
|||||||
if (onToggleModelHandler) {
|
if (onToggleModelHandler) {
|
||||||
onToggleModelHandler();
|
onToggleModelHandler();
|
||||||
}
|
}
|
||||||
}, [data, graphsVisibilityStates, name, notifications, onToggleModelHandler]);
|
}, [
|
||||||
|
graphsVisibilityStates,
|
||||||
|
name,
|
||||||
|
notifications,
|
||||||
|
onToggleModelHandler,
|
||||||
|
options,
|
||||||
|
]);
|
||||||
|
|
||||||
const dataSource = tableDataSet.filter((item) => item.show);
|
const dataSource = tableDataSet.filter(
|
||||||
|
(item, index) => index !== 0 && item.show,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="graph-manager-container">
|
<div className="graph-manager-container">
|
||||||
<div className="filter-table-container">
|
<div className="graph-manager-header">
|
||||||
<Input onChange={filterHandler} placeholder="Filter Series" />
|
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||||
|
<div className="save-cancel-container">
|
||||||
|
<span className="save-cancel-button">
|
||||||
|
<Button type="default" onClick={onToggleModelHandler}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
<span className="save-cancel-button">
|
||||||
|
<Button type="primary" onClick={saveHandler}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="legends-list-container">
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
rowKey="index"
|
rowKey="index"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: 240 }}
|
style={{
|
||||||
|
maxHeight: 200,
|
||||||
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'auto',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="save-cancel-container">
|
|
||||||
<span className="save-cancel-button">
|
|
||||||
<Button type="default" onClick={onToggleModelHandler}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
<span className="save-cancel-button">
|
|
||||||
<Button onClick={saveHandler} type="primary">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,11 @@ function CustomCheckBox({
|
|||||||
graphVisibilityState = [],
|
graphVisibilityState = [],
|
||||||
checkBoxOnChangeHandler,
|
checkBoxOnChangeHandler,
|
||||||
}: CheckBoxProps): JSX.Element {
|
}: CheckBoxProps): JSX.Element {
|
||||||
const { datasets } = data;
|
|
||||||
|
|
||||||
const onChangeHandler = (e: CheckboxChangeEvent): void => {
|
const onChangeHandler = (e: CheckboxChangeEvent): void => {
|
||||||
checkBoxOnChangeHandler(e, index);
|
checkBoxOnChangeHandler(e, index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const color = datasets[index]?.borderColor?.toString() || grey[0];
|
const color = data[index]?.stroke?.toString() || grey[0];
|
||||||
|
|
||||||
const isChecked = graphVisibilityState[index] || false;
|
const isChecked = graphVisibilityState[index] || false;
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import Label from './Label';
|
|||||||
export const getLabel = (
|
export const getLabel = (
|
||||||
labelClickedHandler: (labelIndex: number) => void,
|
labelClickedHandler: (labelIndex: number) => void,
|
||||||
): ColumnType<DataSetProps> => ({
|
): ColumnType<DataSetProps> => ({
|
||||||
render: (label, record): JSX.Element => (
|
render: (label: string, record): JSX.Element => (
|
||||||
<Label
|
<Label
|
||||||
label={label}
|
label={label}
|
||||||
labelIndex={record.index}
|
labelIndex={record.index}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
import { ColumnType } from 'antd/es/table';
|
import { ColumnType } from 'antd/es/table';
|
||||||
import { ChartData } from 'chart.js';
|
|
||||||
|
|
||||||
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
|
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
|
||||||
import { DataSetProps } from '../types';
|
import { DataSetProps, ExtendedChartDataset } from '../types';
|
||||||
import { getGraphManagerTableHeaderTitle } from '../utils';
|
import { getGraphManagerTableHeaderTitle } from '../utils';
|
||||||
import CustomCheckBox from './CustomCheckBox';
|
import CustomCheckBox from './CustomCheckBox';
|
||||||
import { getLabel } from './GetLabel';
|
import { getLabel } from './GetLabel';
|
||||||
|
|
||||||
export const getGraphManagerTableColumns = ({
|
export const getGraphManagerTableColumns = ({
|
||||||
data,
|
tableDataSet,
|
||||||
checkBoxOnChangeHandler,
|
checkBoxOnChangeHandler,
|
||||||
graphVisibilityState,
|
graphVisibilityState,
|
||||||
labelClickedHandler,
|
labelClickedHandler,
|
||||||
@ -22,7 +21,7 @@ export const getGraphManagerTableColumns = ({
|
|||||||
key: ColumnsKeyAndDataIndex.Index,
|
key: ColumnsKeyAndDataIndex.Index,
|
||||||
render: (_: string, record: DataSetProps): JSX.Element => (
|
render: (_: string, record: DataSetProps): JSX.Element => (
|
||||||
<CustomCheckBox
|
<CustomCheckBox
|
||||||
data={data}
|
data={tableDataSet}
|
||||||
index={record.index}
|
index={record.index}
|
||||||
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
|
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
|
||||||
graphVisibilityState={graphVisibilityState}
|
graphVisibilityState={graphVisibilityState}
|
||||||
@ -75,7 +74,7 @@ export const getGraphManagerTableColumns = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface GetGraphManagerTableColumnsProps {
|
interface GetGraphManagerTableColumnsProps {
|
||||||
data: ChartData;
|
tableDataSet: ExtendedChartDataset[];
|
||||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||||
labelClickedHandler: (labelIndex: number) => void;
|
labelClickedHandler: (labelIndex: number) => void;
|
||||||
graphVisibilityState: boolean[];
|
graphVisibilityState: boolean[];
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
|
||||||
import { LabelContainer } from '../styles';
|
import { LabelContainer } from '../styles';
|
||||||
import { LabelProps } from '../types';
|
import { LabelProps } from '../types';
|
||||||
import { getAbbreviatedLabel } from '../utils';
|
import { getAbbreviatedLabel } from '../utils';
|
||||||
@ -7,12 +9,18 @@ function Label({
|
|||||||
labelIndex,
|
labelIndex,
|
||||||
label,
|
label,
|
||||||
}: LabelProps): JSX.Element {
|
}: LabelProps): JSX.Element {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const onClickHandler = (): void => {
|
const onClickHandler = (): void => {
|
||||||
labelClickedHandler(labelIndex);
|
labelClickedHandler(labelIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LabelContainer type="button" onClick={onClickHandler}>
|
<LabelContainer
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
type="button"
|
||||||
|
onClick={onClickHandler}
|
||||||
|
>
|
||||||
{getAbbreviatedLabel(label)}
|
{getAbbreviatedLabel(label)}
|
||||||
</LabelContainer>
|
</LabelContainer>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
.full-view-container {
|
||||||
|
height: 80vh;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
.full-view-header-container {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container {
|
||||||
|
height: calc(60% - 40px);
|
||||||
|
min-height: 300px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 16px 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-manager-container {
|
||||||
|
height: calc(40% - 40px);
|
||||||
|
|
||||||
|
.graph-manager-header {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legends-list-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-cancel-container {
|
||||||
|
flex-basis: 20%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-cancel-button {
|
||||||
|
margin: 0 0.313rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
import './WidgetFullView.styles.scss';
|
||||||
|
|
||||||
|
import { SyncOutlined } from '@ant-design/icons';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
@ -10,16 +13,20 @@ import {
|
|||||||
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 { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
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 { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||||
import GraphManager from './GraphManager';
|
import GraphManager from './GraphManager';
|
||||||
|
// import GraphManager from './GraphManager';
|
||||||
import { GraphContainer, TimeContainer } from './styles';
|
import { GraphContainer, TimeContainer } from './styles';
|
||||||
import { FullViewProps } from './types';
|
import { FullViewProps } from './types';
|
||||||
|
|
||||||
@ -33,14 +40,18 @@ function FullView({
|
|||||||
isDependedDataLoaded = false,
|
isDependedDataLoaded = false,
|
||||||
graphsVisibilityStates,
|
graphsVisibilityStates,
|
||||||
onToggleModelHandler,
|
onToggleModelHandler,
|
||||||
setGraphsVisibilityStates,
|
|
||||||
parentChartRef,
|
parentChartRef,
|
||||||
|
setGraphsVisibilityStates,
|
||||||
}: 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 fullViewRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
|
||||||
|
|
||||||
const { selectedDashboard } = useDashboard();
|
const { selectedDashboard } = useDashboard();
|
||||||
|
|
||||||
const getSelectedTime = useCallback(
|
const getSelectedTime = useCallback(
|
||||||
@ -49,7 +60,7 @@ function FullView({
|
|||||||
[widget],
|
[widget],
|
||||||
);
|
);
|
||||||
|
|
||||||
const lineChartRef = useRef<ToggleGraphProps>();
|
const fullViewChartRef = useRef<ToggleGraphProps>();
|
||||||
|
|
||||||
const [selectedTime, setSelectedTime] = useState<timePreferance>({
|
const [selectedTime, setSelectedTime] = useState<timePreferance>({
|
||||||
name: getSelectedTime()?.name || '',
|
name: getSelectedTime()?.name || '',
|
||||||
@ -77,80 +88,110 @@ function FullView({
|
|||||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||||
});
|
});
|
||||||
|
|
||||||
const chartDataSet = useMemo(
|
const chartData = getUPlotChartData(response?.data?.payload);
|
||||||
() =>
|
|
||||||
getChartData({
|
const isDarkMode = useIsDarkMode();
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
queryData: response?.data?.payload?.data?.result || [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[response],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!response.isFetching && lineChartRef.current) {
|
if (!response.isFetching && fullViewRef.current) {
|
||||||
graphsVisibilityStates?.forEach((e, i) => {
|
const width = fullViewRef.current?.clientWidth
|
||||||
lineChartRef?.current?.toggleGraph(i, e);
|
? fullViewRef.current.clientWidth - 45
|
||||||
parentChartRef?.current?.toggleGraph(i, e);
|
: 700;
|
||||||
|
|
||||||
|
const height = fullViewRef.current?.clientWidth
|
||||||
|
? fullViewRef.current.clientHeight
|
||||||
|
: 300;
|
||||||
|
|
||||||
|
const newChartOptions = getUPlotChartOptions({
|
||||||
|
yAxisUnit: yAxisUnit || '',
|
||||||
|
apiResponse: response.data?.payload,
|
||||||
|
dimensions: {
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
isDarkMode,
|
||||||
|
onDragSelect,
|
||||||
|
graphsVisibilityStates,
|
||||||
|
setGraphsVisibilityStates,
|
||||||
|
thresholds: widget.thresholds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setChartOptions(newChartOptions);
|
||||||
}
|
}
|
||||||
}, [graphsVisibilityStates, parentChartRef, response.isFetching]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [response.isFetching, graphsVisibilityStates, fullViewRef.current]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
graphsVisibilityStates?.forEach((e, i) => {
|
||||||
|
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||||
|
parentChartRef?.current?.toggleGraph(i, e);
|
||||||
|
});
|
||||||
|
}, [graphsVisibilityStates, parentChartRef]);
|
||||||
|
|
||||||
if (response.isFetching) {
|
if (response.isFetching) {
|
||||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="full-view-container">
|
||||||
{fullViewOptions && (
|
<div className="full-view-header-container">
|
||||||
<TimeContainer $panelType={widget.panelTypes}>
|
{fullViewOptions && (
|
||||||
<TimePreference
|
<TimeContainer $panelType={widget.panelTypes}>
|
||||||
selectedTime={selectedTime}
|
<TimePreference
|
||||||
setSelectedTime={setSelectedTime}
|
selectedTime={selectedTime}
|
||||||
/>
|
setSelectedTime={setSelectedTime}
|
||||||
<Button
|
/>
|
||||||
onClick={(): void => {
|
<Button
|
||||||
response.refetch();
|
style={{
|
||||||
}}
|
marginLeft: '4px',
|
||||||
type="primary"
|
}}
|
||||||
|
onClick={(): void => {
|
||||||
|
response.refetch();
|
||||||
|
}}
|
||||||
|
type="primary"
|
||||||
|
icon={<SyncOutlined />}
|
||||||
|
/>
|
||||||
|
</TimeContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="graph-container" ref={fullViewRef}>
|
||||||
|
{chartOptions && (
|
||||||
|
<GraphContainer
|
||||||
|
style={{ height: '90%' }}
|
||||||
|
isGraphLegendToggleAvailable={canModifyChart}
|
||||||
>
|
>
|
||||||
Refresh
|
<GridPanelSwitch
|
||||||
</Button>
|
panelType={widget.panelTypes}
|
||||||
</TimeContainer>
|
data={chartData}
|
||||||
)}
|
options={chartOptions}
|
||||||
|
onClickHandler={onClickHandler}
|
||||||
|
name={name}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
onDragSelect={onDragSelect}
|
||||||
|
panelData={response.data?.payload.data.newResult.data.result || []}
|
||||||
|
query={widget.query}
|
||||||
|
ref={fullViewChartRef}
|
||||||
|
thresholds={widget.thresholds}
|
||||||
|
/>
|
||||||
|
</GraphContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<GraphContainer isGraphLegendToggleAvailable={canModifyChart}>
|
{canModifyChart && chartOptions && (
|
||||||
<GridPanelSwitch
|
|
||||||
panelType={widget.panelTypes}
|
|
||||||
data={chartDataSet.data}
|
|
||||||
isStacked={widget.isStacked}
|
|
||||||
opacity={widget.opacity}
|
|
||||||
title={widget.title}
|
|
||||||
onClickHandler={onClickHandler}
|
|
||||||
name={name}
|
|
||||||
yAxisUnit={yAxisUnit}
|
|
||||||
onDragSelect={onDragSelect}
|
|
||||||
panelData={response.data?.payload.data.newResult.data.result || []}
|
|
||||||
query={widget.query}
|
|
||||||
ref={lineChartRef}
|
|
||||||
/>
|
|
||||||
</GraphContainer>
|
|
||||||
|
|
||||||
{canModifyChart && (
|
|
||||||
<GraphManager
|
<GraphManager
|
||||||
data={chartDataSet.data}
|
data={chartData}
|
||||||
name={name}
|
name={name}
|
||||||
|
options={chartOptions}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
onToggleModelHandler={onToggleModelHandler}
|
onToggleModelHandler={onToggleModelHandler}
|
||||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
||||||
graphsVisibilityStates={graphsVisibilityStates}
|
graphsVisibilityStates={graphsVisibilityStates}
|
||||||
lineChartRef={lineChartRef}
|
lineChartRef={fullViewChartRef}
|
||||||
parentChartRef={parentChartRef}
|
parentChartRef={parentChartRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,10 +31,11 @@ export const GraphContainer = styled.div<GraphContainerProps>`
|
|||||||
isGraphLegendToggleAvailable ? '50%' : '100%'};
|
isGraphLegendToggleAvailable ? '50%' : '100%'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LabelContainer = styled.button`
|
export const LabelContainer = styled.button<{ isDarkMode?: boolean }>`
|
||||||
max-width: 18.75rem;
|
max-width: 18.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: ${themeColors.white};
|
color: ${(props): string =>
|
||||||
|
props.isDarkMode ? themeColors.white : themeColors.black};
|
||||||
`;
|
`;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
import { ChartData, ChartDataset } from 'chart.js';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
|
import { UplotProps } from 'components/Uplot/Uplot';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { MutableRefObject } from 'react';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
|
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
export interface DataSetProps {
|
export interface DataSetProps {
|
||||||
index: number;
|
index: number;
|
||||||
@ -22,12 +24,13 @@ export interface LegendEntryProps {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedChartDataset = ChartDataset & {
|
export type ExtendedChartDataset = uPlot.Series & {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
sum: number;
|
sum: number;
|
||||||
avg: number;
|
avg: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PanelTypeAndGraphManagerVisibilityProps = Record<
|
export type PanelTypeAndGraphManagerVisibilityProps = Record<
|
||||||
@ -44,22 +47,22 @@ export interface LabelProps {
|
|||||||
export interface FullViewProps {
|
export interface FullViewProps {
|
||||||
widget: Widgets;
|
widget: Widgets;
|
||||||
fullViewOptions?: boolean;
|
fullViewOptions?: boolean;
|
||||||
onClickHandler?: GraphOnClickHandler;
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
isDependedDataLoaded?: boolean;
|
isDependedDataLoaded?: boolean;
|
||||||
graphsVisibilityStates?: boolean[];
|
graphsVisibilityStates?: boolean[];
|
||||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||||
setGraphsVisibilityStates: (graphsVisibilityStates: boolean[]) => void;
|
setGraphsVisibilityStates: Dispatch<SetStateAction<boolean[]>>;
|
||||||
parentChartRef: GraphManagerProps['lineChartRef'];
|
parentChartRef: GraphManagerProps['lineChartRef'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphManagerProps {
|
export interface GraphManagerProps extends UplotProps {
|
||||||
data: ChartData;
|
|
||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
onToggleModelHandler?: () => void;
|
onToggleModelHandler?: () => void;
|
||||||
|
options: uPlot.Options;
|
||||||
setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates'];
|
setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates'];
|
||||||
graphsVisibilityStates: FullViewProps['graphsVisibilityStates'];
|
graphsVisibilityStates: FullViewProps['graphsVisibilityStates'];
|
||||||
lineChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
|
lineChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
|
||||||
@ -67,14 +70,14 @@ export interface GraphManagerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CheckBoxProps {
|
export interface CheckBoxProps {
|
||||||
data: ChartData;
|
data: ExtendedChartDataset[];
|
||||||
index: number;
|
index: number;
|
||||||
graphVisibilityState: boolean[];
|
graphVisibilityState: boolean[];
|
||||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SaveLegendEntriesToLocalStoreProps {
|
export interface SaveLegendEntriesToLocalStoreProps {
|
||||||
data: ChartData;
|
options: uPlot.Options;
|
||||||
graphVisibilityState: boolean[];
|
graphVisibilityState: boolean[];
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ChartData, ChartDataset } from 'chart.js';
|
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExtendedChartDataset,
|
ExtendedChartDataset,
|
||||||
@ -21,33 +21,23 @@ function convertToTwoDecimalsOrZero(value: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getDefaultTableDataSet = (
|
export const getDefaultTableDataSet = (
|
||||||
data: ChartData,
|
options: uPlot.Options,
|
||||||
|
data: uPlot.AlignedData,
|
||||||
): ExtendedChartDataset[] =>
|
): ExtendedChartDataset[] =>
|
||||||
data.datasets.map(
|
options.series.map(
|
||||||
(item: ChartDataset): ExtendedChartDataset => {
|
(item: uPlot.Series, index: number): ExtendedChartDataset => ({
|
||||||
if (item.data.length === 0) {
|
...item,
|
||||||
return {
|
index,
|
||||||
...item,
|
show: true,
|
||||||
show: true,
|
sum: convertToTwoDecimalsOrZero(
|
||||||
sum: 0,
|
(data[index] as number[]).reduce((a, b) => a + b, 0),
|
||||||
avg: 0,
|
),
|
||||||
max: 0,
|
avg: convertToTwoDecimalsOrZero(
|
||||||
min: 0,
|
(data[index] as number[]).reduce((a, b) => a + b, 0) / data[index].length,
|
||||||
};
|
),
|
||||||
}
|
max: convertToTwoDecimalsOrZero(Math.max(...(data[index] as number[]))),
|
||||||
return {
|
min: convertToTwoDecimalsOrZero(Math.min(...(data[index] as number[]))),
|
||||||
...item,
|
}),
|
||||||
show: true,
|
|
||||||
sum: convertToTwoDecimalsOrZero(
|
|
||||||
(item.data as number[]).reduce((a, b) => a + b, 0),
|
|
||||||
),
|
|
||||||
avg: convertToTwoDecimalsOrZero(
|
|
||||||
(item.data as number[]).reduce((a, b) => a + b, 0) / item.data.length,
|
|
||||||
),
|
|
||||||
max: convertToTwoDecimalsOrZero(Math.max(...(item.data as number[]))),
|
|
||||||
min: convertToTwoDecimalsOrZero(Math.min(...(item.data as number[]))),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getAbbreviatedLabel = (label: string): string => {
|
export const getAbbreviatedLabel = (label: string): string => {
|
||||||
@ -58,22 +48,24 @@ export const getAbbreviatedLabel = (label: string): string => {
|
|||||||
return newLabel;
|
return newLabel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showAllDataSet = (data: ChartData): LegendEntryProps[] =>
|
export const showAllDataSet = (options: uPlot.Options): LegendEntryProps[] =>
|
||||||
data.datasets.map(
|
options.series
|
||||||
(item): LegendEntryProps => ({
|
.map(
|
||||||
label: item.label || '',
|
(item): LegendEntryProps => ({
|
||||||
show: true,
|
label: item.label || '',
|
||||||
}),
|
show: true,
|
||||||
);
|
}),
|
||||||
|
)
|
||||||
|
.filter((_, index) => index !== 0);
|
||||||
|
|
||||||
export const saveLegendEntriesToLocalStorage = ({
|
export const saveLegendEntriesToLocalStorage = ({
|
||||||
data,
|
options,
|
||||||
graphVisibilityState,
|
graphVisibilityState,
|
||||||
name,
|
name,
|
||||||
}: SaveLegendEntriesToLocalStoreProps): void => {
|
}: SaveLegendEntriesToLocalStoreProps): void => {
|
||||||
const newLegendEntry = {
|
const newLegendEntry = {
|
||||||
name,
|
name,
|
||||||
dataIndex: data.datasets.map(
|
dataIndex: options.series.map(
|
||||||
(item, index): LegendEntryProps => ({
|
(item, index): LegendEntryProps => ({
|
||||||
label: item.label || '',
|
label: item.label || '',
|
||||||
show: graphVisibilityState[index],
|
show: graphVisibilityState[index],
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import { mockTestData } from './__mock__/mockChartData';
|
|
||||||
import { mocklegendEntryResult } from './__mock__/mockLegendEntryData';
|
|
||||||
import { showAllDataSet } from './FullView/utils';
|
|
||||||
import { getGraphVisibilityStateOnDataChange } from './utils';
|
|
||||||
|
|
||||||
describe('getGraphVisibilityStateOnDataChange', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const localStorageMock = {
|
|
||||||
getItem: jest.fn(),
|
|
||||||
};
|
|
||||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the correct visibility state and legend entry', () => {
|
|
||||||
// Mock the localStorage behavior
|
|
||||||
const mockLocalStorageData = [
|
|
||||||
{
|
|
||||||
name: 'exampleexpanded',
|
|
||||||
dataIndex: [
|
|
||||||
{ label: 'customer', show: true },
|
|
||||||
{ label: 'demo-app', show: false },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
jest
|
|
||||||
.spyOn(window.localStorage, 'getItem')
|
|
||||||
.mockReturnValue(JSON.stringify(mockLocalStorageData));
|
|
||||||
|
|
||||||
const result1 = getGraphVisibilityStateOnDataChange({
|
|
||||||
data: mockTestData,
|
|
||||||
isExpandedName: true,
|
|
||||||
name: 'example',
|
|
||||||
});
|
|
||||||
expect(result1.graphVisibilityStates).toEqual([true, false]);
|
|
||||||
expect(result1.legendEntry).toEqual(mocklegendEntryResult);
|
|
||||||
|
|
||||||
const result2 = getGraphVisibilityStateOnDataChange({
|
|
||||||
data: mockTestData,
|
|
||||||
isExpandedName: false,
|
|
||||||
name: 'example',
|
|
||||||
});
|
|
||||||
expect(result2.graphVisibilityStates).toEqual(
|
|
||||||
Array(mockTestData.datasets.length).fill(true),
|
|
||||||
);
|
|
||||||
expect(result2.legendEntry).toEqual(showAllDataSet(mockTestData));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return default values if localStorage data is not available', () => {
|
|
||||||
// Mock the localStorage behavior to return null
|
|
||||||
jest.spyOn(window.localStorage, 'getItem').mockReturnValue(null);
|
|
||||||
|
|
||||||
const result = getGraphVisibilityStateOnDataChange({
|
|
||||||
data: mockTestData,
|
|
||||||
isExpandedName: true,
|
|
||||||
name: 'example',
|
|
||||||
});
|
|
||||||
expect(result.graphVisibilityStates).toEqual(
|
|
||||||
Array(mockTestData.datasets.length).fill(true),
|
|
||||||
);
|
|
||||||
expect(result.legendEntry).toEqual(showAllDataSet(mockTestData));
|
|
||||||
});
|
|
||||||
});
|
|
@ -25,21 +25,22 @@ import { v4 } from 'uuid';
|
|||||||
|
|
||||||
import WidgetHeader from '../WidgetHeader';
|
import WidgetHeader from '../WidgetHeader';
|
||||||
import FullView from './FullView';
|
import FullView from './FullView';
|
||||||
import { FullViewContainer, Modal } from './styles';
|
import { Modal } from './styles';
|
||||||
import { WidgetGraphComponentProps } from './types';
|
import { WidgetGraphComponentProps } from './types';
|
||||||
import { getGraphVisibilityStateOnDataChange } from './utils';
|
import { getGraphVisibilityStateOnDataChange } from './utils';
|
||||||
|
|
||||||
function WidgetGraphComponent({
|
function WidgetGraphComponent({
|
||||||
data,
|
|
||||||
widget,
|
widget,
|
||||||
queryResponse,
|
queryResponse,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
name,
|
name,
|
||||||
onDragSelect,
|
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
threshold,
|
threshold,
|
||||||
headerMenuList,
|
headerMenuList,
|
||||||
isWarning,
|
isWarning,
|
||||||
|
data,
|
||||||
|
options,
|
||||||
|
onDragSelect,
|
||||||
}: WidgetGraphComponentProps): JSX.Element {
|
}: WidgetGraphComponentProps): JSX.Element {
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
const [modal, setModal] = useState<boolean>(false);
|
const [modal, setModal] = useState<boolean>(false);
|
||||||
@ -48,15 +49,16 @@ function WidgetGraphComponent({
|
|||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const lineChartRef = useRef<ToggleGraphProps>();
|
const lineChartRef = useRef<ToggleGraphProps>();
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
|
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getGraphVisibilityStateOnDataChange({
|
getGraphVisibilityStateOnDataChange({
|
||||||
data,
|
options,
|
||||||
isExpandedName: true,
|
isExpandedName: true,
|
||||||
name,
|
name,
|
||||||
}),
|
}),
|
||||||
[data, name],
|
[options, name],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
|
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
|
||||||
@ -64,6 +66,7 @@ function WidgetGraphComponent({
|
|||||||
>(localStoredVisibilityStates);
|
>(localStoredVisibilityStates);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setGraphsVisibilityStates(localStoredVisibilityStates);
|
||||||
if (!lineChartRef.current) return;
|
if (!lineChartRef.current) return;
|
||||||
|
|
||||||
localStoredVisibilityStates.forEach((state, index) => {
|
localStoredVisibilityStates.forEach((state, index) => {
|
||||||
@ -74,9 +77,10 @@ function WidgetGraphComponent({
|
|||||||
|
|
||||||
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||||
|
|
||||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
const featureResponse = useSelector<AppState, AppReducer['featureResponse']>(
|
||||||
(state) => state.app,
|
(state) => state.app.featureResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onToggleModal = useCallback(
|
const onToggleModal = useCallback(
|
||||||
(func: Dispatch<SetStateAction<boolean>>) => {
|
(func: Dispatch<SetStateAction<boolean>>) => {
|
||||||
func((value) => !value);
|
func((value) => !value);
|
||||||
@ -133,7 +137,7 @@ function WidgetGraphComponent({
|
|||||||
i: uuid,
|
i: uuid,
|
||||||
w: 6,
|
w: 6,
|
||||||
x: 0,
|
x: 0,
|
||||||
h: 2,
|
h: 3,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -186,8 +190,22 @@ function WidgetGraphComponent({
|
|||||||
onToggleModal(setModal);
|
onToggleModal(setModal);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (queryResponse.isLoading || queryResponse.status === 'idle') {
|
||||||
|
return (
|
||||||
|
<Skeleton
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
padding: '16px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<div
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
onMouseOver={(): void => {
|
onMouseOver={(): void => {
|
||||||
setHovered(true);
|
setHovered(true);
|
||||||
}}
|
}}
|
||||||
@ -200,6 +218,7 @@ function WidgetGraphComponent({
|
|||||||
onBlur={(): void => {
|
onBlur={(): void => {
|
||||||
setHovered(false);
|
setHovered(false);
|
||||||
}}
|
}}
|
||||||
|
id={name}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
@ -214,7 +233,7 @@ function WidgetGraphComponent({
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="View"
|
title={widget?.title || 'View'}
|
||||||
footer={[]}
|
footer={[]}
|
||||||
centered
|
centered
|
||||||
open={modal}
|
open={modal}
|
||||||
@ -222,17 +241,16 @@ function WidgetGraphComponent({
|
|||||||
width="85%"
|
width="85%"
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<FullViewContainer>
|
<FullView
|
||||||
<FullView
|
name={`${name}expanded`}
|
||||||
name={`${name}expanded`}
|
widget={widget}
|
||||||
widget={widget}
|
yAxisUnit={widget.yAxisUnit}
|
||||||
yAxisUnit={widget.yAxisUnit}
|
onToggleModelHandler={onToggleModelHandler}
|
||||||
graphsVisibilityStates={graphsVisibilityStates}
|
parentChartRef={lineChartRef}
|
||||||
onToggleModelHandler={onToggleModelHandler}
|
onDragSelect={onDragSelect}
|
||||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
||||||
parentChartRef={lineChartRef}
|
graphsVisibilityStates={graphsVisibilityStates}
|
||||||
/>
|
/>
|
||||||
</FullViewContainer>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div className="drag-handle">
|
<div className="drag-handle">
|
||||||
@ -252,29 +270,28 @@ function WidgetGraphComponent({
|
|||||||
</div>
|
</div>
|
||||||
{queryResponse.isLoading && <Skeleton />}
|
{queryResponse.isLoading && <Skeleton />}
|
||||||
{queryResponse.isSuccess && (
|
{queryResponse.isSuccess && (
|
||||||
<GridPanelSwitch
|
<div style={{ height: '90%' }} ref={graphRef}>
|
||||||
panelType={widget.panelTypes}
|
<GridPanelSwitch
|
||||||
data={data}
|
panelType={widget.panelTypes}
|
||||||
isStacked={widget.isStacked}
|
data={data}
|
||||||
opacity={widget.opacity}
|
name={name}
|
||||||
title={' '}
|
ref={lineChartRef}
|
||||||
name={name}
|
options={options}
|
||||||
yAxisUnit={widget.yAxisUnit}
|
yAxisUnit={widget.yAxisUnit}
|
||||||
onClickHandler={onClickHandler}
|
onClickHandler={onClickHandler}
|
||||||
onDragSelect={onDragSelect}
|
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
|
||||||
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
|
query={widget.query}
|
||||||
query={widget.query}
|
thresholds={widget.thresholds}
|
||||||
ref={lineChartRef}
|
/>
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetGraphComponent.defaultProps = {
|
WidgetGraphComponent.defaultProps = {
|
||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
setLayout: undefined,
|
setLayout: undefined,
|
||||||
onDragSelect: undefined,
|
|
||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
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 { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||||
import isEmpty from 'lodash-es/isEmpty';
|
import isEmpty from 'lodash-es/isEmpty';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import _noop from 'lodash-es/noop';
|
||||||
import { memo, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useInView } from 'react-intersection-observer';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -20,30 +23,30 @@ import WidgetGraphComponent from './WidgetGraphComponent';
|
|||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
widget,
|
widget,
|
||||||
name,
|
name,
|
||||||
onClickHandler,
|
onClickHandler = _noop,
|
||||||
headerMenuList = [MenuItemKeys.View],
|
headerMenuList = [MenuItemKeys.View],
|
||||||
isQueryEnabled,
|
isQueryEnabled,
|
||||||
threshold,
|
threshold,
|
||||||
|
variables,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
|
||||||
const onDragSelect = (start: number, end: number): void => {
|
const onDragSelect = useCallback(
|
||||||
const startTimestamp = Math.trunc(start);
|
(start: number, end: number): void => {
|
||||||
const endTimestamp = Math.trunc(end);
|
const startTimestamp = Math.trunc(start);
|
||||||
|
const endTimestamp = Math.trunc(end);
|
||||||
|
|
||||||
if (startTimestamp !== endTimestamp) {
|
if (startTimestamp !== endTimestamp) {
|
||||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
threshold: 0,
|
|
||||||
triggerOnce: true,
|
|
||||||
initialInView: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { selectedDashboard } = useDashboard();
|
const isVisible = useIntersectionObserver(graphRef, undefined, true);
|
||||||
|
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@ -61,20 +64,20 @@ function GridCardGraph({
|
|||||||
graphType: widget?.panelTypes,
|
graphType: widget?.panelTypes,
|
||||||
query: updatedQuery,
|
query: updatedQuery,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
variables: getDashboardVariables(variables),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
maxTime,
|
maxTime,
|
||||||
minTime,
|
minTime,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
selectedDashboard?.data?.variables,
|
variables,
|
||||||
widget?.query,
|
widget?.query,
|
||||||
widget?.panelTypes,
|
widget?.panelTypes,
|
||||||
widget.timePreferance,
|
widget.timePreferance,
|
||||||
],
|
],
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled,
|
enabled: isVisible && !isEmptyWidget && isQueryEnabled,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setErrorMessage(error.message);
|
setErrorMessage(error.message);
|
||||||
@ -82,39 +85,63 @@ function GridCardGraph({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartData = useMemo(
|
|
||||||
() =>
|
|
||||||
getChartData({
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
queryData: queryResponse?.data?.payload?.data?.result || [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
createDataset: undefined,
|
|
||||||
isWarningLimit: widget.panelTypes === PANEL_TYPES.TIME_SERIES,
|
|
||||||
}),
|
|
||||||
[queryResponse, widget?.panelTypes],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
|
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
|
||||||
|
|
||||||
return (
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
<span ref={graphRef}>
|
|
||||||
<WidgetGraphComponent
|
|
||||||
widget={widget}
|
|
||||||
queryResponse={queryResponse}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
data={chartData.data}
|
|
||||||
isWarning={chartData.isWarning}
|
|
||||||
name={name}
|
|
||||||
onDragSelect={onDragSelect}
|
|
||||||
threshold={threshold}
|
|
||||||
headerMenuList={headerMenuList}
|
|
||||||
onClickHandler={onClickHandler}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isEmptyLayout && <EmptyWidget />}
|
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||||
</span>
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const menuList =
|
||||||
|
widget.panelTypes === PANEL_TYPES.TABLE
|
||||||
|
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
|
||||||
|
: headerMenuList;
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
getUPlotChartOptions({
|
||||||
|
id: widget?.id,
|
||||||
|
apiResponse: queryResponse.data?.payload,
|
||||||
|
dimensions: containerDimensions,
|
||||||
|
isDarkMode,
|
||||||
|
onDragSelect,
|
||||||
|
yAxisUnit: widget?.yAxisUnit,
|
||||||
|
onClickHandler,
|
||||||
|
thresholds: widget.thresholds,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
widget?.id,
|
||||||
|
widget?.yAxisUnit,
|
||||||
|
widget.thresholds,
|
||||||
|
queryResponse.data?.payload,
|
||||||
|
containerDimensions,
|
||||||
|
isDarkMode,
|
||||||
|
onDragSelect,
|
||||||
|
onClickHandler,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||||
|
{isEmptyLayout ? (
|
||||||
|
<EmptyWidget />
|
||||||
|
) : (
|
||||||
|
<WidgetGraphComponent
|
||||||
|
data={chartData}
|
||||||
|
options={options}
|
||||||
|
widget={widget}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
isWarning={false}
|
||||||
|
name={name}
|
||||||
|
onDragSelect={onDragSelect}
|
||||||
|
threshold={threshold}
|
||||||
|
headerMenuList={menuList}
|
||||||
|
onClickHandler={onClickHandler}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { ChartData } from 'chart.js';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
|
import { UplotProps } from 'components/Uplot/Uplot';
|
||||||
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { MutableRefObject, ReactNode } from 'react';
|
import { MutableRefObject, ReactNode } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { 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 { MenuItemKeys } from '../WidgetHeader/contants';
|
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||||
import { LegendEntryProps } from './FullView/types';
|
import { LegendEntryProps } from './FullView/types';
|
||||||
@ -14,16 +16,15 @@ export interface GraphVisibilityLegendEntryProps {
|
|||||||
legendEntry: LegendEntryProps[];
|
legendEntry: LegendEntryProps[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WidgetGraphComponentProps {
|
export interface WidgetGraphComponentProps extends UplotProps {
|
||||||
widget: Widgets;
|
widget: Widgets;
|
||||||
queryResponse: UseQueryResult<
|
queryResponse: UseQueryResult<
|
||||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||||
>;
|
>;
|
||||||
errorMessage: string | undefined;
|
errorMessage: string | undefined;
|
||||||
data: ChartData;
|
|
||||||
name: string;
|
name: string;
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
onClickHandler?: GraphOnClickHandler;
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
threshold?: ReactNode;
|
threshold?: ReactNode;
|
||||||
headerMenuList: MenuItemKeys[];
|
headerMenuList: MenuItemKeys[];
|
||||||
isWarning: boolean;
|
isWarning: boolean;
|
||||||
@ -33,14 +34,15 @@ export interface GridCardGraphProps {
|
|||||||
widget: Widgets;
|
widget: Widgets;
|
||||||
name: string;
|
name: string;
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
onClickHandler?: GraphOnClickHandler;
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
threshold?: ReactNode;
|
threshold?: ReactNode;
|
||||||
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
||||||
isQueryEnabled: boolean;
|
isQueryEnabled: boolean;
|
||||||
|
variables?: Dashboard['data']['variables'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||||
data: ChartData;
|
options: uPlot.Options;
|
||||||
isExpandedName: boolean;
|
isExpandedName: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
|
||||||
import { LegendEntryProps } from './FullView/types';
|
import { LegendEntryProps } from './FullView/types';
|
||||||
@ -9,13 +10,13 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const getGraphVisibilityStateOnDataChange = ({
|
export const getGraphVisibilityStateOnDataChange = ({
|
||||||
data,
|
options,
|
||||||
isExpandedName,
|
isExpandedName,
|
||||||
name,
|
name,
|
||||||
}: GetGraphVisibilityStateOnLegendClickProps): GraphVisibilityLegendEntryProps => {
|
}: GetGraphVisibilityStateOnLegendClickProps): GraphVisibilityLegendEntryProps => {
|
||||||
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
|
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
|
||||||
graphVisibilityStates: Array(data.datasets.length).fill(true),
|
graphVisibilityStates: Array(options.series.length).fill(true),
|
||||||
legendEntry: showAllDataSet(data),
|
legendEntry: showAllDataSet(options),
|
||||||
};
|
};
|
||||||
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||||
const legendGraphFromLocalStore = localStorage.getItem(
|
const legendGraphFromLocalStore = localStorage.getItem(
|
||||||
@ -35,17 +36,19 @@ export const getGraphVisibilityStateOnDataChange = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGraphVisibilityStates = Array(data.datasets.length).fill(true);
|
const newGraphVisibilityStates = Array(options.series.length).fill(true);
|
||||||
legendFromLocalStore.forEach((item) => {
|
legendFromLocalStore.forEach((item) => {
|
||||||
const newName = isExpandedName ? `${name}expanded` : name;
|
const newName = isExpandedName ? `${name}expanded` : name;
|
||||||
if (item.name === newName) {
|
if (item.name === newName) {
|
||||||
visibilityStateAndLegendEntry.legendEntry = item.dataIndex;
|
visibilityStateAndLegendEntry.legendEntry = item.dataIndex;
|
||||||
data.datasets.forEach((datasets, i) => {
|
options.series.forEach((datasets, i) => {
|
||||||
const index = item.dataIndex.findIndex(
|
if (i !== 0) {
|
||||||
(dataKey) => dataKey.label === datasets.label,
|
const index = item.dataIndex.findIndex(
|
||||||
);
|
(dataKey) => dataKey.label === datasets.label,
|
||||||
if (index !== -1) {
|
);
|
||||||
newGraphVisibilityStates[i] = item.dataIndex[index].show;
|
if (index !== -1) {
|
||||||
|
newGraphVisibilityStates[i] = item.dataIndex[index].show;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
visibilityStateAndLegendEntry.graphVisibilityStates = newGraphVisibilityStates;
|
visibilityStateAndLegendEntry.graphVisibilityStates = newGraphVisibilityStates;
|
||||||
|
@ -11,8 +11,10 @@ import { useSelector } from 'react-redux';
|
|||||||
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';
|
||||||
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
|
import { ComponentTypes } from 'utils/permission';
|
||||||
|
|
||||||
import { headerMenuList } from './config';
|
import { EditMenuAction, ViewMenuAction } from './config';
|
||||||
import GridCard from './GridCard';
|
import GridCard from './GridCard';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -23,19 +25,21 @@ import {
|
|||||||
} from './styles';
|
} from './styles';
|
||||||
import { GraphLayoutProps } from './types';
|
import { GraphLayoutProps } from './types';
|
||||||
|
|
||||||
function GraphLayout({
|
function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||||
onAddPanelHandler,
|
|
||||||
widgets,
|
|
||||||
}: GraphLayoutProps): JSX.Element {
|
|
||||||
const {
|
const {
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
layouts,
|
layouts,
|
||||||
setLayouts,
|
setLayouts,
|
||||||
setSelectedDashboard,
|
setSelectedDashboard,
|
||||||
|
isDashboardLocked,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
const { data } = selectedDashboard || {};
|
||||||
|
|
||||||
|
const { widgets, variables } = data || {};
|
||||||
|
|
||||||
const { t } = useTranslation(['dashboard']);
|
const { t } = useTranslation(['dashboard']);
|
||||||
|
|
||||||
const { featureResponse, role } = useSelector<AppState, AppReducer>(
|
const { featureResponse, role, user } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -45,9 +49,20 @@ function GraphLayout({
|
|||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
|
||||||
|
|
||||||
|
if (isDashboardLocked) {
|
||||||
|
permissions = ['edit_locked_dashboard', 'add_panel_locked_dashboard'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRole: ROLES | null =
|
||||||
|
selectedDashboard?.created_by === user?.email
|
||||||
|
? (USER_ROLES.AUTHOR as ROLES)
|
||||||
|
: role;
|
||||||
|
|
||||||
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
||||||
['save_layout', 'add_panel'],
|
permissions,
|
||||||
role,
|
userRole,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSaveHandler = (): void => {
|
const onSaveHandler = (): void => {
|
||||||
@ -83,35 +98,42 @@ function GraphLayout({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const widgetActions = !isDashboardLocked
|
||||||
|
? [...ViewMenuAction, ...EditMenuAction]
|
||||||
|
: [...ViewMenuAction];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonContainer>
|
{!isDashboardLocked && (
|
||||||
{saveLayoutPermission && (
|
<ButtonContainer>
|
||||||
<Button
|
{saveLayoutPermission && (
|
||||||
loading={updateDashboardMutation.isLoading}
|
<Button
|
||||||
onClick={onSaveHandler}
|
loading={updateDashboardMutation.isLoading}
|
||||||
icon={<SaveFilled />}
|
onClick={onSaveHandler}
|
||||||
disabled={updateDashboardMutation.isLoading}
|
icon={<SaveFilled />}
|
||||||
>
|
disabled={updateDashboardMutation.isLoading}
|
||||||
{t('dashboard:save_layout')}
|
>
|
||||||
</Button>
|
{t('dashboard:save_layout')}
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{addPanelPermission && (
|
{addPanelPermission && (
|
||||||
<Button onClick={onAddPanelHandler} icon={<PlusOutlined />}>
|
<Button onClick={onAddPanelHandler} icon={<PlusOutlined />}>
|
||||||
{t('dashboard:add_panel')}
|
{t('dashboard:add_panel')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
<ReactGridLayout
|
<ReactGridLayout
|
||||||
cols={12}
|
cols={12}
|
||||||
rowHeight={100}
|
rowHeight={100}
|
||||||
autoSize
|
autoSize
|
||||||
width={100}
|
width={100}
|
||||||
isDraggable={addPanelPermission}
|
useCSSTransforms
|
||||||
isDroppable={addPanelPermission}
|
isDraggable={!isDashboardLocked && addPanelPermission}
|
||||||
isResizable={addPanelPermission}
|
isDroppable={!isDashboardLocked && addPanelPermission}
|
||||||
|
isResizable={!isDashboardLocked && addPanelPermission}
|
||||||
allowOverlap={false}
|
allowOverlap={false}
|
||||||
onLayoutChange={setLayouts}
|
onLayoutChange={setLayouts}
|
||||||
draggableHandle=".drag-handle"
|
draggableHandle=".drag-handle"
|
||||||
@ -122,12 +144,21 @@ function GraphLayout({
|
|||||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer isDarkMode={isDarkMode} key={id} data-grid={layout}>
|
<CardContainer
|
||||||
<Card $panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}>
|
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
key={id}
|
||||||
|
data-grid={layout}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className="grid-item"
|
||||||
|
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
|
||||||
|
>
|
||||||
<GridCard
|
<GridCard
|
||||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
||||||
name={currentWidget?.id || ''}
|
name={currentWidget?.id || ''}
|
||||||
headerMenuList={headerMenuList}
|
headerMenuList={widgetActions}
|
||||||
|
variables={variables}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
AlertOutlined,
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
@ -157,7 +158,7 @@ function WidgetHeader({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: MenuItemKeys.CreateAlerts,
|
key: MenuItemKeys.CreateAlerts,
|
||||||
icon: <DeleteOutlined />,
|
icon: <AlertOutlined />,
|
||||||
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
|
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.CreateAlerts],
|
||||||
isVisible: headerMenuList?.includes(MenuItemKeys.CreateAlerts) || false,
|
isVisible: headerMenuList?.includes(MenuItemKeys.CreateAlerts) || false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@ -168,9 +169,9 @@ function WidgetHeader({
|
|||||||
|
|
||||||
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
||||||
|
|
||||||
const onClickHandler = useCallback(() => {
|
const onClickHandler = (): void => {
|
||||||
setIsOpen((open) => !open);
|
setIsOpen(!isOpen);
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const menu = useMemo(
|
const menu = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -5,10 +5,8 @@ import styled from 'styled-components';
|
|||||||
export const HeaderContainer = styled.div<{ hover: boolean }>`
|
export const HeaderContainer = styled.div<{ hover: boolean }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: ${({ hover }): string => (hover ? `${grey[0]}66` : 'inherit')};
|
|
||||||
padding: 0.25rem 0;
|
padding: 0.25rem 0;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
cursor: all-scroll;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -20,12 +18,6 @@ export const HeaderContentContainer = styled.span`
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ArrowContainer = styled.span<{ hover: boolean }>`
|
|
||||||
visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')};
|
|
||||||
position: absolute;
|
|
||||||
right: -1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ThesholdContainer = styled.span`
|
export const ThesholdContainer = styled.span`
|
||||||
margin-top: -0.3rem;
|
margin-top: -0.3rem;
|
||||||
`;
|
`;
|
||||||
@ -39,8 +31,18 @@ export const DisplayThresholdContainer = styled.div`
|
|||||||
|
|
||||||
export const WidgetHeaderContainer = styled.div`
|
export const WidgetHeaderContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ArrowContainer = styled.span<{ hover: boolean }>`
|
||||||
|
visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')};
|
||||||
|
position: absolute;
|
||||||
|
right: -1rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Typography = styled(TypographyComponent)`
|
export const Typography = styled(TypographyComponent)`
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||||
|
|
||||||
export const headerMenuList = [
|
export const ViewMenuAction = [MenuItemKeys.View];
|
||||||
MenuItemKeys.View,
|
|
||||||
|
export const EditMenuAction = [
|
||||||
MenuItemKeys.Clone,
|
MenuItemKeys.Clone,
|
||||||
MenuItemKeys.Delete,
|
MenuItemKeys.Delete,
|
||||||
MenuItemKeys.Edit,
|
MenuItemKeys.Edit,
|
||||||
|
MenuItemKeys.CreateAlerts,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const headerMenuList = [...ViewMenuAction];
|
||||||
|
|
||||||
export const EMPTY_WIDGET_LAYOUT = {
|
export const EMPTY_WIDGET_LAYOUT = {
|
||||||
i: PANEL_TYPES.EMPTY_WIDGET,
|
i: PANEL_TYPES.EMPTY_WIDGET,
|
||||||
w: 6,
|
w: 6,
|
||||||
x: 0,
|
x: 0,
|
||||||
h: 2,
|
h: 3,
|
||||||
y: 0,
|
y: 0,
|
||||||
};
|
};
|
||||||
|
@ -6,14 +6,7 @@ import { EMPTY_WIDGET_LAYOUT } from './config';
|
|||||||
import GraphLayoutContainer from './GridCardLayout';
|
import GraphLayoutContainer from './GridCardLayout';
|
||||||
|
|
||||||
function GridGraph(): JSX.Element {
|
function GridGraph(): JSX.Element {
|
||||||
const {
|
const { handleToggleDashboardSlider, setLayouts } = useDashboard();
|
||||||
selectedDashboard,
|
|
||||||
setLayouts,
|
|
||||||
handleToggleDashboardSlider,
|
|
||||||
} = useDashboard();
|
|
||||||
|
|
||||||
const { data } = selectedDashboard || {};
|
|
||||||
const { widgets } = data || {};
|
|
||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
handleToggleDashboardSlider(true);
|
||||||
@ -24,12 +17,7 @@ function GridGraph(): JSX.Element {
|
|||||||
]);
|
]);
|
||||||
}, [handleToggleDashboardSlider, setLayouts]);
|
}, [handleToggleDashboardSlider, setLayouts]);
|
||||||
|
|
||||||
return (
|
return <GraphLayoutContainer onAddPanelHandler={onEmptyWidgetHandler} />;
|
||||||
<GraphLayoutContainer
|
|
||||||
onAddPanelHandler={onEmptyWidgetHandler}
|
|
||||||
widgets={widgets}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GridGraph;
|
export default GridGraph;
|
||||||
|
@ -13,10 +13,11 @@ interface CardProps {
|
|||||||
export const Card = styled(CardComponent)<CardProps>`
|
export const Card = styled(CardComponent)<CardProps>`
|
||||||
&&& {
|
&&& {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
height: 95%;
|
height: 90%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
${({ $panelType }): FlattenSimpleInterpolation =>
|
${({ $panelType }): FlattenSimpleInterpolation =>
|
||||||
$panelType === PANEL_TYPES.TABLE
|
$panelType === PANEL_TYPES.TABLE
|
||||||
@ -34,29 +35,31 @@ interface Props {
|
|||||||
export const CardContainer = styled.div<Props>`
|
export const CardContainer = styled.div<Props>`
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
:hover {
|
&.enable-resize {
|
||||||
.react-resizable-handle {
|
:hover {
|
||||||
position: absolute;
|
.react-resizable-handle {
|
||||||
width: 20px;
|
position: absolute;
|
||||||
height: 20px;
|
width: 20px;
|
||||||
bottom: 0;
|
height: 20px;
|
||||||
right: 0;
|
bottom: 0;
|
||||||
background-position: bottom right;
|
right: 0;
|
||||||
padding: 0 3px 3px 0;
|
background-position: bottom right;
|
||||||
background-repeat: no-repeat;
|
padding: 0 3px 3px 0;
|
||||||
background-origin: content-box;
|
background-repeat: no-repeat;
|
||||||
box-sizing: border-box;
|
background-origin: content-box;
|
||||||
cursor: se-resize;
|
box-sizing: border-box;
|
||||||
|
cursor: se-resize;
|
||||||
|
|
||||||
${({ isDarkMode }): StyledCSS => {
|
${({ isDarkMode }): StyledCSS => {
|
||||||
const uri = `data:image/svg+xml,%3Csvg viewBox='0 0 6 6' style='background-color:%23ffffff00' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' x='0px' y='0px' width='6px' height='6px'%0A%3E%3Cg opacity='0.302'%3E%3Cpath d='M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z' fill='${
|
const uri = `data:image/svg+xml,%3Csvg viewBox='0 0 6 6' style='background-color:%23ffffff00' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' x='0px' y='0px' width='6px' height='6px'%0A%3E%3Cg opacity='0.302'%3E%3Cpath d='M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z' fill='${
|
||||||
isDarkMode ? 'white' : 'grey'
|
isDarkMode ? 'white' : 'grey'
|
||||||
}'/%3E%3C/g%3E%3C/svg%3E`;
|
}'/%3E%3C/g%3E%3C/svg%3E`;
|
||||||
|
|
||||||
return css`
|
return css`
|
||||||
background-image: ${(): string => `url("${uri}")`};
|
background-image: ${(): string => `url("${uri}")`};
|
||||||
`;
|
`;
|
||||||
}}
|
}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
|
||||||
|
|
||||||
export interface GraphLayoutProps {
|
export interface GraphLayoutProps {
|
||||||
onAddPanelHandler: VoidFunction;
|
onAddPanelHandler: VoidFunction;
|
||||||
widgets?: Widgets[];
|
|
||||||
}
|
}
|
||||||
|
@ -11,39 +11,20 @@ const GridPanelSwitch = forwardRef<
|
|||||||
GridPanelSwitchProps
|
GridPanelSwitchProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{ panelType, data, yAxisUnit, panelData, query, options, thresholds },
|
||||||
panelType,
|
|
||||||
data,
|
|
||||||
title,
|
|
||||||
isStacked,
|
|
||||||
onClickHandler,
|
|
||||||
name,
|
|
||||||
yAxisUnit,
|
|
||||||
staticLine,
|
|
||||||
onDragSelect,
|
|
||||||
panelData,
|
|
||||||
query,
|
|
||||||
},
|
|
||||||
ref,
|
ref,
|
||||||
): JSX.Element | null => {
|
): JSX.Element | null => {
|
||||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||||
const result: PropsTypePropsMap = {
|
const result: PropsTypePropsMap = {
|
||||||
[PANEL_TYPES.TIME_SERIES]: {
|
[PANEL_TYPES.TIME_SERIES]: {
|
||||||
type: 'line',
|
|
||||||
data,
|
data,
|
||||||
title,
|
options,
|
||||||
isStacked,
|
|
||||||
onClickHandler,
|
|
||||||
name,
|
|
||||||
yAxisUnit,
|
|
||||||
staticLine,
|
|
||||||
onDragSelect,
|
|
||||||
ref,
|
ref,
|
||||||
},
|
},
|
||||||
[PANEL_TYPES.VALUE]: {
|
[PANEL_TYPES.VALUE]: {
|
||||||
title,
|
|
||||||
data,
|
data,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
thresholds,
|
||||||
},
|
},
|
||||||
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
|
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
|
||||||
[PANEL_TYPES.LIST]: null,
|
[PANEL_TYPES.LIST]: null,
|
||||||
@ -52,19 +33,7 @@ const GridPanelSwitch = forwardRef<
|
|||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [
|
}, [data, options, ref, yAxisUnit, thresholds, panelData, query]);
|
||||||
data,
|
|
||||||
isStacked,
|
|
||||||
name,
|
|
||||||
onClickHandler,
|
|
||||||
onDragSelect,
|
|
||||||
staticLine,
|
|
||||||
title,
|
|
||||||
yAxisUnit,
|
|
||||||
panelData,
|
|
||||||
query,
|
|
||||||
ref,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
||||||
PropsTypePropsMap[typeof panelType]
|
PropsTypePropsMap[typeof panelType]
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
import { ChartData } from 'chart.js';
|
import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
|
||||||
import {
|
import { UplotProps } from 'components/Uplot/Uplot';
|
||||||
GraphOnClickHandler,
|
|
||||||
GraphProps,
|
|
||||||
StaticLineProps,
|
|
||||||
} from 'components/Graph/types';
|
|
||||||
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 { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
|
import { ForwardedRef } from 'react';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import { PANEL_TYPES } from '../../constants/queryBuilder';
|
import { PANEL_TYPES } from '../../constants/queryBuilder';
|
||||||
|
|
||||||
export type GridPanelSwitchProps = {
|
export type GridPanelSwitchProps = {
|
||||||
panelType: PANEL_TYPES;
|
panelType: PANEL_TYPES;
|
||||||
data: ChartData;
|
data: uPlot.AlignedData;
|
||||||
title?: Widgets['title'];
|
options: uPlot.Options;
|
||||||
opacity?: string;
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
isStacked?: boolean;
|
|
||||||
onClickHandler?: GraphOnClickHandler;
|
|
||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
staticLine?: StaticLineProps;
|
staticLine?: StaticLineProps;
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
panelData: QueryDataV3[];
|
panelData: QueryDataV3[];
|
||||||
query: Query;
|
query: Query;
|
||||||
|
thresholds?: Widgets['thresholds'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsTypePropsMap = {
|
export type PropsTypePropsMap = {
|
||||||
[PANEL_TYPES.TIME_SERIES]: GraphProps;
|
[PANEL_TYPES.TIME_SERIES]: UplotProps & {
|
||||||
|
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||||
|
};
|
||||||
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
||||||
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
||||||
[PANEL_TYPES.TRACE]: null;
|
[PANEL_TYPES.TRACE]: null;
|
||||||
|
@ -12,17 +12,18 @@ function GridValueComponent({
|
|||||||
data,
|
data,
|
||||||
title,
|
title,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
thresholds,
|
||||||
}: GridValueComponentProps): JSX.Element {
|
}: GridValueComponentProps): JSX.Element {
|
||||||
const value = (((data.datasets[0] || []).data || [])[0] || 0) as number;
|
const value = ((data[1] || [])[0] || 0) as number;
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
|
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
|
||||||
|
|
||||||
const isDashboardPage = location.pathname.split('/').length === 3;
|
const isDashboardPage = location.pathname.split('/').length === 3;
|
||||||
|
|
||||||
if (data.datasets.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<ValueContainer isDashboardPage={isDashboardPage}>
|
<ValueContainer>
|
||||||
<Typography>No Data</Typography>
|
<Typography>No Data</Typography>
|
||||||
</ValueContainer>
|
</ValueContainer>
|
||||||
);
|
);
|
||||||
@ -33,8 +34,10 @@ function GridValueComponent({
|
|||||||
<TitleContainer isDashboardPage={isDashboardPage}>
|
<TitleContainer isDashboardPage={isDashboardPage}>
|
||||||
<Typography>{gridTitle}</Typography>
|
<Typography>{gridTitle}</Typography>
|
||||||
</TitleContainer>
|
</TitleContainer>
|
||||||
<ValueContainer isDashboardPage={isDashboardPage}>
|
<ValueContainer>
|
||||||
<ValueGraph
|
<ValueGraph
|
||||||
|
thresholds={thresholds || []}
|
||||||
|
rawValue={value}
|
||||||
value={
|
value={
|
||||||
yAxisUnit
|
yAxisUnit
|
||||||
? getYAxisFormattedValue(String(value), yAxisUnit)
|
? getYAxisFormattedValue(String(value), yAxisUnit)
|
||||||
|
@ -3,9 +3,9 @@ import styled from 'styled-components';
|
|||||||
interface Props {
|
interface Props {
|
||||||
isDashboardPage: boolean;
|
isDashboardPage: boolean;
|
||||||
}
|
}
|
||||||
export const ValueContainer = styled.div<Props>`
|
|
||||||
height: ${({ isDashboardPage }): string =>
|
export const ValueContainer = styled.div`
|
||||||
isDashboardPage ? '100%' : '55vh'};
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ChartData } from 'chart.js';
|
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||||
import { ReactNode } from 'react';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
export type GridValueComponentProps = {
|
export type GridValueComponentProps = {
|
||||||
data: ChartData;
|
data: uPlot.AlignedData;
|
||||||
title?: ReactNode;
|
options?: uPlot.Options;
|
||||||
|
title?: React.ReactNode;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
|
thresholds?: ThresholdProps[];
|
||||||
};
|
};
|
||||||
|
@ -104,7 +104,7 @@ function HeaderContainer(): JSX.Element {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: licenseData, isFetching } = useLicense();
|
const { data: licenseData, isFetching, status: licenseStatus } = useLicense();
|
||||||
|
|
||||||
const isLicenseActive =
|
const isLicenseActive =
|
||||||
licenseData?.payload?.licenses?.find((e) => e.isCurrent)?.status ===
|
licenseData?.payload?.licenses?.find((e) => e.isCurrent)?.status ===
|
||||||
@ -169,7 +169,7 @@ function HeaderContainer(): JSX.Element {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<Space size="middle" align="center">
|
<Space size="middle" align="center">
|
||||||
{!isLicenseActive && (
|
{licenseStatus === 'success' && !isLicenseActive && (
|
||||||
<Button onClick={onClickSignozCloud} type="primary">
|
<Button onClick={onClickSignozCloud} type="primary">
|
||||||
Try Signoz Cloud
|
Try Signoz Cloud
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||||
import { Modal } from 'antd';
|
import { Modal, Tooltip } from 'antd';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
import { Data } from '../index';
|
import { Data } from '../index';
|
||||||
import { TableLinkText } from './styles';
|
import { TableLinkText } from './styles';
|
||||||
|
|
||||||
function DeleteButton({ id }: Data): JSX.Element {
|
function DeleteButton({ id, createdBy, isLocked }: Data): JSX.Element {
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
|
const { role, user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
const isAuthor = user?.email === createdBy;
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { t } = useTranslation(['dashboard']);
|
||||||
|
|
||||||
const deleteDashboardMutation = useDeleteDashboard(id);
|
const deleteDashboardMutation = useDeleteDashboard(id);
|
||||||
|
|
||||||
const openConfirmationDialog = useCallback((): void => {
|
const openConfirmationDialog = useCallback((): void => {
|
||||||
@ -32,11 +41,33 @@ function DeleteButton({ id }: Data): JSX.Element {
|
|||||||
});
|
});
|
||||||
}, [modal, deleteDashboardMutation, queryClient]);
|
}, [modal, deleteDashboardMutation, queryClient]);
|
||||||
|
|
||||||
|
const getDeleteTooltipContent = (): string => {
|
||||||
|
if (isLocked) {
|
||||||
|
if (role === USER_ROLES.ADMIN || isAuthor) {
|
||||||
|
return t('dashboard:locked_dashboard_delete_tooltip_admin_author');
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('dashboard:locked_dashboard_delete_tooltip_editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableLinkText type="danger" onClick={openConfirmationDialog}>
|
<Tooltip placement="left" title={getDeleteTooltipContent()}>
|
||||||
Delete
|
<TableLinkText
|
||||||
</TableLinkText>
|
type="danger"
|
||||||
|
onClick={(): void => {
|
||||||
|
if (!isLocked) {
|
||||||
|
openConfirmationDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isLocked}
|
||||||
|
>
|
||||||
|
<DeleteOutlined /> Delete
|
||||||
|
</TableLinkText>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
</>
|
</>
|
||||||
@ -55,6 +86,7 @@ function Wrapper(props: Data): JSX.Element {
|
|||||||
tags,
|
tags,
|
||||||
createdBy,
|
createdBy,
|
||||||
lastUpdatedBy,
|
lastUpdatedBy,
|
||||||
|
isLocked,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -69,6 +101,7 @@ function Wrapper(props: Data): JSX.Element {
|
|||||||
tags,
|
tags,
|
||||||
createdBy,
|
createdBy,
|
||||||
lastUpdatedBy,
|
lastUpdatedBy,
|
||||||
|
isLocked,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,22 +1,28 @@
|
|||||||
|
import { LockFilled } from '@ant-design/icons';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { generatePath } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { Data } from '..';
|
import { Data } from '..';
|
||||||
import { TableLinkText } from './styles';
|
import { TableLinkText } from './styles';
|
||||||
|
|
||||||
function Name(name: Data['name'], data: Data): JSX.Element {
|
function Name(name: Data['name'], data: Data): JSX.Element {
|
||||||
const onClickHandler = (): void => {
|
const { id: DashboardId, isLocked } = data;
|
||||||
const { id: DashboardId } = data;
|
|
||||||
|
|
||||||
history.push(
|
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${DashboardId}`;
|
||||||
generatePath(ROUTES.DASHBOARD, {
|
|
||||||
dashboardId: DashboardId,
|
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||||
}),
|
if (event.metaKey || event.ctrlKey) {
|
||||||
);
|
window.open(getLink(), '_blank');
|
||||||
|
} else {
|
||||||
|
history.push(getLink());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return <TableLinkText onClick={onClickHandler}>{name}</TableLinkText>;
|
return (
|
||||||
|
<TableLinkText onClick={onClickHandler}>
|
||||||
|
{isLocked && <LockFilled />} {name}
|
||||||
|
</TableLinkText>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Name;
|
export default Name;
|
||||||
|
@ -130,7 +130,7 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tags (can be multiple)',
|
title: 'Tags',
|
||||||
dataIndex: 'tags',
|
dataIndex: 'tags',
|
||||||
width: 50,
|
width: 50,
|
||||||
render: (value): JSX.Element => <LabelColumn labels={value} />,
|
render: (value): JSX.Element => <LabelColumn labels={value} />,
|
||||||
@ -159,6 +159,7 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
tags: e.data.tags || [],
|
tags: e.data.tags || [],
|
||||||
key: e.uuid,
|
key: e.uuid,
|
||||||
createdBy: e.created_by,
|
createdBy: e.created_by,
|
||||||
|
isLocked: !!e.isLocked || false,
|
||||||
lastUpdatedBy: e.updated_by,
|
lastUpdatedBy: e.updated_by,
|
||||||
refetchDashboardList,
|
refetchDashboardList,
|
||||||
})) || [];
|
})) || [];
|
||||||
@ -342,6 +343,7 @@ export interface Data {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastUpdatedTime: string;
|
lastUpdatedTime: string;
|
||||||
lastUpdatedBy: string;
|
lastUpdatedBy: string;
|
||||||
|
isLocked: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
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 } from '../constant';
|
import { GraphTitle, 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';
|
||||||
@ -104,17 +104,17 @@ function DBCall(): JSX.Element {
|
|||||||
>
|
>
|
||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
<Card>
|
<Card data-testid="database_call_rps">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
name="database_call_rps"
|
name="database_call_rps"
|
||||||
widget={databaseCallsRPSWidget}
|
widget={databaseCallsRPSWidget}
|
||||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
ChartEvent,
|
xValue,
|
||||||
activeElements,
|
yValue,
|
||||||
chart,
|
mouseX,
|
||||||
data,
|
mouseY,
|
||||||
'database_call_rps',
|
'database_call_rps',
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@ -137,17 +137,18 @@ function DBCall(): JSX.Element {
|
|||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Card>
|
<Card data-testid="database_call_avg_duration">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
name="database_call_avg_duration"
|
name="database_call_avg_duration"
|
||||||
widget={databaseCallsAverageDurationWidget}
|
widget={databaseCallsAverageDurationWidget}
|
||||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
headerMenuList={MENU_ITEMS}
|
||||||
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
ChartEvent,
|
xValue,
|
||||||
activeElements,
|
yValue,
|
||||||
chart,
|
mouseX,
|
||||||
data,
|
mouseY,
|
||||||
'database_call_avg_duration',
|
'database_call_avg_duration',
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -17,7 +17,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 } 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';
|
||||||
@ -145,17 +145,18 @@ function External(): JSX.Element {
|
|||||||
>
|
>
|
||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
<Card>
|
<Card data-testid="external_call_error_percentage">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
|
headerMenuList={MENU_ITEMS}
|
||||||
name="external_call_error_percentage"
|
name="external_call_error_percentage"
|
||||||
widget={externalCallErrorWidget}
|
widget={externalCallErrorWidget}
|
||||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
ChartEvent,
|
xValue,
|
||||||
activeElements,
|
yValue,
|
||||||
chart,
|
mouseX,
|
||||||
data,
|
mouseY,
|
||||||
'external_call_error_percentage',
|
'external_call_error_percentage',
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@ -179,17 +180,18 @@ function External(): JSX.Element {
|
|||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Card>
|
<Card data-testid="external_call_duration">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
name="external_call_duration"
|
name="external_call_duration"
|
||||||
|
headerMenuList={MENU_ITEMS}
|
||||||
widget={externalCallDurationWidget}
|
widget={externalCallDurationWidget}
|
||||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
ChartEvent,
|
xValue,
|
||||||
activeElements,
|
yValue,
|
||||||
chart,
|
mouseX,
|
||||||
data,
|
mouseY,
|
||||||
'external_call_duration',
|
'external_call_duration',
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@ -214,20 +216,21 @@ function External(): JSX.Element {
|
|||||||
>
|
>
|
||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
<Card>
|
<Card data-testid="external_call_rps_by_address">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
name="external_call_rps_by_address"
|
name="external_call_rps_by_address"
|
||||||
widget={externalCallRPSWidget}
|
widget={externalCallRPSWidget}
|
||||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
headerMenuList={MENU_ITEMS}
|
||||||
|
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
ChartEvent,
|
xValue,
|
||||||
activeElements,
|
yValue,
|
||||||
chart,
|
mouseX,
|
||||||
data,
|
mouseY,
|
||||||
'external_call_rps_by_address',
|
'external_call_rps_by_address',
|
||||||
);
|
)
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
</Card>
|
</Card>
|
||||||
@ -248,17 +251,18 @@ function External(): JSX.Element {
|
|||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Card>
|
<Card data-testid="external_call_duration_by_address">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
name="external_call_duration_by_address"
|
name="external_call_duration_by_address"
|
||||||
widget={externalCallDurationAddressWidget}
|
widget={externalCallDurationAddressWidget}
|
||||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
headerMenuList={MENU_ITEMS}
|
||||||
|
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||||
onGraphClickHandler(setSelectedTimeStamp)(
|
onGraphClickHandler(setSelectedTimeStamp)(
|
||||||
ChartEvent,
|
xValue,
|
||||||
activeElements,
|
yValue,
|
||||||
chart,
|
mouseX,
|
||||||
data,
|
mouseY,
|
||||||
'external_call_duration_by_address',
|
'external_call_duration_by_address',
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import getTopLevelOperations, {
|
import getTopLevelOperations, {
|
||||||
ServiceDataProps,
|
ServiceDataProps,
|
||||||
} from 'api/metrics/getTopLevelOperations';
|
} from 'api/metrics/getTopLevelOperations';
|
||||||
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
|
||||||
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';
|
||||||
@ -15,6 +14,7 @@ import {
|
|||||||
resourceAttributesToTagFilterItems,
|
resourceAttributesToTagFilterItems,
|
||||||
} from 'hooks/useResourceAttribute/utils';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
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, useSelector } from 'react-redux';
|
||||||
@ -73,20 +73,19 @@ function Application(): JSX.Element {
|
|||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const handleGraphClick = useCallback(
|
const handleGraphClick = useCallback(
|
||||||
(type: string): ClickHandlerType => (
|
(type: string): OnClickPluginOpts['onClick'] => (
|
||||||
ChartEvent: ChartEvent,
|
xValue,
|
||||||
activeElements: ActiveElement[],
|
yValue,
|
||||||
chart: Chart,
|
mouseX,
|
||||||
data: ChartData,
|
mouseY,
|
||||||
): void => {
|
): Promise<void> =>
|
||||||
onGraphClickHandler(handleSetTimeStamp)(
|
onGraphClickHandler(handleSetTimeStamp)(
|
||||||
ChartEvent,
|
xValue,
|
||||||
activeElements,
|
yValue,
|
||||||
chart,
|
mouseX,
|
||||||
data,
|
mouseY,
|
||||||
type,
|
type,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
[handleSetTimeStamp],
|
[handleSetTimeStamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -283,12 +282,6 @@ function Application(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClickHandlerType = (
|
export type ClickHandlerType = () => void;
|
||||||
ChartEvent: ChartEvent,
|
|
||||||
activeElements: ActiveElement[],
|
|
||||||
chart: Chart,
|
|
||||||
data: ChartData,
|
|
||||||
type?: string,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
export default Application;
|
export default Application;
|
||||||
|
@ -77,7 +77,7 @@ function ApDexMetrics({
|
|||||||
|
|
||||||
const isQueryEnabled =
|
const isQueryEnabled =
|
||||||
topLevelOperationsRoute.length > 0 &&
|
topLevelOperationsRoute.length > 0 &&
|
||||||
metricsBuckets &&
|
!!metricsBuckets &&
|
||||||
metricsBuckets?.length > 0 &&
|
metricsBuckets?.length > 0 &&
|
||||||
delta !== undefined;
|
delta !== undefined;
|
||||||
|
|
||||||
|
@ -10,8 +10,8 @@ function ApDexMetricsApplication({
|
|||||||
handleGraphClick,
|
handleGraphClick,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
tagFilterItems,
|
tagFilterItems,
|
||||||
topLevelOperationsRoute,
|
|
||||||
thresholdValue,
|
thresholdValue,
|
||||||
|
topLevelOperationsRoute,
|
||||||
}: ApDexDataSwitcherProps): JSX.Element {
|
}: ApDexDataSwitcherProps): JSX.Element {
|
||||||
const { data, isLoading, error } = useGetMetricMeta(metricMeta);
|
const { data, isLoading, error } = useGetMetricMeta(metricMeta);
|
||||||
useErrorNotification(error);
|
useErrorNotification(error);
|
||||||
@ -22,11 +22,11 @@ function ApDexMetricsApplication({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ApDexMetrics
|
<ApDexMetrics
|
||||||
|
topLevelOperationsRoute={topLevelOperationsRoute}
|
||||||
handleGraphClick={handleGraphClick}
|
handleGraphClick={handleGraphClick}
|
||||||
delta={data?.data.delta}
|
delta={data?.data.delta}
|
||||||
metricsBuckets={data?.data.le}
|
metricsBuckets={data?.data.le || []}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
topLevelOperationsRoute={topLevelOperationsRoute}
|
|
||||||
tagFilterItems={tagFilterItems}
|
tagFilterItems={tagFilterItems}
|
||||||
thresholdValue={thresholdValue}
|
thresholdValue={thresholdValue}
|
||||||
/>
|
/>
|
||||||
|
@ -30,7 +30,7 @@ function ApDexApplication({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card data-testid="apdex">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<ApDexMetricsApplication
|
<ApDexMetricsApplication
|
||||||
handleGraphClick={handleGraphClick}
|
handleGraphClick={handleGraphClick}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { ClickHandlerType } from '../../Overview';
|
|
||||||
|
|
||||||
export interface ApDexApplicationProps {
|
export interface ApDexApplicationProps {
|
||||||
handleGraphClick: (type: string) => ClickHandlerType;
|
handleGraphClick: (type: string) => OnClickPluginOpts['onClick'];
|
||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
topLevelOperationsRoute: string[];
|
topLevelOperationsRoute: string[];
|
||||||
tagFilterItems: TagFilterItem[];
|
tagFilterItems: TagFilterItem[];
|
||||||
|
@ -8,12 +8,12 @@ import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
|||||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils';
|
import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils';
|
||||||
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
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 { ClickHandlerType } from '../Overview';
|
|
||||||
import { Button } from '../styles';
|
import { Button } from '../styles';
|
||||||
import { IServiceName } from '../types';
|
import { IServiceName } from '../types';
|
||||||
import { handleNonInQueryRange, onViewTracePopupClick } from '../util';
|
import { handleNonInQueryRange, onViewTracePopupClick } from '../util';
|
||||||
@ -80,7 +80,7 @@ function ServiceOverview({
|
|||||||
>
|
>
|
||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
<Card>
|
<Card data-testid="service_latency">
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
name="service_latency"
|
name="service_latency"
|
||||||
@ -99,7 +99,7 @@ interface ServiceOverviewProps {
|
|||||||
selectedTimeStamp: number;
|
selectedTimeStamp: number;
|
||||||
selectedTraceTags: string;
|
selectedTraceTags: string;
|
||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
handleGraphClick: (type: string) => ClickHandlerType;
|
handleGraphClick: (type: string) => OnClickPluginOpts['onClick'];
|
||||||
topLevelOperationsRoute: string[];
|
topLevelOperationsRoute: string[];
|
||||||
topLevelOperationsIsLoading: boolean;
|
topLevelOperationsIsLoading: boolean;
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,9 @@ import axios from 'axios';
|
|||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import Graph from 'container/GridCardLayout/GridCard';
|
import Graph from 'container/GridCardLayout/GridCard';
|
||||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||||
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
import { ClickHandlerType } from '../Overview';
|
|
||||||
|
|
||||||
function TopLevelOperation({
|
function TopLevelOperation({
|
||||||
name,
|
name,
|
||||||
opName,
|
opName,
|
||||||
@ -18,7 +17,7 @@ function TopLevelOperation({
|
|||||||
topLevelOperationsIsLoading,
|
topLevelOperationsIsLoading,
|
||||||
}: TopLevelOperationProps): JSX.Element {
|
}: TopLevelOperationProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card data-testid={name}>
|
||||||
{topLevelOperationsIsError ? (
|
{topLevelOperationsIsError ? (
|
||||||
<Typography>
|
<Typography>
|
||||||
{axios.isAxiosError(topLevelOperationsError)
|
{axios.isAxiosError(topLevelOperationsError)
|
||||||
@ -46,7 +45,7 @@ interface TopLevelOperationProps {
|
|||||||
topLevelOperationsIsError: boolean;
|
topLevelOperationsIsError: boolean;
|
||||||
topLevelOperationsError: unknown;
|
topLevelOperationsError: unknown;
|
||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
handleGraphClick: (type: string) => ClickHandlerType;
|
handleGraphClick: (type: string) => OnClickPluginOpts['onClick'];
|
||||||
widget: Widgets;
|
widget: Widgets;
|
||||||
topLevelOperationsIsLoading: boolean;
|
topLevelOperationsIsLoading: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { routeConfig } from 'container/SideNav/config';
|
import { routeConfig } from 'container/SideNav/config';
|
||||||
@ -32,7 +31,8 @@ export function onViewTracePopupClick({
|
|||||||
}: OnViewTracePopupClickProps): VoidFunction {
|
}: OnViewTracePopupClickProps): VoidFunction {
|
||||||
return (): void => {
|
return (): void => {
|
||||||
const currentTime = timestamp;
|
const currentTime = timestamp;
|
||||||
const tPlusOne = timestamp + 60 * 1000;
|
|
||||||
|
const tPlusOne = timestamp + 60;
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
urlParams.set(QueryParams.startTime, currentTime.toString());
|
urlParams.set(QueryParams.startTime, currentTime.toString());
|
||||||
@ -54,37 +54,25 @@ export function onGraphClickHandler(
|
|||||||
setSelectedTimeStamp: (n: number) => void | Dispatch<SetStateAction<number>>,
|
setSelectedTimeStamp: (n: number) => void | Dispatch<SetStateAction<number>>,
|
||||||
) {
|
) {
|
||||||
return async (
|
return async (
|
||||||
event: ChartEvent,
|
xValue: number,
|
||||||
elements: ActiveElement[],
|
yValue: number,
|
||||||
chart: Chart,
|
mouseX: number,
|
||||||
data: ChartData,
|
mouseY: number,
|
||||||
from: string,
|
type: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (event.native) {
|
const id = `${type}_button`;
|
||||||
const points = chart.getElementsAtEventForMode(
|
|
||||||
event.native,
|
|
||||||
'nearest',
|
|
||||||
{ intersect: false },
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const id = `${from}_button`;
|
|
||||||
const buttonElement = document.getElementById(id);
|
|
||||||
|
|
||||||
if (points.length !== 0) {
|
const buttonElement = document.getElementById(id);
|
||||||
const firstPoint = points[0];
|
|
||||||
|
|
||||||
if (data.labels) {
|
if (xValue) {
|
||||||
const time = data?.labels[firstPoint.index] as Date;
|
if (buttonElement) {
|
||||||
if (buttonElement) {
|
buttonElement.style.display = 'block';
|
||||||
buttonElement.style.display = 'block';
|
buttonElement.style.left = `${mouseX}px`;
|
||||||
buttonElement.style.left = `${firstPoint.element.x}px`;
|
buttonElement.style.top = `${mouseY}px`;
|
||||||
buttonElement.style.top = `${firstPoint.element.y}px`;
|
setSelectedTimeStamp(xValue);
|
||||||
setSelectedTimeStamp(time.getTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (buttonElement && buttonElement.style.display === 'block') {
|
|
||||||
buttonElement.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
} else if (buttonElement && buttonElement.style.display === 'block') {
|
||||||
|
buttonElement.style.display = 'none';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { DownloadOptions } from 'container/Download/Download.types';
|
import { DownloadOptions } from 'container/Download/Download.types';
|
||||||
|
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||||
|
|
||||||
export const legend = {
|
export const legend = {
|
||||||
address: '{{address}}',
|
address: '{{address}}',
|
||||||
@ -13,6 +14,8 @@ export const LATENCY_AGGREGATEOPERATOR_SPAN_METRICS = [
|
|||||||
];
|
];
|
||||||
export const OPERATION_LEGENDS = ['Operations'];
|
export const OPERATION_LEGENDS = ['Operations'];
|
||||||
|
|
||||||
|
export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
|
||||||
|
|
||||||
export enum FORMULA {
|
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',
|
||||||
@ -21,6 +24,8 @@ export enum FORMULA {
|
|||||||
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
|
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TOP_LEVEL_OPERATIONS = ['{{.top_level_operations}}'];
|
||||||
|
|
||||||
export enum GraphTitle {
|
export enum GraphTitle {
|
||||||
APDEX = 'Apdex',
|
APDEX = 'Apdex',
|
||||||
LATENCY = 'Latency',
|
LATENCY = 'Latency',
|
||||||
|
@ -45,7 +45,7 @@ function DashboardGraphSlider(): JSX.Element {
|
|||||||
i: id,
|
i: id,
|
||||||
w: 6,
|
w: 6,
|
||||||
x: 0,
|
x: 0,
|
||||||
h: 2,
|
h: 3,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) ||
|
...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) ||
|
||||||
|
@ -3,11 +3,13 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.6rem;
|
justify-content: right;
|
||||||
|
gap: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Card = styled(CardComponent)`
|
export const Card = styled(CardComponent)`
|
||||||
min-height: 10vh;
|
min-height: 10vh;
|
||||||
|
min-width: 120px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import Input from 'components/Input';
|
import Input from 'components/Input';
|
||||||
import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react';
|
import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react';
|
||||||
|
|
||||||
function NameOfTheDashboard({
|
function DashboardName({ setName, name }: DashboardNameProps): JSX.Element {
|
||||||
setName,
|
|
||||||
name,
|
|
||||||
}: NameOfTheDashboardProps): JSX.Element {
|
|
||||||
const onChangeHandler = useCallback(
|
const onChangeHandler = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setName(e.target.value);
|
setName(e.target.value);
|
||||||
@ -22,9 +19,9 @@ function NameOfTheDashboard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NameOfTheDashboardProps {
|
interface DashboardNameProps {
|
||||||
name: string;
|
name: string;
|
||||||
setName: Dispatch<SetStateAction<string>>;
|
setName: Dispatch<SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NameOfTheDashboard;
|
export default DashboardName;
|
@ -0,0 +1,7 @@
|
|||||||
|
.dashboard-description {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2; /* Show up to 2 lines */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
@ -5,7 +5,7 @@ import { useState } from 'react';
|
|||||||
import DashboardSettingsContent from '../DashboardSettings';
|
import DashboardSettingsContent from '../DashboardSettings';
|
||||||
import { DrawerContainer } from './styles';
|
import { DrawerContainer } from './styles';
|
||||||
|
|
||||||
function SettingsDrawer(): JSX.Element {
|
function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
|
||||||
const [visible, setVisible] = useState<boolean>(false);
|
const [visible, setVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
const showDrawer = (): void => {
|
const showDrawer = (): void => {
|
||||||
@ -18,12 +18,13 @@ function SettingsDrawer(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button type="dashed" onClick={showDrawer}>
|
<Button type="dashed" onClick={showDrawer} style={{ width: '100%' }}>
|
||||||
<SettingOutlined /> Configure
|
<SettingOutlined /> Configure
|
||||||
</Button>
|
</Button>
|
||||||
<DrawerContainer
|
<DrawerContainer
|
||||||
|
title={drawerTitle}
|
||||||
placement="right"
|
placement="right"
|
||||||
width="70%"
|
width="50%"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
open={visible}
|
open={visible}
|
||||||
>
|
>
|
@ -1,4 +1,5 @@
|
|||||||
import { Button, Modal, Typography } from 'antd';
|
import { CopyFilled, DownloadOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Modal } from 'antd';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
@ -6,7 +7,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
import { downloadObjectAsJson } from './util';
|
import { downloadObjectAsJson } from './utils';
|
||||||
|
|
||||||
function ShareModal({
|
function ShareModal({
|
||||||
isJSONModalVisible,
|
isJSONModalVisible,
|
||||||
@ -16,7 +17,6 @@ function ShareModal({
|
|||||||
const getParsedValue = (): string => JSON.stringify(selectedData, null, 2);
|
const getParsedValue = (): string => JSON.stringify(selectedData, null, 2);
|
||||||
|
|
||||||
const [jsonValue, setJSONValue] = useState<string>(getParsedValue());
|
const [jsonValue, setJSONValue] = useState<string>(getParsedValue());
|
||||||
const [isViewJSON, setIsViewJSON] = useState<boolean>(false);
|
|
||||||
const { t } = useTranslation(['dashboard', 'common']);
|
const { t } = useTranslation(['dashboard', 'common']);
|
||||||
const [state, setCopy] = useCopyToClipboard();
|
const [state, setCopy] = useCopyToClipboard();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@ -39,44 +39,41 @@ function ShareModal({
|
|||||||
}
|
}
|
||||||
}, [state.error, state.value, t, notifications]);
|
}, [state.error, state.value, t, notifications]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line arrow-body-style
|
||||||
const GetFooterComponent = useMemo(() => {
|
const GetFooterComponent = useMemo(() => {
|
||||||
if (!isViewJSON) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={(): void => {
|
|
||||||
setIsViewJSON(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('view_json')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={(): void => {
|
|
||||||
downloadObjectAsJson(selectedData, selectedData.title);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('download_json')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={(): void => setCopy(jsonValue)} type="primary">
|
<>
|
||||||
{t('copy_to_clipboard')}
|
<Button
|
||||||
</Button>
|
style={{
|
||||||
|
marginTop: '16px',
|
||||||
|
}}
|
||||||
|
onClick={(): void => setCopy(jsonValue)}
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<CopyFilled /> {t('copy_to_clipboard')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={(): void => {
|
||||||
|
downloadObjectAsJson(selectedData, selectedData.title);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DownloadOutlined /> {t('download_json')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}, [isViewJSON, jsonValue, selectedData, setCopy, t]);
|
}, [jsonValue, selectedData, setCopy, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={isJSONModalVisible}
|
open={isJSONModalVisible}
|
||||||
onCancel={(): void => {
|
onCancel={(): void => {
|
||||||
onToggleHandler();
|
onToggleHandler();
|
||||||
setIsViewJSON(false);
|
|
||||||
}}
|
}}
|
||||||
width="70vw"
|
width="80vw"
|
||||||
centered
|
centered
|
||||||
title={t('share', {
|
title={t('share', {
|
||||||
ns: 'common',
|
ns: 'common',
|
||||||
@ -86,11 +83,11 @@ function ShareModal({
|
|||||||
destroyOnClose
|
destroyOnClose
|
||||||
footer={GetFooterComponent}
|
footer={GetFooterComponent}
|
||||||
>
|
>
|
||||||
{!isViewJSON ? (
|
<Editor
|
||||||
<Typography>{t('export_dashboard')}</Typography>
|
height="70vh"
|
||||||
) : (
|
onChange={(value): void => setJSONValue(value)}
|
||||||
<Editor onChange={(value): void => setJSONValue(value)} value={jsonValue} />
|
value={jsonValue}
|
||||||
)}
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
import './Description.styles.scss';
|
||||||
|
|
||||||
|
import { LockFilled, ShareAltOutlined, UnlockFilled } from '@ant-design/icons';
|
||||||
|
import { Button, Card, Col, Row, Space, Tag, Tooltip, Typography } from 'antd';
|
||||||
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
|
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||||
|
import SettingsDrawer from './SettingsDrawer';
|
||||||
|
import ShareModal from './ShareModal';
|
||||||
|
|
||||||
|
function DashboardDescription(): JSX.Element {
|
||||||
|
const {
|
||||||
|
selectedDashboard,
|
||||||
|
isDashboardLocked,
|
||||||
|
handleDashboardLockToggle,
|
||||||
|
} = useDashboard();
|
||||||
|
|
||||||
|
const selectedData = selectedDashboard?.data || ({} as DashboardData);
|
||||||
|
|
||||||
|
const { title = '', tags, description } = selectedData || {};
|
||||||
|
|
||||||
|
const [openDashboardJSON, setOpenDashboardJSON] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
const { user, role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
const [editDashboard] = useComponentPermission(['edit_dashboard'], role);
|
||||||
|
|
||||||
|
let isAuthor = false;
|
||||||
|
|
||||||
|
if (selectedDashboard && user && user.email) {
|
||||||
|
isAuthor = selectedDashboard?.created_by === user?.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onToggleHandler = (): void => {
|
||||||
|
setOpenDashboardJSON((state) => !state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLockDashboardToggle = (): void => {
|
||||||
|
handleDashboardLockToggle(!isDashboardLocked);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col flex={1} span={12}>
|
||||||
|
<Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
|
||||||
|
{isDashboardLocked && (
|
||||||
|
<Tooltip title="Dashboard Locked" placement="top">
|
||||||
|
<LockFilled />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{title}
|
||||||
|
</Typography.Title>
|
||||||
|
{description && (
|
||||||
|
<Typography className="dashboard-description">{description}</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tags && (
|
||||||
|
<div style={{ margin: '0.5rem 0' }}>
|
||||||
|
{tags?.map((tag) => (
|
||||||
|
<Tag key={tag}>{tag}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Row justify="end">
|
||||||
|
<DashboardVariableSelection />
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col span={4} style={{ textAlign: 'right' }}>
|
||||||
|
{selectedData && (
|
||||||
|
<ShareModal
|
||||||
|
isJSONModalVisible={openDashboardJSON}
|
||||||
|
onToggleHandler={onToggleHandler}
|
||||||
|
selectedData={selectedData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Space direction="vertical">
|
||||||
|
{!isDashboardLocked && editDashboard && (
|
||||||
|
<SettingsDrawer drawerTitle={title} />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
type="dashed"
|
||||||
|
onClick={onToggleHandler}
|
||||||
|
icon={<ShareAltOutlined />}
|
||||||
|
>
|
||||||
|
{t('share')}
|
||||||
|
</Button>
|
||||||
|
{(isAuthor || role === USER_ROLES.ADMIN) && (
|
||||||
|
<Tooltip
|
||||||
|
placement="left"
|
||||||
|
title={isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
type="dashed"
|
||||||
|
onClick={handleLockDashboardToggle}
|
||||||
|
icon={isDashboardLocked ? <LockFilled /> : <UnlockFilled />}
|
||||||
|
>
|
||||||
|
{isDashboardLocked ? 'Unlock' : 'Lock'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DashboardDescription;
|
@ -14,7 +14,11 @@ export const Button = styled(ButtonComponent)`
|
|||||||
|
|
||||||
export const DrawerContainer = styled(Drawer)`
|
export const DrawerContainer = styled(Drawer)`
|
||||||
.ant-drawer-header {
|
.ant-drawer-header {
|
||||||
padding: 0;
|
padding: 16px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-drawer-body {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
`;
|
`;
|
@ -104,7 +104,13 @@ function AddTags({ tags, setTags }: AddTagsProps): JSX.Element {
|
|||||||
|
|
||||||
{!inputVisible && (
|
{!inputVisible && (
|
||||||
<NewTagContainer icon={<PlusOutlined />} onClick={showInput}>
|
<NewTagContainer icon={<PlusOutlined />} onClick={showInput}>
|
||||||
<Typography>New Tag</Typography>
|
<Typography
|
||||||
|
style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New Tag
|
||||||
|
</Typography>
|
||||||
</NewTagContainer>
|
</NewTagContainer>
|
||||||
)}
|
)}
|
||||||
</TagsContainer>
|
</TagsContainer>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SaveOutlined } from '@ant-design/icons';
|
import { SaveOutlined } from '@ant-design/icons';
|
||||||
import { Col, Divider, Input, Space, Typography } from 'antd';
|
import { Col, Input, Space, Typography } from 'antd';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
|
import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
@ -71,6 +71,7 @@ function GeneralDashboardSettings(): JSX.Element {
|
|||||||
<div>
|
<div>
|
||||||
<Typography style={{ marginBottom: '0.5rem' }}>Description</Typography>
|
<Typography style={{ marginBottom: '0.5rem' }}>Description</Typography>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
rows={5}
|
||||||
value={updatedDescription}
|
value={updatedDescription}
|
||||||
onChange={(e): void => setUpdatedDescription(e.target.value)}
|
onChange={(e): void => setUpdatedDescription(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -80,8 +81,10 @@ function GeneralDashboardSettings(): JSX.Element {
|
|||||||
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
|
<AddTags tags={updatedTags} setTags={setUpdatedTags} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Divider />
|
|
||||||
<Button
|
<Button
|
||||||
|
style={{
|
||||||
|
margin: '16px 0',
|
||||||
|
}}
|
||||||
disabled={updateDashboardMutation.isLoading}
|
disabled={updateDashboardMutation.isLoading}
|
||||||
loading={updateDashboardMutation.isLoading}
|
loading={updateDashboardMutation.isLoading}
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
|
@ -3,7 +3,7 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { map, sortBy } from 'lodash-es';
|
import { map, sortBy } from 'lodash-es';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
@ -114,4 +114,4 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DashboardVariableSelection;
|
export default memo(DashboardVariableSelection);
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
import { ShareAltOutlined } from '@ant-design/icons';
|
|
||||||
import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd';
|
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
|
||||||
import SettingsDrawer from './SettingsDrawer';
|
|
||||||
import ShareModal from './ShareModal';
|
|
||||||
|
|
||||||
function DescriptionOfDashboard(): JSX.Element {
|
|
||||||
const { selectedDashboard } = useDashboard();
|
|
||||||
|
|
||||||
const selectedData = selectedDashboard?.data;
|
|
||||||
const { title, tags, description } = selectedData || {};
|
|
||||||
|
|
||||||
const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const { t } = useTranslation('common');
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
const [editDashboard] = useComponentPermission(['edit_dashboard'], role);
|
|
||||||
|
|
||||||
const onToggleHandler = (): void => {
|
|
||||||
isIsJSONModalVisible((state) => !state);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Row>
|
|
||||||
<Col flex={1}>
|
|
||||||
<Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
|
|
||||||
{title}
|
|
||||||
</Typography.Title>
|
|
||||||
<Typography>{description}</Typography>
|
|
||||||
|
|
||||||
<div style={{ margin: '0.5rem 0' }}>
|
|
||||||
{tags?.map((tag) => (
|
|
||||||
<Tag key={tag}>{tag}</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DashboardVariableSelection />
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
{selectedData && (
|
|
||||||
<ShareModal
|
|
||||||
isJSONModalVisible={isJSONModalVisible}
|
|
||||||
onToggleHandler={onToggleHandler}
|
|
||||||
selectedData={selectedData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Space direction="vertical">
|
|
||||||
{editDashboard && <SettingsDrawer />}
|
|
||||||
<Button
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
type="dashed"
|
|
||||||
onClick={onToggleHandler}
|
|
||||||
icon={<ShareAltOutlined />}
|
|
||||||
>
|
|
||||||
{t('share')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DescriptionOfDashboard;
|
|
@ -1,4 +1,4 @@
|
|||||||
import Description from './DescriptionOfDashboard';
|
import Description from './DashboardDescription';
|
||||||
import GridGraphs from './GridGraphs';
|
import GridGraphs from './GridGraphs';
|
||||||
|
|
||||||
function NewDashboard(): JSX.Element {
|
function NewDashboard(): JSX.Element {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user