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