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

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

View File

@ -146,7 +146,7 @@ services:
condition: on-failure
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",

View File

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

View File

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

View File

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

View File

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

View File

@ -160,6 +160,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/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)

View File

@ -0,0 +1,51 @@
package api
import (
"github.com/gorilla/mux"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
"go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/model"
"net/http"
)
func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
ah.lockUnlockDashboard(w, r, true)
}
func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) {
ah.lockUnlockDashboard(w, r, false)
}
func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) {
// Locking can only be done by the owner of the dashboard
// or an admin
// - Fetch the dashboard
// - Check if the user is the owner or an admin
// - If yes, lock/unlock the dashboard
// - If no, return 403
// Get the dashboard UUID from the request
uuid := mux.Vars(r)["uuid"]
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
return
}
user := common.GetUserFromContext(r.Context())
if !auth.IsAdmin(user) && (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) {
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, "You are not authorized to lock/unlock this dashboard")
return
}
// Lock/Unlock the dashboard
err = dashboards.LockUnlockDashboard(r.Context(), uuid, lock)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
return
}
ah.Respond(w, "Dashboard updated successfully")
}

View File

@ -52,7 +52,6 @@ func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
}
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
interface LockDashboardProps {
uuid: string;
}
const lockDashboard = (props: LockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/lock`);
export default lockDashboard;

View File

@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
interface UnlockDashboardProps {
uuid: string;
}
const unlockDashboard = (props: UnlockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/unlock`);
export default unlockDashboard;

View File

@ -3,10 +3,10 @@
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
<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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,141 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './uplot.scss';
import { Typography } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types';
import {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
useRef,
} from 'react';
import UPlot from 'uplot';
import { dataMatch, optionsUpdateState } from './utils';
export interface UplotProps {
options: uPlot.Options;
data: uPlot.AlignedData;
onDelete?: (chart: uPlot) => void;
onCreate?: (chart: uPlot) => void;
resetScales?: boolean;
}
const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
(
{ options, data, onDelete, onCreate, resetScales = true },
ref,
): JSX.Element | null => {
const chartRef = useRef<uPlot | null>(null);
const propOptionsRef = useRef(options);
const targetRef = useRef<HTMLDivElement>(null);
const propDataRef = useRef(data);
const onCreateRef = useRef(onCreate);
const onDeleteRef = useRef(onDelete);
useImperativeHandle(
ref,
(): ToggleGraphProps => ({
toggleGraph(graphIndex: number, isVisible: boolean): void {
chartRef.current?.setSeries(graphIndex, { show: isVisible });
},
}),
);
useEffect(() => {
onCreateRef.current = onCreate;
onDeleteRef.current = onDelete;
});
const destroy = useCallback((chart: uPlot | null) => {
if (chart) {
onDeleteRef.current?.(chart);
chart.destroy();
chartRef.current = null;
}
}, []);
const create = useCallback(() => {
if (targetRef.current === null) return;
// If data is empty, hide cursor
if (data && data[0] && data[0]?.length === 0) {
propOptionsRef.current = {
...propOptionsRef.current,
cursor: { show: false },
};
}
const newChart = new UPlot(
propOptionsRef.current,
propDataRef.current,
targetRef.current,
);
chartRef.current = newChart;
onCreateRef.current?.(newChart);
}, [data]);
useEffect(() => {
create();
return (): void => {
destroy(chartRef.current);
};
}, [create, destroy]);
useEffect(() => {
if (propOptionsRef.current !== options) {
const optionsState = optionsUpdateState(propOptionsRef.current, options);
propOptionsRef.current = options;
if (!chartRef.current || optionsState === 'create') {
destroy(chartRef.current);
create();
} else if (optionsState === 'update') {
chartRef.current.setSize({
width: options.width,
height: options.height,
});
}
}
}, [options, create, destroy]);
useEffect(() => {
if (propDataRef.current !== data) {
if (!chartRef.current) {
propDataRef.current = data;
create();
} else if (!dataMatch(propDataRef.current, data)) {
if (resetScales) {
chartRef.current.setData(data, true);
} else {
chartRef.current.setData(data, false);
chartRef.current.redraw();
}
}
propDataRef.current = data;
}
}, [data, resetScales, create]);
return (
<div className="uplot-graph-container" ref={targetRef}>
{data && data[0] && data[0]?.length === 0 ? (
<div className="not-found">
<Typography>No Data</Typography>
</div>
) : null}
</div>
);
},
);
Uplot.displayName = 'Uplot';
Uplot.defaultProps = {
onDelete: undefined,
onCreate: undefined,
resetScales: true,
};
export default memo(Uplot);

View File

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

View File

@ -0,0 +1,15 @@
.not-found {
display: flex;
justify-content: center;
align-items: center;
z-index: 0;
height: 85%;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
}
.uplot-graph-container {
height: 100%;
width: 100%;
}

View File

@ -0,0 +1,48 @@
import uPlot from 'uplot';
type OptionsUpdateState = 'keep' | 'update' | 'create';
export const optionsUpdateState = (
_lhs: uPlot.Options,
_rhs: uPlot.Options,
): OptionsUpdateState => {
const { width: lhsWidth, height: lhsHeight, ...lhs } = _lhs;
const { width: rhsWidth, height: rhsHeight, ...rhs } = _rhs;
let state: OptionsUpdateState = 'keep';
if (lhsHeight !== rhsHeight || lhsWidth !== rhsWidth) {
state = 'update';
}
if (Object.keys(lhs).length !== Object.keys(rhs).length) {
return 'create';
}
// eslint-disable-next-line no-restricted-syntax
for (const k of Object.keys(lhs)) {
if (!Object.is((lhs as any)[k], (rhs as any)[k])) {
state = 'create';
break;
}
}
return state;
};
export const dataMatch = (
lhs: uPlot.AlignedData,
rhs: uPlot.AlignedData,
): boolean => {
if (lhs.length !== rhs.length) {
return false;
}
return lhs.every((lhsOneSeries, seriesIdx) => {
const rhsOneSeries = rhs[seriesIdx];
if (lhsOneSeries.length !== rhsOneSeries.length) {
return false;
}
// compare each value in the series
return (lhsOneSeries as number[])?.every(
(value, valueIdx) => value === rhsOneSeries[valueIdx],
);
});
};

View File

@ -0,0 +1,31 @@
.value-graph-container {
width: 50%;
height: 50%;
max-width: 200px;
max-height: 200px;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.value-graph-text {
font-size: 2.5vw;
text-align: center;
}
.value-graph-bgconflict {
position: absolute;
right: 10px;
bottom: 10px;
}
.value-graph-textconflict {
margin-left: 10px;
margin-top: 20px;
}
.value-graph-icon {
color: #E89A3C;
}
}

View File

@ -1,11 +1,66 @@
import { Value } from './styles';
import './ValueGraph.styles.scss';
function ValueGraph({ value }: ValueGraphProps): JSX.Element {
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;

View File

@ -1,7 +0,0 @@
import { Typography } from 'antd';
import styled from 'styled-components';
export const Value = styled(Typography)`
font-size: 2.5vw;
text-align: center;
`;

View File

@ -0,0 +1,97 @@
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
function compareThreshold(
rawValue: number,
threshold: ThresholdProps,
): boolean {
if (
threshold.thresholdOperator === undefined ||
threshold.thresholdValue === undefined
) {
return false;
}
switch (threshold.thresholdOperator) {
case '>':
return rawValue > threshold.thresholdValue;
case '>=':
return rawValue >= threshold.thresholdValue;
case '<':
return rawValue < threshold.thresholdValue;
case '<=':
return rawValue <= threshold.thresholdValue;
case '=':
return rawValue === threshold.thresholdValue;
default:
return false;
}
}
function extractNumbersFromString(inputString: string): number[] {
const regex = /[+-]?\d+(\.\d+)?/g;
const matches = inputString.match(regex);
if (matches) {
return matches.map(Number);
}
return [];
}
function getHighestPrecedenceThreshold(
matchingThresholds: ThresholdProps[],
thresholds: ThresholdProps[],
): ThresholdProps | null {
if (matchingThresholds.length === 0) {
return null;
}
// whichever threshold from matchingThresholds is found first in thresholds array return the threshold from thresholds array
let highestPrecedenceThreshold = matchingThresholds[0];
for (let i = 1; i < matchingThresholds.length; i += 1) {
if (
thresholds.indexOf(matchingThresholds[i]) <
thresholds.indexOf(highestPrecedenceThreshold)
) {
highestPrecedenceThreshold = matchingThresholds[i];
}
}
return highestPrecedenceThreshold;
}
export function getBackgroundColorAndThresholdCheck(
thresholds: ThresholdProps[],
rawValue: number,
): {
threshold: ThresholdProps;
isConflictingThresholds: boolean;
} {
const matchingThresholds = thresholds.filter((threshold) =>
compareThreshold(
extractNumbersFromString(
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
)[0],
threshold,
),
);
if (matchingThresholds.length === 0) {
return {
threshold: {} as ThresholdProps,
isConflictingThresholds: false,
};
}
const highestPrecedenceThreshold = getHighestPrecedenceThreshold(
matchingThresholds,
thresholds,
);
const isConflictingThresholds = matchingThresholds.length > 1;
return {
threshold: highestPrecedenceThreshold || ({} as ThresholdProps),
isConflictingThresholds,
};
}

View File

@ -1,11 +1,11 @@
import Graph from 'components/Graph';
import Uplot from 'components/Uplot';
import GridTableComponent from 'container/GridTableComponent';
import 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,

View File

@ -9,7 +9,6 @@ const themeColors = {
silver: '#BDBDBD',
outrageousOrange: '#FF6633',
roseBud: '#FFB399',
magentaPink: '#FF33FF',
canary: '#FFFF99',
deepSkyBlue: '#00B3E6',
goldTips: '#E6B333',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
.graph-manager-container {
margin-top: 1.25rem;
display: flex;
align-items: flex-end;
overflow-x: scroll;
.filter-table-container {
flex-basis: 80%;
}
.save-cancel-container {
flex-basis: 20%;
display: flex;
justify-content: flex-end;
}
.save-cancel-button {
margin: 0 0.313rem;
}
}

View File

@ -1,4 +1,4 @@
import './GraphManager.styles.scss';
import './WidgetFullView.styles.scss';
import { Button, Input } from 'antd';
import { 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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
.full-view-container {
height: 80vh;
overflow-x: auto;
overflow-y: hidden;
.full-view-header-container {
height: 40px;
}
.graph-container {
height: calc(60% - 40px);
min-height: 300px;
border: 1px solid #333;
width: 100%;
padding: 12px;
box-sizing: border-box;
margin: 16px 0;
border-radius: 3px;
}
.graph-manager-container {
height: calc(40% - 40px);
.graph-manager-header {
display: flex;
margin-bottom: 16px;
}
.legends-list-container {
width: 100%;
overflow: hidden;
overflow-y: auto;
}
.save-cancel-container {
flex-basis: 20%;
display: flex;
justify-content: flex-end;
}
.save-cancel-button {
margin: 0 0.313rem;
}
}
}

View File

@ -1,3 +1,6 @@
import './WidgetFullView.styles.scss';
import { SyncOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { 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>
);
}

View File

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

View File

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

View File

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

View File

@ -1,62 +0,0 @@
import { mockTestData } from './__mock__/mockChartData';
import { mocklegendEntryResult } from './__mock__/mockLegendEntryData';
import { showAllDataSet } from './FullView/utils';
import { getGraphVisibilityStateOnDataChange } from './utils';
describe('getGraphVisibilityStateOnDataChange', () => {
beforeEach(() => {
const localStorageMock = {
getItem: jest.fn(),
};
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
});
it('should return the correct visibility state and legend entry', () => {
// Mock the localStorage behavior
const mockLocalStorageData = [
{
name: 'exampleexpanded',
dataIndex: [
{ label: 'customer', show: true },
{ label: 'demo-app', show: false },
],
},
];
jest
.spyOn(window.localStorage, 'getItem')
.mockReturnValue(JSON.stringify(mockLocalStorageData));
const result1 = getGraphVisibilityStateOnDataChange({
data: mockTestData,
isExpandedName: true,
name: 'example',
});
expect(result1.graphVisibilityStates).toEqual([true, false]);
expect(result1.legendEntry).toEqual(mocklegendEntryResult);
const result2 = getGraphVisibilityStateOnDataChange({
data: mockTestData,
isExpandedName: false,
name: 'example',
});
expect(result2.graphVisibilityStates).toEqual(
Array(mockTestData.datasets.length).fill(true),
);
expect(result2.legendEntry).toEqual(showAllDataSet(mockTestData));
});
it('should return default values if localStorage data is not available', () => {
// Mock the localStorage behavior to return null
jest.spyOn(window.localStorage, 'getItem').mockReturnValue(null);
const result = getGraphVisibilityStateOnDataChange({
data: mockTestData,
isExpandedName: true,
name: 'example',
});
expect(result.graphVisibilityStates).toEqual(
Array(mockTestData.datasets.length).fill(true),
);
expect(result.legendEntry).toEqual(showAllDataSet(mockTestData));
});
});

View File

@ -25,21 +25,22 @@ import { v4 } from 'uuid';
import WidgetHeader from '../WidgetHeader';
import 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,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -77,7 +77,7 @@ function ApDexMetrics({
const isQueryEnabled =
topLevelOperationsRoute.length > 0 &&
metricsBuckets &&
!!metricsBuckets &&
metricsBuckets?.length > 0 &&
delta !== undefined;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
.dashboard-description {
display: -webkit-box;
-webkit-line-clamp: 2; /* Show up to 2 lines */
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}

View File

@ -5,7 +5,7 @@ import { useState } from 'react';
import DashboardSettingsContent from '../DashboardSettings';
import { 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}
>

View File

@ -1,4 +1,5 @@
import { Button, Modal, Typography } from 'antd';
import { CopyFilled, DownloadOutlined } from '@ant-design/icons';
import { Button, Modal } from 'antd';
import Editor from 'components/Editor';
import { 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>
);
}

View File

@ -0,0 +1,122 @@
import './Description.styles.scss';
import { LockFilled, ShareAltOutlined, UnlockFilled } from '@ant-design/icons';
import { Button, Card, Col, Row, Space, Tag, Tooltip, Typography } from 'antd';
import useComponentPermission from 'hooks/useComponentPermission';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DashboardData } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import DashboardVariableSelection from '../DashboardVariablesSelection';
import SettingsDrawer from './SettingsDrawer';
import ShareModal from './ShareModal';
function DashboardDescription(): JSX.Element {
const {
selectedDashboard,
isDashboardLocked,
handleDashboardLockToggle,
} = useDashboard();
const selectedData = selectedDashboard?.data || ({} as DashboardData);
const { title = '', tags, description } = selectedData || {};
const [openDashboardJSON, setOpenDashboardJSON] = useState<boolean>(false);
const { t } = useTranslation('common');
const { user, role } = useSelector<AppState, AppReducer>((state) => state.app);
const [editDashboard] = useComponentPermission(['edit_dashboard'], role);
let isAuthor = false;
if (selectedDashboard && user && user.email) {
isAuthor = selectedDashboard?.created_by === user?.email;
}
const onToggleHandler = (): void => {
setOpenDashboardJSON((state) => !state);
};
const handleLockDashboardToggle = (): void => {
handleDashboardLockToggle(!isDashboardLocked);
};
return (
<Card>
<Row gutter={16}>
<Col flex={1} span={12}>
<Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
{isDashboardLocked && (
<Tooltip title="Dashboard Locked" placement="top">
<LockFilled /> &nbsp;
</Tooltip>
)}
{title}
</Typography.Title>
{description && (
<Typography className="dashboard-description">{description}</Typography>
)}
{tags && (
<div style={{ margin: '0.5rem 0' }}>
{tags?.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</div>
)}
</Col>
<Col span={8}>
<Row justify="end">
<DashboardVariableSelection />
</Row>
</Col>
<Col span={4} style={{ textAlign: 'right' }}>
{selectedData && (
<ShareModal
isJSONModalVisible={openDashboardJSON}
onToggleHandler={onToggleHandler}
selectedData={selectedData}
/>
)}
<Space direction="vertical">
{!isDashboardLocked && editDashboard && (
<SettingsDrawer drawerTitle={title} />
)}
<Button
style={{ width: '100%' }}
type="dashed"
onClick={onToggleHandler}
icon={<ShareAltOutlined />}
>
{t('share')}
</Button>
{(isAuthor || role === USER_ROLES.ADMIN) && (
<Tooltip
placement="left"
title={isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
>
<Button
style={{ width: '100%' }}
type="dashed"
onClick={handleLockDashboardToggle}
icon={isDashboardLocked ? <LockFilled /> : <UnlockFilled />}
>
{isDashboardLocked ? 'Unlock' : 'Lock'}
</Button>
</Tooltip>
)}
</Space>
</Col>
</Row>
</Card>
);
}
export default DashboardDescription;

View File

@ -14,7 +14,11 @@ export const Button = styled(ButtonComponent)`
export const DrawerContainer = styled(Drawer)`
.ant-drawer-header {
padding: 0;
padding: 16px;
border: none;
}
.ant-drawer-body {
padding-top: 0;
}
`;

View File

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

View File

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

View File

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

View File

@ -1,74 +0,0 @@
import { ShareAltOutlined } from '@ant-design/icons';
import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd';
import useComponentPermission from 'hooks/useComponentPermission';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import DashboardVariableSelection from '../DashboardVariablesSelection';
import SettingsDrawer from './SettingsDrawer';
import ShareModal from './ShareModal';
function DescriptionOfDashboard(): JSX.Element {
const { selectedDashboard } = useDashboard();
const selectedData = selectedDashboard?.data;
const { title, tags, description } = selectedData || {};
const [isJSONModalVisible, isIsJSONModalVisible] = useState<boolean>(false);
const { t } = useTranslation('common');
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [editDashboard] = useComponentPermission(['edit_dashboard'], role);
const onToggleHandler = (): void => {
isIsJSONModalVisible((state) => !state);
};
return (
<Card>
<Row>
<Col flex={1}>
<Typography.Title level={4} style={{ padding: 0, margin: 0 }}>
{title}
</Typography.Title>
<Typography>{description}</Typography>
<div style={{ margin: '0.5rem 0' }}>
{tags?.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</div>
<DashboardVariableSelection />
</Col>
<Col>
{selectedData && (
<ShareModal
isJSONModalVisible={isJSONModalVisible}
onToggleHandler={onToggleHandler}
selectedData={selectedData}
/>
)}
<Space direction="vertical">
{editDashboard && <SettingsDrawer />}
<Button
style={{ width: '100%' }}
type="dashed"
onClick={onToggleHandler}
icon={<ShareAltOutlined />}
>
{t('share')}
</Button>
</Space>
</Col>
</Row>
</Card>
);
}
export default DescriptionOfDashboard;

View File

@ -1,4 +1,4 @@
import Description from './DescriptionOfDashboard';
import Description from './DashboardDescription';
import GridGraphs from './GridGraphs';
function NewDashboard(): JSX.Element {

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