Merge pull request #3982 from SigNoz/release/v0.34.0

Release/v0.34.0
This commit is contained in:
Ankit Nayan 2023-11-17 01:00:06 +05:30 committed by GitHub
commit 73fc262f04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
213 changed files with 5982 additions and 2131 deletions

View File

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

View File

@ -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]

View File

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

View File

@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # 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:
[ [

View File

@ -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]

View File

@ -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)

View 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")
}

View File

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

View File

@ -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)
}
} }
}) })

View File

@ -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)
}
} }
}() }()

View File

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

View File

@ -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"
} }

View File

@ -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"
} }

View File

@ -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"
} }

View File

@ -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."
} }

View File

@ -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."
} }

View File

@ -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"
} }

View File

@ -0,0 +1,3 @@
{
"this_value_satisfies_multiple_thresholds": "This value satisfies multiple thresholds."
}

View 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;

View 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;

View File

@ -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"

View File

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

View File

@ -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 & {

View File

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

View File

@ -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"
> >

View File

@ -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>
); );

View 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);

View File

@ -0,0 +1,3 @@
import Uplot from './Uplot';
export default Uplot;

View 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%;
}

View 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],
);
});
};

View 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;
}
}

View File

@ -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;

View File

@ -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;
`;

View 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,
};
}

View File

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

View File

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

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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}
/>
))}
</>
); );
} }

View File

@ -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;

View File

@ -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()}

View File

@ -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)`

View File

@ -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,
}));

View File

@ -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;
}
}

View File

@ -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>
); );
} }

View File

@ -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;

View File

@ -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}

View File

@ -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[];

View File

@ -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>
); );

View File

@ -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;
}
}
}

View File

@ -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>
); );
} }

View File

@ -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};
`; `;

View File

@ -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;
} }

View File

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

View File

@ -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));
});
});

View File

@ -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,
}; };

View File

@ -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>
); );
} }

View File

@ -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;
} }

View File

@ -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;

View File

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

View File

@ -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(
() => ({ () => ({

View File

@ -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)`

View File

@ -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,
}; };

View File

@ -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;

View File

@ -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}")`};
`; `;
}} }}
}
} }
} }
`; `;

View File

@ -1,6 +1,3 @@
import { Widgets } from 'types/api/dashboard/getAll';
export interface GraphLayoutProps { export interface GraphLayoutProps {
onAddPanelHandler: VoidFunction; onAddPanelHandler: VoidFunction;
widgets?: Widgets[];
} }

View File

@ -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]

View File

@ -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;

View File

@ -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)

View File

@ -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;

View File

@ -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[];
}; };

View File

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

View File

@ -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,
}} }}
/> />
); );

View File

@ -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;

View File

@ -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;
} }

View File

@ -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',
); );
}} }}

View File

@ -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',
); );
}} }}

View File

@ -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;

View File

@ -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;

View File

@ -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}
/> />

View File

@ -30,7 +30,7 @@ function ApDexApplication({
} }
return ( return (
<Card> <Card data-testid="apdex">
<GraphContainer> <GraphContainer>
<ApDexMetricsApplication <ApDexMetricsApplication
handleGraphClick={handleGraphClick} handleGraphClick={handleGraphClick}

View File

@ -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[];

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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';
} }
}; };
} }

View File

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

View File

@ -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) ||

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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}
> >

View File

@ -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>
); );
} }

View File

@ -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 /> &nbsp;
</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;

View File

@ -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;
}
`; `;

View File

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

View File

@ -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 />}

View File

@ -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);

View File

@ -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;

View File

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