mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-01 07:22:01 +08:00
commit
d0feff00a7
@ -133,7 +133,7 @@ services:
|
||||
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:0.23.4
|
||||
image: signoz/alertmanager:0.23.5
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
command:
|
||||
@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.40.0
|
||||
image: signoz/query-service:0.41.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.40.0
|
||||
image: signoz/frontend:0.41.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.88.14
|
||||
image: signoz/signoz-otel-collector:0.88.15
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.88.14
|
||||
image: signoz/signoz-schema-migrator:0.88.15
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
@ -54,7 +54,7 @@ services:
|
||||
|
||||
alertmanager:
|
||||
container_name: signoz-alertmanager
|
||||
image: signoz/alertmanager:0.23.4
|
||||
image: signoz/alertmanager:0.23.5
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
depends_on:
|
||||
@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.14}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.15}
|
||||
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.88.14
|
||||
image: signoz/signoz-otel-collector:0.88.15
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
|
@ -149,7 +149,7 @@ services:
|
||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.4}
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
|
||||
container_name: signoz-alertmanager
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
@ -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.40.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.41.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.40.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.41.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.88.14}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.15}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.14}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.15}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/license"
|
||||
"go.signoz.io/signoz/ee/query-service/usage"
|
||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/integrations"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
@ -31,6 +32,7 @@ type APIHandlerOptions struct {
|
||||
UsageManager *usage.Manager
|
||||
FeatureFlags baseint.FeatureLookup
|
||||
LicenseManager *license.Manager
|
||||
IntegrationsController *integrations.Controller
|
||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||
Cache cache.Cache
|
||||
// Querier Influx Interval
|
||||
@ -56,6 +58,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
||||
AppDao: opts.AppDao,
|
||||
RuleManager: opts.RulesManager,
|
||||
FeatureFlags: opts.FeatureFlags,
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
Cache: opts.Cache,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
|
@ -12,6 +12,20 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type DayWiseBreakdown struct {
|
||||
Type string `json:"type"`
|
||||
Breakdown []DayWiseData `json:"breakdown"`
|
||||
}
|
||||
|
||||
type DayWiseData struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Count float64 `json:"count"`
|
||||
Size float64 `json:"size"`
|
||||
UnitPrice float64 `json:"unitPrice"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Total float64 `json:"total"`
|
||||
}
|
||||
|
||||
type tierBreakdown struct {
|
||||
UnitPrice float64 `json:"unitPrice"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
@ -21,9 +35,10 @@ type tierBreakdown struct {
|
||||
}
|
||||
|
||||
type usageResponse struct {
|
||||
Type string `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
Tiers []tierBreakdown `json:"tiers"`
|
||||
Type string `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
Tiers []tierBreakdown `json:"tiers"`
|
||||
DayWiseBreakdown DayWiseBreakdown `json:"dayWiseBreakdown"`
|
||||
}
|
||||
|
||||
type details struct {
|
||||
|
@ -35,6 +35,7 @@ import (
|
||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/integrations"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
||||
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
||||
@ -171,13 +172,22 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
// initiate opamp
|
||||
_, err = opAmpModel.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||
_, err = opAmpModel.InitDB(localDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integrationsController, err := integrations.NewController(localDB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't create integrations controller: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
// ingestion pipelines manager
|
||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(localDB, "sqlite")
|
||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
||||
localDB, "sqlite", integrationsController.GetPipelinesForInstalledIntegrations,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -233,6 +243,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
UsageManager: usageManager,
|
||||
FeatureFlags: lm,
|
||||
LicenseManager: lm,
|
||||
IntegrationsController: integrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
Cache: c,
|
||||
FluxInterval: fluxInterval,
|
||||
@ -278,6 +289,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
||||
|
||||
r := mux.NewRouter()
|
||||
|
||||
r.Use(baseapp.LogCommentEnricher)
|
||||
r.Use(setTimeoutMiddleware)
|
||||
r.Use(s.analyticsMiddleware)
|
||||
r.Use(loggingMiddlewarePrivate)
|
||||
@ -310,6 +322,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
}
|
||||
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
||||
|
||||
r.Use(baseapp.LogCommentEnricher)
|
||||
r.Use(setTimeoutMiddleware)
|
||||
r.Use(s.analyticsMiddleware)
|
||||
r.Use(loggingMiddleware)
|
||||
@ -317,6 +330,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
apiHandler.RegisterMetricsRoutes(r, am)
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
|
||||
@ -412,7 +426,7 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
|
||||
data["queryType"] = postData.CompositeQuery.QueryType
|
||||
data["panelType"] = postData.CompositeQuery.PanelType
|
||||
|
||||
signozLogsUsed, signozMetricsUsed = telemetry.GetInstance().CheckSigNozSignals(postData)
|
||||
signozLogsUsed, signozMetricsUsed, _ = telemetry.GetInstance().CheckSigNozSignals(postData)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +90,13 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelEmail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: false,
|
||||
@ -177,6 +184,13 @@ var ProPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelEmail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: true,
|
||||
@ -264,6 +278,13 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelEmail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: true,
|
||||
@ -279,17 +300,17 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: Onboarding,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
Name: Onboarding,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: ChatSupport,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
Name: ChatSupport,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
@ -107,6 +107,7 @@
|
||||
"react-virtuoso": "4.0.3",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rehype-raw": "7.0.0",
|
||||
"stream": "^0.0.2",
|
||||
"style-loader": "1.3.0",
|
||||
"styled-components": "^5.3.11",
|
||||
@ -203,6 +204,7 @@
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"lint-staged": "^12.5.0",
|
||||
"msw": "1.3.2",
|
||||
"npm-run-all": "latest",
|
||||
"portfinder-sync": "^0.0.2",
|
||||
"prettier": "2.2.1",
|
||||
"raw-loader": "4.0.2",
|
||||
@ -216,8 +218,7 @@
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"npm-run-all": "latest"
|
||||
"webpack-cli": "^4.9.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.(js|jsx|ts|tsx)": [
|
||||
|
1
frontend/public/Icons/redis-logo.svg
Normal file
1
frontend/public/Icons/redis-logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23.06 17.526c-1.281.668-7.916 3.396-9.328 4.132-1.413.736-2.198.73-3.314.196C9.303 21.32 2.242 18.468.97 17.86c-.636-.303-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089 1.263.463 8.814 1.826 10.062 2.283v2.391c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 15.114c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.302 18.91 2.242 16.056.97 15.45c-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.546 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M23.06 13.6c-1.281.668-7.916 3.396-9.328 4.133-1.413.736-2.198.73-3.314.196S2.242 14.543.97 13.935c-.636-.304-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089C15.2 8.547 22.752 9.91 24 10.366v2.392c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 11.19c-1.281.667-7.916 3.395-9.329 4.131-1.412.737-2.197.73-3.313.196-1.116-.533-8.176-3.386-9.448-3.993-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.545 1.248.458 1.296.835.015 1.502z" fill="#C6302B"/><path d="M23.06 9.53c-1.281.668-7.916 3.396-9.328 4.132-1.413.737-2.198.73-3.314.196-1.116-.533-8.176-3.386-9.448-3.993C.334 9.56 0 9.305 0 9.062V6.636s9.192-2 10.676-2.533c1.484-.533 1.999-.552 3.262-.09C15.2 4.477 22.752 5.84 24 6.297v2.392c0 .24-.288.502-.94.842z" fill="#912626"/><path d="M23.06 7.118c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.303 10.913 2.242 8.061.97 7.453-.302 6.845-.328 6.427.921 5.937c1.25-.489 8.271-3.244 9.755-3.776 1.484-.532 1.999-.552 3.262-.089 1.263.463 7.858 3.088 9.106 3.545 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M14.933 4.758l-2.064.215-.462 1.111-.746-1.24L9.28 4.63l1.778-.641-.534-.985 1.665.651 1.569-.513-.424 1.017 1.6.6zm-2.649 5.393l-3.85-1.597 5.517-.847-1.667 2.444zM6.945 5.376c1.63 0 2.95.512 2.95 1.143 0 .632-1.32 1.144-2.95 1.144-1.629 0-2.95-.512-2.95-1.144 0-.63 1.321-1.143 2.95-1.143z" fill="#fff"/><path d="M17.371 5.062l3.266 1.29-3.263 1.29-.003-2.58z" fill="#621B1C"/><path d="M13.758 6.492l3.613-1.43.003 2.58-.354.139-3.262-1.29z" fill="#9A2928"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
@ -111,5 +111,7 @@
|
||||
"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",
|
||||
"text_alert_on_absent": "Send a notification if data is missing for",
|
||||
"text_for": "minutes",
|
||||
"selected_query_placeholder": "Select query"
|
||||
}
|
||||
|
@ -25,5 +25,5 @@
|
||||
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
|
||||
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||
"your_graph_build_with": "Your graph built with",
|
||||
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
"dashboard_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
}
|
||||
|
@ -111,5 +111,7 @@
|
||||
"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",
|
||||
"text_alert_on_absent": "Send a notification if data is missing for",
|
||||
"text_for": "minutes",
|
||||
"selected_query_placeholder": "Select query"
|
||||
}
|
||||
|
@ -23,6 +23,12 @@
|
||||
"field_opsgenie_api_key": "API Key",
|
||||
"field_opsgenie_description": "Description",
|
||||
"placeholder_opsgenie_description": "Description",
|
||||
"help_email_to": "Email address(es) to send alerts to (comma separated)",
|
||||
"field_email_to": "To",
|
||||
"placeholder_email_to": "To",
|
||||
"help_email_html": "Send email in html format",
|
||||
"field_email_html": "Email body template",
|
||||
"placeholder_email_html": "Email body template",
|
||||
"field_webhook_username": "User Name (optional)",
|
||||
"field_webhook_password": "Password (optional)",
|
||||
"field_pager_routing_key": "Routing Key",
|
||||
|
@ -28,5 +28,5 @@
|
||||
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
|
||||
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||
"your_graph_build_with": "Your graph built with",
|
||||
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
"dashboard_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
}
|
||||
|
@ -4,6 +4,10 @@
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"GET_STARTED": "SigNoz | Get Started",
|
||||
"GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM",
|
||||
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
|
||||
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
|
||||
"GET_STARTED_AWS_MONITORING": "SigNoz | Get Started | AWS",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
@ -40,8 +44,9 @@
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||
"SUPPORT": "SigNoz | Support",
|
||||
"LOGS_SAVE_VIEWS": "SigNoz | Logs Save Views",
|
||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Save Views",
|
||||
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
|
||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||
"SHORTCUTS": "SigNoz | Shortcuts"
|
||||
"SHORTCUTS": "SigNoz | Shortcuts",
|
||||
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations"
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ done
|
||||
# create temporary tsconfig which includes only passed files
|
||||
str="{
|
||||
\"extends\": \"./tsconfig.json\",
|
||||
\"include\": [\"src/types/global.d.ts\",\"src/typings/window.ts\", \"src/typings/chartjs-adapter-date-fns.d.ts\", \"src/typings/environment.ts\" ,\"src/container/OnboardingContainer/typings.d.ts\",$files]
|
||||
\"include\": [ \"src/typings/**/*.ts\",\"src/**/*.d.ts\", \"./babel.config.js\", \"./jest.config.ts\", \"./.eslintrc.js\",\"./__mocks__\",\"./conf/default.conf\",\"./public\",\"./tests\",\"./playwright.config.ts\",\"./commitlint.config.ts\",\"./webpack.config.js\",\"./webpack.config.prod.js\",\"./jest.setup.ts\",\"./**/*.d.ts\",$files]
|
||||
}"
|
||||
echo $str > tsconfig.tmp
|
||||
|
||||
|
@ -190,3 +190,18 @@ export const WorkspaceBlocked = Loadable(
|
||||
export const ShortcutsPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
||||
);
|
||||
|
||||
export const InstalledIntegrations = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
|
||||
),
|
||||
);
|
||||
|
||||
export const IntegrationsMarketPlace = Loadable(
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
|
||||
),
|
||||
);
|
||||
|
@ -1,6 +1,4 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import Shortcuts from 'pages/Shortcuts/Shortcuts';
|
||||
import WorkspaceBlocked from 'pages/WorkspaceLocked';
|
||||
import { RouteProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
@ -16,6 +14,8 @@ import {
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
IngestionSettings,
|
||||
InstalledIntegrations,
|
||||
IntegrationsMarketPlace,
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LiveLogs,
|
||||
@ -35,6 +35,7 @@ import {
|
||||
ServiceMetricsPage,
|
||||
ServicesTablePage,
|
||||
SettingsPage,
|
||||
ShortcutsPage,
|
||||
SignupPage,
|
||||
SomethingWentWrong,
|
||||
StatusPage,
|
||||
@ -45,6 +46,7 @@ import {
|
||||
TracesSaveViews,
|
||||
UnAuthorized,
|
||||
UsageExplorerPage,
|
||||
WorkspaceBlocked,
|
||||
} from './pageComponents';
|
||||
|
||||
const routes: AppRoutes[] = [
|
||||
@ -57,7 +59,7 @@ const routes: AppRoutes[] = [
|
||||
},
|
||||
{
|
||||
path: ROUTES.GET_STARTED,
|
||||
exact: true,
|
||||
exact: false,
|
||||
component: Onboarding,
|
||||
isPrivate: true,
|
||||
key: 'GET_STARTED',
|
||||
@ -331,10 +333,24 @@ const routes: AppRoutes[] = [
|
||||
{
|
||||
path: ROUTES.SHORTCUTS,
|
||||
exact: true,
|
||||
component: Shortcuts,
|
||||
component: ShortcutsPage,
|
||||
isPrivate: true,
|
||||
key: 'SHORTCUTS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS_INSTALLED,
|
||||
exact: true,
|
||||
component: InstalledIntegrations,
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS_INSTALLED',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INTEGRATIONS_MARKETPLACE,
|
||||
exact: true,
|
||||
component: IntegrationsMarketPlace,
|
||||
isPrivate: true,
|
||||
key: 'INTEGRATIONS_MARKETPLACE',
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORT_ROUTE: AppRoutes = {
|
||||
@ -358,6 +374,8 @@ export const oldRoutes = [
|
||||
'/logs/old-logs-explorer',
|
||||
'/logs-explorer',
|
||||
'/logs-explorer/live',
|
||||
'/logs-save-views',
|
||||
'/traces-save-views',
|
||||
'/settings/api-keys',
|
||||
];
|
||||
|
||||
@ -366,6 +384,8 @@ export const oldNewRoutesMapping: Record<string, string> = {
|
||||
'/logs/old-logs-explorer': '/logs/old-logs-explorer',
|
||||
'/logs-explorer': '/logs/logs-explorer',
|
||||
'/logs-explorer/live': '/logs/logs-explorer/live',
|
||||
'/logs-save-views': '/logs/saved-views',
|
||||
'/traces-save-views': '/traces/saved-views',
|
||||
'/settings/api-keys': '/settings/access-tokens',
|
||||
};
|
||||
|
||||
|
7
frontend/src/api/Integrations/getAllIntegrations.ts
Normal file
7
frontend/src/api/Integrations/getAllIntegrations.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { AllIntegrationsProps } from 'types/api/integrations/types';
|
||||
|
||||
export const getAllIntegrations = (): Promise<
|
||||
AxiosResponse<AllIntegrationsProps>
|
||||
> => axios.get(`/integrations`);
|
11
frontend/src/api/Integrations/getIntegration.ts
Normal file
11
frontend/src/api/Integrations/getIntegration.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import {
|
||||
GetIntegrationPayloadProps,
|
||||
GetIntegrationProps,
|
||||
} from 'types/api/integrations/types';
|
||||
|
||||
export const getIntegration = (
|
||||
props: GetIntegrationPayloadProps,
|
||||
): Promise<AxiosResponse<GetIntegrationProps>> =>
|
||||
axios.get(`/integrations/${props.integrationId}`);
|
11
frontend/src/api/Integrations/getIntegrationStatus.ts
Normal file
11
frontend/src/api/Integrations/getIntegrationStatus.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import {
|
||||
GetIntegrationPayloadProps,
|
||||
GetIntegrationStatusProps,
|
||||
} from 'types/api/integrations/types';
|
||||
|
||||
export const getIntegrationStatus = (
|
||||
props: GetIntegrationPayloadProps,
|
||||
): Promise<AxiosResponse<GetIntegrationStatusProps>> =>
|
||||
axios.get(`/integrations/${props.integrationId}/connection_status`);
|
31
frontend/src/api/Integrations/installIntegration.ts
Normal file
31
frontend/src/api/Integrations/installIntegration.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
InstalledIntegrationsSuccessResponse,
|
||||
InstallIntegrationKeyProps,
|
||||
} from 'types/api/integrations/types';
|
||||
|
||||
const installIntegration = async (
|
||||
props: InstallIntegrationKeyProps,
|
||||
): Promise<
|
||||
SuccessResponse<InstalledIntegrationsSuccessResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.post('/integrations/install', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default installIntegration;
|
31
frontend/src/api/Integrations/uninstallIntegration.ts
Normal file
31
frontend/src/api/Integrations/uninstallIntegration.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
UninstallIntegrationProps,
|
||||
UninstallIntegrationSuccessResponse,
|
||||
} from 'types/api/integrations/types';
|
||||
|
||||
const unInstallIntegration = async (
|
||||
props: UninstallIntegrationProps,
|
||||
): Promise<
|
||||
SuccessResponse<UninstallIntegrationSuccessResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.post('/integrations/uninstall', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default unInstallIntegration;
|
@ -2,6 +2,7 @@ const apiV1 = '/api/v1/';
|
||||
|
||||
export const apiV2 = '/api/v2/';
|
||||
export const apiV3 = '/api/v3/';
|
||||
export const apiV4 = '/api/v4/';
|
||||
export const apiAlertManager = '/api/alertmanager';
|
||||
|
||||
export default apiV1;
|
||||
|
34
frontend/src/api/channels/createEmail.ts
Normal file
34
frontend/src/api/channels/createEmail.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createEmail';
|
||||
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/channels', {
|
||||
name: props.name,
|
||||
email_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
to: props.to,
|
||||
html: props.html,
|
||||
headers: props.headers,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default create;
|
34
frontend/src/api/channels/editEmail.ts
Normal file
34
frontend/src/api/channels/editEmail.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/editEmail';
|
||||
|
||||
const editEmail = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/channels/${props.id}`, {
|
||||
name: props.name,
|
||||
email_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
to: props.to,
|
||||
html: props.html,
|
||||
headers: props.headers,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default editEmail;
|
34
frontend/src/api/channels/testEmail.ts
Normal file
34
frontend/src/api/channels/testEmail.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/channels/createEmail';
|
||||
|
||||
const testEmail = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/testChannel', {
|
||||
name: props.name,
|
||||
email_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
to: props.to,
|
||||
html: props.html,
|
||||
headers: props.headers,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default testEmail;
|
@ -9,7 +9,7 @@ import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import store from 'store';
|
||||
|
||||
import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1';
|
||||
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4 } from './apiV1';
|
||||
import { Logout } from './utils';
|
||||
|
||||
const interceptorsResponse = (
|
||||
@ -114,6 +114,7 @@ ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
export const ApiV3Instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
|
||||
});
|
||||
|
||||
ApiV3Instance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
@ -121,6 +122,18 @@ ApiV3Instance.interceptors.response.use(
|
||||
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
// axios V4
|
||||
export const ApiV4Instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV4}`,
|
||||
});
|
||||
|
||||
ApiV4Instance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
AxiosAlertManagerInstance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ApiV3Instance, ApiV4Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
MetricRangePayloadV3,
|
||||
@ -9,10 +10,23 @@ import {
|
||||
|
||||
export const getMetricsQueryRange = async (
|
||||
props: QueryRangePayload,
|
||||
version: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/query_range', props, { signal });
|
||||
if (version && version === ENTITY_VERSION_V4) {
|
||||
const response = await ApiV4Instance.post('/query_range', props, { signal });
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await ApiV3Instance.post('/query_range', props, { signal });
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
@ -115,6 +115,9 @@ function CustomTimePicker({
|
||||
|
||||
const handleOpenChange = (newOpen: boolean): void => {
|
||||
setOpen(newOpen);
|
||||
if (!newOpen) {
|
||||
setCustomDTPickerVisible?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedHandleInputChange = debounce((inputValue): void => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import './CustomTimePicker.styles.scss';
|
||||
|
||||
import { Button, DatePicker } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
@ -9,12 +9,10 @@ import {
|
||||
Option,
|
||||
RelativeDurationSuggestionOptions,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import RangePickerModal from './RangePickerModal';
|
||||
|
||||
interface CustomTimePickerPopoverContentProps {
|
||||
options: any[];
|
||||
@ -40,35 +38,12 @@ function CustomTimePickerPopoverContent({
|
||||
handleGoLive,
|
||||
selectedTime,
|
||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||
const { RangePicker } = DatePicker;
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const disabledDate = (current: Dayjs): boolean => {
|
||||
const currentDay = dayjs(current);
|
||||
return currentDay.isAfter(dayjs());
|
||||
};
|
||||
|
||||
const onPopoverClose = (visible: boolean): void => {
|
||||
if (!visible) {
|
||||
setCustomDTPickerVisible(false);
|
||||
}
|
||||
setIsOpen(visible);
|
||||
};
|
||||
|
||||
const onModalOkHandler = (date_time: any): void => {
|
||||
if (date_time?.[1]) {
|
||||
onPopoverClose(false);
|
||||
}
|
||||
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
||||
};
|
||||
function getTimeChips(options: Option[]): JSX.Element {
|
||||
return (
|
||||
<div className="relative-date-time-section">
|
||||
@ -105,26 +80,32 @@ function CustomTimePickerPopoverContent({
|
||||
}}
|
||||
className={cx(
|
||||
'date-time-options-btn',
|
||||
selectedTime === option.value && 'active',
|
||||
customDateTimeVisible
|
||||
? option.value === 'custom' && 'active'
|
||||
: selectedTime === option.value && 'active',
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative-date-time">
|
||||
<div
|
||||
className={cx(
|
||||
'relative-date-time',
|
||||
selectedTime === 'custom' || customDateTimeVisible
|
||||
? 'date-picker'
|
||||
: 'relative-times',
|
||||
)}
|
||||
>
|
||||
{selectedTime === 'custom' || customDateTimeVisible ? (
|
||||
<RangePicker
|
||||
disabledDate={disabledDate}
|
||||
allowClear
|
||||
onCalendarChange={onModalOkHandler}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(selectedTime === 'custom' && {
|
||||
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
|
||||
})}
|
||||
<RangePickerModal
|
||||
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||
setIsOpen={setIsOpen}
|
||||
onCustomDateHandler={onCustomDateHandler}
|
||||
selectedTime={selectedTime}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<div className="relative-times-container">
|
||||
<div className="time-heading">RELATIVE TIMES</div>
|
||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,4 @@
|
||||
.custom-date-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import './RangePickerModal.styles.scss';
|
||||
|
||||
import { DatePicker } from 'antd';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
interface RangePickerModalProps {
|
||||
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
onCustomDateHandler: (
|
||||
dateTimeRange: DateTimeRangeType,
|
||||
lexicalContext?: LexicalContext | undefined,
|
||||
) => void;
|
||||
selectedTime: string;
|
||||
}
|
||||
|
||||
function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
||||
const {
|
||||
setCustomDTPickerVisible,
|
||||
setIsOpen,
|
||||
onCustomDateHandler,
|
||||
selectedTime,
|
||||
} = props;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const disabledDate = (current: Dayjs): boolean => {
|
||||
const currentDay = dayjs(current);
|
||||
return currentDay.isAfter(dayjs());
|
||||
};
|
||||
|
||||
const onPopoverClose = (visible: boolean): void => {
|
||||
if (!visible) {
|
||||
setCustomDTPickerVisible(false);
|
||||
}
|
||||
setIsOpen(visible);
|
||||
};
|
||||
|
||||
const onModalOkHandler = (date_time: any): void => {
|
||||
if (date_time?.[1]) {
|
||||
onPopoverClose(false);
|
||||
}
|
||||
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
||||
};
|
||||
return (
|
||||
<div className="custom-date-picker">
|
||||
<RangePicker
|
||||
disabledDate={disabledDate}
|
||||
allowClear
|
||||
showTime
|
||||
onOk={onModalOkHandler}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(selectedTime === 'custom' && {
|
||||
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RangePickerModal;
|
@ -18,6 +18,8 @@
|
||||
}
|
||||
|
||||
.ant-drawer-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
// utils
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@ -19,9 +20,8 @@ import { ILog } from 'types/api/logs/log';
|
||||
// components
|
||||
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
||||
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||
import LogStateIndicator, {
|
||||
LogType,
|
||||
} from '../LogStateIndicator/LogStateIndicator';
|
||||
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorType } from '../LogStateIndicator/utils';
|
||||
// styles
|
||||
import {
|
||||
Container,
|
||||
@ -114,6 +114,8 @@ function ListLogView({
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
} = useActiveLog();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const handlerClearActiveContextLog = useCallback(
|
||||
(event: React.MouseEvent | React.KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
@ -149,7 +151,7 @@ function ListLogView({
|
||||
[flattenLogData.timestamp],
|
||||
);
|
||||
|
||||
const logType = logData?.attributes_string?.log_level || LogType.INFO;
|
||||
const logType = getLogIndicatorType(logData);
|
||||
|
||||
const handleMouseEnter = (): void => {
|
||||
setHasActionButtons(true);
|
||||
@ -163,6 +165,7 @@ function ListLogView({
|
||||
<>
|
||||
<Container
|
||||
$isActiveLog={isHighlighted}
|
||||
$isDarkMode={isDarkMode}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleDetailedView}
|
||||
|
@ -1,18 +1,24 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Card, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground } from 'utils/logs';
|
||||
|
||||
export const Container = styled(Card)<{
|
||||
$isActiveLog: boolean;
|
||||
$isDarkMode: boolean;
|
||||
}>`
|
||||
width: 100% !important;
|
||||
margin-bottom: 0.3rem;
|
||||
cursor: pointer;
|
||||
.ant-card-body {
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
|
||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||
${({ $isActiveLog, $isDarkMode }): string =>
|
||||
$isActiveLog
|
||||
? `background-color: ${
|
||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
||||
} !important`
|
||||
: ''}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Text = styled(Typography.Text)`
|
||||
|
@ -10,15 +10,27 @@
|
||||
background-color: transparent;
|
||||
|
||||
&.INFO {
|
||||
background-color: #1d212d;
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
&.WARNING {
|
||||
background-color: #ffcd56;
|
||||
&.WARNING, &.WARN {
|
||||
background-color: var(--bg-amber-500);
|
||||
}
|
||||
|
||||
&.ERROR {
|
||||
background-color: #e5484d;
|
||||
background-color: var(--bg-cherry-500);
|
||||
}
|
||||
|
||||
&.TRACE {
|
||||
background-color: var(--bg-robin-300);
|
||||
}
|
||||
|
||||
&.DEBUG {
|
||||
background-color: var(--bg-forest-500);
|
||||
}
|
||||
|
||||
&.FATAL {
|
||||
background-color: var(--bg-sakura-500);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,45 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import LogStateIndicator from './LogStateIndicator';
|
||||
|
||||
describe('LogStateIndicator', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
const { container } = render(<LogStateIndicator type="INFO" />);
|
||||
const indicator = container.firstChild as HTMLElement;
|
||||
expect(indicator.classList.contains('log-state-indicator')).toBe(true);
|
||||
expect(indicator.classList.contains('isActive')).toBe(false);
|
||||
expect(container.querySelector('.line')).toBeTruthy();
|
||||
expect(container.querySelector('.line')?.classList.contains('INFO')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders correctly when isActive is true', () => {
|
||||
const { container } = render(<LogStateIndicator type="INFO" isActive />);
|
||||
const indicator = container.firstChild as HTMLElement;
|
||||
expect(indicator.classList.contains('isActive')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correctly with different types', () => {
|
||||
const { container: containerInfo } = render(
|
||||
<LogStateIndicator type="INFO" />,
|
||||
);
|
||||
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
const { container: containerWarning } = render(
|
||||
<LogStateIndicator type="WARNING" />,
|
||||
);
|
||||
expect(
|
||||
containerWarning.querySelector('.line')?.classList.contains('WARNING'),
|
||||
).toBe(true);
|
||||
|
||||
const { container: containerError } = render(
|
||||
<LogStateIndicator type="ERROR" />,
|
||||
);
|
||||
expect(
|
||||
containerError.querySelector('.line')?.classList.contains('ERROR'),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
@ -2,11 +2,40 @@ import './LogStateIndicator.styles.scss';
|
||||
|
||||
import cx from 'classnames';
|
||||
|
||||
export const SEVERITY_TEXT_TYPE = {
|
||||
TRACE: 'TRACE',
|
||||
TRACE2: 'TRACE2',
|
||||
TRACE3: 'TRACE3',
|
||||
TRACE4: 'TRACE4',
|
||||
DEBUG: 'DEBUG',
|
||||
DEBUG2: 'DEBUG2',
|
||||
DEBUG3: 'DEBUG3',
|
||||
DEBUG4: 'DEBUG4',
|
||||
INFO: 'INFO',
|
||||
INFO2: 'INFO2',
|
||||
INFO3: 'INFO3',
|
||||
INFO4: 'INFO4',
|
||||
WARN: 'WARN',
|
||||
WARN2: 'WARN2',
|
||||
WARN3: 'WARN3',
|
||||
WARN4: 'WARN4',
|
||||
WARNING: 'WARNING',
|
||||
ERROR: 'ERROR',
|
||||
ERROR2: 'ERROR2',
|
||||
ERROR3: 'ERROR3',
|
||||
ERROR4: 'ERROR4',
|
||||
FATAL: 'FATAL',
|
||||
FATAL2: 'FATAL2',
|
||||
FATAL3: 'FATAL3',
|
||||
FATAL4: 'FATAL4',
|
||||
} as const;
|
||||
|
||||
export const LogType = {
|
||||
INFO: 'INFO',
|
||||
WARNING: 'WARNING',
|
||||
ERROR: 'ERROR',
|
||||
};
|
||||
} as const;
|
||||
|
||||
function LogStateIndicator({
|
||||
type,
|
||||
isActive,
|
||||
|
89
frontend/src/components/Logs/LogStateIndicator/utils.test.ts
Normal file
89
frontend/src/components/Logs/LogStateIndicator/utils.test.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
|
||||
|
||||
describe('getLogIndicatorType', () => {
|
||||
it('should return severity type for valid log with severityText', () => {
|
||||
const log = {
|
||||
date: '2024-02-29T12:34:46Z',
|
||||
timestamp: 1646115296,
|
||||
id: '123456',
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severityText: 'INFO',
|
||||
severityNumber: 2,
|
||||
body: 'Sample log Message',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
attributes_string: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
severity_text: 'INFO',
|
||||
};
|
||||
expect(getLogIndicatorType(log)).toBe('INFO');
|
||||
});
|
||||
|
||||
it('should return log level if severityText is missing', () => {
|
||||
const log: ILog = {
|
||||
date: '2024-02-29T12:34:58Z',
|
||||
timestamp: 1646115296,
|
||||
id: '123456',
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severityNumber: 2,
|
||||
body: 'Sample log',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
attributes_string: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
severity_text: 'FATAL',
|
||||
severityText: '',
|
||||
};
|
||||
expect(getLogIndicatorType(log)).toBe('FATAL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLogIndicatorTypeForTable', () => {
|
||||
it('should return severity type for valid log with severityText', () => {
|
||||
const log = {
|
||||
date: '2024-02-29T12:34:56Z',
|
||||
timestamp: 1646115296,
|
||||
id: '123456',
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severity_number: 2,
|
||||
body: 'Sample log message',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
attributes_string: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
severity_text: 'WARN',
|
||||
};
|
||||
expect(getLogIndicatorTypeForTable(log)).toBe('WARN');
|
||||
});
|
||||
|
||||
it('should return log level if severityText is missing', () => {
|
||||
const log = {
|
||||
date: '2024-02-29T12:34:56Z',
|
||||
timestamp: 1646115296,
|
||||
id: '123456',
|
||||
traceId: '987654',
|
||||
spanId: '54321',
|
||||
traceFlags: 0,
|
||||
severityNumber: 2,
|
||||
body: 'Sample log message',
|
||||
resources_string: {},
|
||||
attributesString: {},
|
||||
attributes_string: {},
|
||||
attributesInt: {},
|
||||
attributesFloat: {},
|
||||
log_level: 'INFO',
|
||||
};
|
||||
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
|
||||
});
|
||||
});
|
57
frontend/src/components/Logs/LogStateIndicator/utils.ts
Normal file
57
frontend/src/components/Logs/LogStateIndicator/utils.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
|
||||
|
||||
const getSeverityType = (severityText: string): string => {
|
||||
switch (severityText) {
|
||||
case SEVERITY_TEXT_TYPE.TRACE:
|
||||
case SEVERITY_TEXT_TYPE.TRACE2:
|
||||
case SEVERITY_TEXT_TYPE.TRACE3:
|
||||
case SEVERITY_TEXT_TYPE.TRACE4:
|
||||
return SEVERITY_TEXT_TYPE.TRACE;
|
||||
case SEVERITY_TEXT_TYPE.DEBUG:
|
||||
case SEVERITY_TEXT_TYPE.DEBUG2:
|
||||
case SEVERITY_TEXT_TYPE.DEBUG3:
|
||||
case SEVERITY_TEXT_TYPE.DEBUG4:
|
||||
return SEVERITY_TEXT_TYPE.DEBUG;
|
||||
case SEVERITY_TEXT_TYPE.INFO:
|
||||
case SEVERITY_TEXT_TYPE.INFO2:
|
||||
case SEVERITY_TEXT_TYPE.INFO3:
|
||||
case SEVERITY_TEXT_TYPE.INFO4:
|
||||
return SEVERITY_TEXT_TYPE.INFO;
|
||||
case SEVERITY_TEXT_TYPE.WARN:
|
||||
case SEVERITY_TEXT_TYPE.WARN2:
|
||||
case SEVERITY_TEXT_TYPE.WARN3:
|
||||
case SEVERITY_TEXT_TYPE.WARN4:
|
||||
case SEVERITY_TEXT_TYPE.WARNING:
|
||||
return SEVERITY_TEXT_TYPE.WARN;
|
||||
case SEVERITY_TEXT_TYPE.ERROR:
|
||||
case SEVERITY_TEXT_TYPE.ERROR2:
|
||||
case SEVERITY_TEXT_TYPE.ERROR3:
|
||||
case SEVERITY_TEXT_TYPE.ERROR4:
|
||||
return SEVERITY_TEXT_TYPE.ERROR;
|
||||
case SEVERITY_TEXT_TYPE.FATAL:
|
||||
case SEVERITY_TEXT_TYPE.FATAL2:
|
||||
case SEVERITY_TEXT_TYPE.FATAL3:
|
||||
case SEVERITY_TEXT_TYPE.FATAL4:
|
||||
return SEVERITY_TEXT_TYPE.FATAL;
|
||||
default:
|
||||
return SEVERITY_TEXT_TYPE.INFO;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLogIndicatorType = (logData: ILog): string => {
|
||||
if (logData.severity_text) {
|
||||
return getSeverityType(logData.severity_text);
|
||||
}
|
||||
return logData.attributes_string?.log_level || LogType.INFO;
|
||||
};
|
||||
|
||||
export const getLogIndicatorTypeForTable = (
|
||||
log: Record<string, unknown>,
|
||||
): string => {
|
||||
if (log.severity_text) {
|
||||
return getSeverityType(log.severity_text as string);
|
||||
}
|
||||
return (log.log_level as string) || LogType.INFO;
|
||||
};
|
@ -23,9 +23,8 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||
import LogStateIndicator, {
|
||||
LogType,
|
||||
} from '../LogStateIndicator/LogStateIndicator';
|
||||
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorType } from '../LogStateIndicator/utils';
|
||||
// styles
|
||||
import { RawLogContent, RawLogViewContainer } from './styles';
|
||||
import { RawLogViewProps } from './types';
|
||||
@ -64,7 +63,7 @@ function RawLogView({
|
||||
|
||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
||||
|
||||
const logType = data?.attributes_string?.log_level || LogType.INFO;
|
||||
const logType = getLogIndicatorType(data);
|
||||
|
||||
const updatedSelecedFields = useMemo(
|
||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||
@ -164,7 +163,11 @@ function RawLogView({
|
||||
>
|
||||
<LogStateIndicator
|
||||
type={logType}
|
||||
isActive={activeLog?.id === data.id || activeContextLog?.id === data.id}
|
||||
isActive={
|
||||
activeLog?.id === data.id ||
|
||||
activeContextLog?.id === data.id ||
|
||||
isActiveLog
|
||||
}
|
||||
/>
|
||||
|
||||
<RawLogContent
|
||||
|
@ -30,6 +30,14 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
$isActiveLog
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||
|
||||
${({ $isHightlightedLog, $isDarkMode }): string =>
|
||||
$isHightlightedLog
|
||||
? `background-color: ${
|
||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
||||
};
|
||||
transition: background-color 2s ease-in;`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
export const ExpandIconWrapper = styled(Col)`
|
||||
|
@ -14,12 +14,12 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||
lineHeight: '18px',
|
||||
letterSpacing: '-0.07px',
|
||||
marginBottom: '0px',
|
||||
minWidth: '10rem',
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultTableStyle: CSSProperties = {
|
||||
minWidth: '40rem',
|
||||
maxWidth: '40rem',
|
||||
};
|
||||
|
||||
export const defaultListViewPanelStyle: CSSProperties = {
|
||||
|
@ -23,6 +23,7 @@ export type UseTableViewProps = {
|
||||
onOpenLogsContext?: (log: ILog) => void;
|
||||
onClickExpand?: (log: ILog) => void;
|
||||
activeLog?: ILog | null;
|
||||
activeLogIndex?: number;
|
||||
activeContextLog?: ILog | null;
|
||||
isListViewPanel?: boolean;
|
||||
} & LogsTableViewProps;
|
||||
|
@ -7,12 +7,10 @@ import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import LogStateIndicator, {
|
||||
LogType,
|
||||
} from '../LogStateIndicator/LogStateIndicator';
|
||||
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||
import { getLogIndicatorTypeForTable } from '../LogStateIndicator/utils';
|
||||
import {
|
||||
defaultListViewPanelStyle,
|
||||
defaultTableStyle,
|
||||
@ -84,7 +82,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
children: (
|
||||
<div className="table-timestamp">
|
||||
<LogStateIndicator
|
||||
type={defaultTo(item.log_level, LogType.INFO) as string}
|
||||
type={getLogIndicatorTypeForTable(item)}
|
||||
isActive={
|
||||
activeLog?.id === item.id || activeContextLog?.id === item.id
|
||||
}
|
||||
|
@ -72,8 +72,6 @@ export default function LogsFormatOptionsMenu({
|
||||
setAddNewColumn(!addNewColumn);
|
||||
};
|
||||
|
||||
// console.log('optionsMenuConfig', config);
|
||||
|
||||
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
|
||||
if (
|
||||
maxLinesPerRow &&
|
||||
@ -221,8 +219,6 @@ export default function LogsFormatOptionsMenu({
|
||||
className="column-name"
|
||||
key={value}
|
||||
onClick={(eve): void => {
|
||||
console.log('coluimn name', label, value);
|
||||
|
||||
eve.stopPropagation();
|
||||
|
||||
if (addColumn && addColumn?.onSelect) {
|
||||
|
@ -1,10 +1,12 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { CodeProps } from 'react-markdown/lib/ast-to-react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
||||
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
|
||||
|
||||
@ -74,6 +76,10 @@ const interpolateMarkdown = (
|
||||
return interpolatedContent;
|
||||
};
|
||||
|
||||
function CustomTag({ color }: { color: string }): JSX.Element {
|
||||
return <h1 style={{ color }}>This is custom element</h1>;
|
||||
}
|
||||
|
||||
function MarkdownRenderer({
|
||||
markdownContent,
|
||||
variables,
|
||||
@ -85,12 +91,14 @@ function MarkdownRenderer({
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
rehypePlugins={[rehypeRaw as any]}
|
||||
components={{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
a: Link,
|
||||
pre: Pre,
|
||||
code: Code,
|
||||
customtag: CustomTag,
|
||||
}}
|
||||
>
|
||||
{interpolatedMarkdown}
|
||||
|
@ -13,3 +13,6 @@ export const SIGNOZ_UPGRADE_PLAN_URL =
|
||||
'https://upgrade.signoz.io/upgrade-from-app';
|
||||
|
||||
export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
|
||||
|
||||
export const DEFAULT_ENTITY_VERSION = 'v3';
|
||||
export const ENTITY_VERSION_V4 = 'v4';
|
||||
|
@ -16,4 +16,5 @@ export enum LOCALSTORAGE {
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
||||
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
|
||||
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
||||
}
|
||||
|
@ -36,6 +36,11 @@ import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
logsAggregateOperatorOptions,
|
||||
metricAggregateOperatorOptions,
|
||||
metricsGaugeAggregateOperatorOptions,
|
||||
metricsGaugeSpaceAggregateOperatorOptions,
|
||||
metricsHistogramSpaceAggregateOperatorOptions,
|
||||
metricsSumAggregateOperatorOptions,
|
||||
metricsSumSpaceAggregateOperatorOptions,
|
||||
tracesAggregateOperatorOptions,
|
||||
} from './queryBuilderOperators';
|
||||
|
||||
@ -74,6 +79,18 @@ export const mapOfOperators = {
|
||||
traces: tracesAggregateOperatorOptions,
|
||||
};
|
||||
|
||||
export const metricsOperatorsByType = {
|
||||
Sum: metricsSumAggregateOperatorOptions,
|
||||
Gauge: metricsGaugeAggregateOperatorOptions,
|
||||
};
|
||||
|
||||
export const metricsSpaceAggregationOperatorsByType = {
|
||||
Sum: metricsSumSpaceAggregateOperatorOptions,
|
||||
Gauge: metricsGaugeSpaceAggregateOperatorOptions,
|
||||
Histogram: metricsHistogramSpaceAggregateOperatorOptions,
|
||||
ExponentialHistogram: metricsHistogramSpaceAggregateOperatorOptions,
|
||||
};
|
||||
|
||||
export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = {
|
||||
metrics: [
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
@ -148,6 +165,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||
aggregateOperator: MetricAggregateOperator.COUNT,
|
||||
aggregateAttribute: initialAutocompleteData,
|
||||
timeAggregation: MetricAggregateOperator.RATE,
|
||||
spaceAggregation: MetricAggregateOperator.SUM,
|
||||
functions: [],
|
||||
filters: { items: [], op: 'AND' },
|
||||
expression: createNewBuilderItemName({
|
||||
existNames: [],
|
||||
@ -160,7 +180,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: 'sum',
|
||||
reduceTo: 'avg',
|
||||
};
|
||||
|
||||
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
|
||||
@ -268,6 +288,14 @@ export enum PANEL_TYPES {
|
||||
EMPTY_WIDGET = 'EMPTY_WIDGET',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export enum ATTRIBUTE_TYPES {
|
||||
SUM = 'Sum',
|
||||
GAUGE = 'Gauge',
|
||||
HISTOGRAM = 'Histogram',
|
||||
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
|
||||
}
|
||||
|
||||
export type IQueryBuilderState = 'search';
|
||||
|
||||
export const QUERY_BUILDER_SEARCH_VALUES = {
|
||||
|
@ -302,3 +302,126 @@ export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
|
||||
label: 'Rate_max',
|
||||
},
|
||||
];
|
||||
|
||||
export const metricsSumAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.RATE,
|
||||
label: 'Rate',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.INCREASE,
|
||||
label: 'Increase',
|
||||
},
|
||||
];
|
||||
|
||||
export const metricsGaugeAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.LATEST,
|
||||
label: 'Latest',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.SUM,
|
||||
label: 'Sum',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.AVG,
|
||||
label: 'Avg',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MIN,
|
||||
label: 'Min',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MAX,
|
||||
label: 'Max',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.COUNT,
|
||||
label: 'Count',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.COUNT_DISTINCT,
|
||||
label: 'Count Distinct',
|
||||
},
|
||||
];
|
||||
|
||||
export const metricsSumSpaceAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.SUM,
|
||||
label: 'Sum',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.AVG,
|
||||
label: 'Avg',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MIN,
|
||||
label: 'Min',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MAX,
|
||||
label: 'Max',
|
||||
},
|
||||
];
|
||||
|
||||
export const metricsGaugeSpaceAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.SUM,
|
||||
label: 'Sum',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.AVG,
|
||||
label: 'Avg',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MIN,
|
||||
label: 'Min',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MAX,
|
||||
label: 'Max',
|
||||
},
|
||||
];
|
||||
|
||||
export const metricsHistogramSpaceAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.P50,
|
||||
label: 'P50',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P75,
|
||||
label: 'P75',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P90,
|
||||
label: 'P90',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P95,
|
||||
label: 'P95',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P99,
|
||||
label: 'P99',
|
||||
},
|
||||
];
|
||||
|
||||
export const metricsEmptyTimeAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [];
|
||||
|
137
frontend/src/constants/queryFunctionOptions.ts
Normal file
137
frontend/src/constants/queryFunctionOptions.ts
Normal file
@ -0,0 +1,137 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
|
||||
export const queryFunctionOptions: SelectOption<string, string>[] = [
|
||||
{
|
||||
value: QueryFunctionsTypes.CUTOFF_MIN,
|
||||
label: 'Cut Off Min',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.CUTOFF_MAX,
|
||||
label: 'Cut Off Max',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.CLAMP_MIN,
|
||||
label: 'Clamp Min',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.CLAMP_MAX,
|
||||
label: 'Clamp Max',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.ABSOLUTE,
|
||||
label: 'Absolute',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.LOG_2,
|
||||
label: 'Log2',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.LOG_10,
|
||||
label: 'Log10',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.CUMULATIVE_SUM,
|
||||
label: 'Cumulative Sum',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.EWMA_3,
|
||||
label: 'EWMA 3',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.EWMA_5,
|
||||
label: 'EWMA 5',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.EWMA_7,
|
||||
label: 'EWMA 7',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.MEDIAN_3,
|
||||
label: 'Median 3',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.MEDIAN_5,
|
||||
label: 'Median 5',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.MEDIAN_7,
|
||||
label: 'Median 7',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.TIME_SHIFT,
|
||||
label: 'Time Shift',
|
||||
},
|
||||
];
|
||||
|
||||
interface QueryFunctionConfigType {
|
||||
[key: string]: {
|
||||
showInput: boolean;
|
||||
inputType?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
|
||||
cutOffMin: {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
placeholder: 'Threshold',
|
||||
},
|
||||
cutOffMax: {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
placeholder: 'Threshold',
|
||||
},
|
||||
clampMin: {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
placeholder: 'Threshold',
|
||||
},
|
||||
clampMax: {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
placeholder: 'Threshold',
|
||||
},
|
||||
absolute: {
|
||||
showInput: false,
|
||||
},
|
||||
log2: {
|
||||
showInput: false,
|
||||
},
|
||||
log10: {
|
||||
showInput: false,
|
||||
},
|
||||
cumSum: {
|
||||
showInput: false,
|
||||
},
|
||||
ewma3: {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
placeholder: 'Alpha',
|
||||
},
|
||||
ewma5: {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
placeholder: 'Alpha',
|
||||
},
|
||||
ewma7: {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
placeholder: 'Alpha',
|
||||
},
|
||||
median3: {
|
||||
showInput: false,
|
||||
},
|
||||
median5: {
|
||||
showInput: false,
|
||||
},
|
||||
median7: {
|
||||
showInput: false,
|
||||
},
|
||||
timeShift: {
|
||||
showInput: true,
|
||||
inputType: 'text',
|
||||
},
|
||||
};
|
@ -7,6 +7,11 @@ const ROUTES = {
|
||||
TRACE_DETAIL: '/trace/:id',
|
||||
TRACES_EXPLORER: '/traces-explorer',
|
||||
GET_STARTED: '/get-started',
|
||||
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
|
||||
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
|
||||
GET_STARTED_INFRASTRUCTURE_MONITORING:
|
||||
'/get-started/infrastructure-monitoring',
|
||||
GET_STARTED_AWS_MONITORING: '/get-started/aws-monitoring',
|
||||
USAGE_EXPLORER: '/usage-explorer',
|
||||
APPLICATION: '/services',
|
||||
ALL_DASHBOARD: '/dashboard',
|
||||
@ -42,10 +47,13 @@ const ROUTES = {
|
||||
TRACE_EXPLORER: '/trace-explorer',
|
||||
BILLING: '/billing',
|
||||
SUPPORT: '/support',
|
||||
LOGS_SAVE_VIEWS: '/logs-save-views',
|
||||
TRACES_SAVE_VIEWS: '/traces-save-views',
|
||||
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
||||
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
SHORTCUTS: '/shortcuts',
|
||||
INTEGRATIONS_BASE: '/integrations',
|
||||
INTEGRATIONS_INSTALLED: '/integrations/installed',
|
||||
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
17
frontend/src/constants/shortcuts/DashboardShortcuts.ts
Normal file
17
frontend/src/constants/shortcuts/DashboardShortcuts.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
const userOS = getUserOperatingSystem();
|
||||
|
||||
export const DashboardShortcuts = {
|
||||
SaveChanges: 's+meta',
|
||||
DiscardChanges: 'd+meta',
|
||||
};
|
||||
|
||||
export const DashboardShortcutsName = {
|
||||
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
|
||||
};
|
||||
|
||||
export const DashboardShortcutsDescription = {
|
||||
SaveChanges: 'Save Changes',
|
||||
DiscardChanges: 'Discard Changes',
|
||||
};
|
17
frontend/src/constants/shortcuts/QBShortcuts.ts
Normal file
17
frontend/src/constants/shortcuts/QBShortcuts.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
const userOS = getUserOperatingSystem();
|
||||
|
||||
export const QBShortcuts = {
|
||||
StageAndRunQuery: 'enter+meta',
|
||||
};
|
||||
|
||||
export const QBShortcutsName = {
|
||||
StageAndRunQuery: `${
|
||||
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
|
||||
}+enter`,
|
||||
};
|
||||
|
||||
export const QBShortcutsDescription = {
|
||||
StageAndRunQuery: 'Stage and Run the query',
|
||||
};
|
@ -231,7 +231,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||
const pageTitle = t(routeKey);
|
||||
const renderFullScreen =
|
||||
pathname === ROUTES.GET_STARTED || pathname === ROUTES.WORKSPACE_LOCKED;
|
||||
pathname === ROUTES.GET_STARTED ||
|
||||
pathname === ROUTES.WORKSPACE_LOCKED ||
|
||||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
||||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
||||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
||||
pathname === ROUTES.GET_STARTED_AWS_MONITORING;
|
||||
|
||||
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
||||
|
||||
|
@ -1,13 +1,29 @@
|
||||
.billing-container {
|
||||
padding: 16px 0;
|
||||
width: 100%;
|
||||
padding-top: 36px;
|
||||
width: 65%;
|
||||
|
||||
.billing-summary {
|
||||
margin: 24px 8px;
|
||||
}
|
||||
|
||||
.billing-details {
|
||||
margin: 36px 8px;
|
||||
margin: 24px 0px;
|
||||
|
||||
.ant-table-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
background-color: rgb(27, 28, 32);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background-color: var(--bg-ink-400);
|
||||
border-color: var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
td {
|
||||
border-color: var(--bg-slate-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upgrade-plan-benefits {
|
||||
@ -24,6 +40,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-graph-card {
|
||||
.ant-card-body {
|
||||
height: 40vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
|
||||
@ -34,3 +59,20 @@
|
||||
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.billing-container {
|
||||
.billing-details {
|
||||
.ant-table-cell {
|
||||
background: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-200);
|
||||
}
|
||||
|
||||
.ant-table-tbody {
|
||||
td {
|
||||
border-color: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,13 +12,36 @@ import BillingContainer from './BillingContainer';
|
||||
|
||||
const lisenceUrl = 'http://localhost/api/v2/licenses';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
window.ResizeObserver =
|
||||
window.ResizeObserver ||
|
||||
jest.fn().mockImplementation(() => ({
|
||||
disconnect: jest.fn(),
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('BillingContainer', () => {
|
||||
test('Component should render', async () => {
|
||||
act(() => {
|
||||
render(<BillingContainer />);
|
||||
});
|
||||
const unit = screen.getAllByText(/unit/i);
|
||||
expect(unit[1]).toBeInTheDocument();
|
||||
|
||||
const dataInjection = screen.getByRole('columnheader', {
|
||||
name: /data ingested/i,
|
||||
});
|
||||
@ -32,24 +55,15 @@ describe('BillingContainer', () => {
|
||||
});
|
||||
expect(cost).toBeInTheDocument();
|
||||
|
||||
const total = screen.getByRole('cell', {
|
||||
name: /total/i,
|
||||
});
|
||||
expect(total).toBeInTheDocument();
|
||||
|
||||
const manageBilling = screen.getByRole('button', {
|
||||
name: /manage billing/i,
|
||||
});
|
||||
expect(manageBilling).toBeInTheDocument();
|
||||
|
||||
const dollar = screen.getByRole('cell', {
|
||||
name: /\$0/i,
|
||||
});
|
||||
const dollar = screen.getByText(/\$0/i);
|
||||
expect(dollar).toBeInTheDocument();
|
||||
|
||||
const currentBill = screen.getByRole('heading', {
|
||||
name: /current bill total/i,
|
||||
});
|
||||
const currentBill = screen.getByText('Billing');
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -61,9 +75,7 @@ describe('BillingContainer', () => {
|
||||
const freeTrailText = await screen.findByText('Free Trial');
|
||||
expect(freeTrailText).toBeInTheDocument();
|
||||
|
||||
const currentBill = await screen.findByRole('heading', {
|
||||
name: /current bill total/i,
|
||||
});
|
||||
const currentBill = screen.getByText('Billing');
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
|
||||
const dollar0 = await screen.findByText(/\$0/i);
|
||||
@ -102,9 +114,7 @@ describe('BillingContainer', () => {
|
||||
render(<BillingContainer />);
|
||||
});
|
||||
|
||||
const currentBill = await screen.findByRole('heading', {
|
||||
name: /current bill total/i,
|
||||
});
|
||||
const currentBill = screen.getByText('Billing');
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
|
||||
const dollar0 = await screen.findByText(/\$0/i);
|
||||
@ -137,45 +147,30 @@ describe('BillingContainer', () => {
|
||||
res(ctx.status(200), ctx.json(notOfTrailResponse)),
|
||||
),
|
||||
);
|
||||
render(<BillingContainer />);
|
||||
const { findByText } = render(<BillingContainer />);
|
||||
|
||||
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
|
||||
billingSuccessResponse.data.billingPeriodStart,
|
||||
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
|
||||
|
||||
const billingPeriod = await screen.findByRole('heading', {
|
||||
name: new RegExp(billingPeriodText, 'i'),
|
||||
});
|
||||
const billingPeriod = await findByText(billingPeriodText);
|
||||
expect(billingPeriod).toBeInTheDocument();
|
||||
|
||||
const currentBill = await screen.findByRole('heading', {
|
||||
name: /current bill total/i,
|
||||
});
|
||||
const currentBill = screen.getByText('Billing');
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
|
||||
const dollar0 = await screen.findAllByText(/\$1278.3/i);
|
||||
expect(dollar0[0]).toBeInTheDocument();
|
||||
expect(dollar0.length).toBe(2);
|
||||
const dollar0 = await screen.findByText(/\$1,278.3/i);
|
||||
expect(dollar0).toBeInTheDocument();
|
||||
|
||||
const metricsRow = await screen.findByRole('row', {
|
||||
name: /metrics Million 4012 0.1 \$ 401.2/i,
|
||||
name: /metrics 4012 Million 0.1 \$ 401.2/i,
|
||||
});
|
||||
expect(metricsRow).toBeInTheDocument();
|
||||
|
||||
const logRow = await screen.findByRole('row', {
|
||||
name: /Logs GB 497 0.4 \$ 198.8/i,
|
||||
name: /Logs 497 GB 0.4 \$ 198.8/i,
|
||||
});
|
||||
expect(logRow).toBeInTheDocument();
|
||||
|
||||
const totalBill = await screen.findByRole('cell', {
|
||||
name: /\$1278/i,
|
||||
});
|
||||
expect(totalBill).toBeInTheDocument();
|
||||
|
||||
const totalBillRow = await screen.findByRole('row', {
|
||||
name: /total \$1278/i,
|
||||
});
|
||||
expect(totalBillRow).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should render corrent day remaining in billing period', async () => {
|
||||
|
@ -2,11 +2,24 @@
|
||||
import './BillingContainer.styles.scss';
|
||||
|
||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Col, Row, Skeleton, Table, Tag, Typography } from 'antd';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Flex,
|
||||
Row,
|
||||
Skeleton,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import updateCreditCardApi from 'api/billing/checkout';
|
||||
import getUsage from 'api/billing/getUsage';
|
||||
import manageCreditCardApi from 'api/billing/manage';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
@ -22,8 +35,11 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { License } from 'types/api/licenses/def';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
|
||||
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
||||
|
||||
interface DataType {
|
||||
key: string;
|
||||
name: string;
|
||||
@ -104,12 +120,11 @@ export default function BillingContainer(): JSX.Element {
|
||||
const daysRemainingStr = 'days remaining in your billing period.';
|
||||
const [headerText, setHeaderText] = useState('');
|
||||
const [billAmount, setBillAmount] = useState(0);
|
||||
const [totalBillAmount, setTotalBillAmount] = useState(0);
|
||||
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
||||
const [daysRemaining, setDaysRemaining] = useState(0);
|
||||
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const billCurrency = '$';
|
||||
const [apiResponse, setApiResponse] = useState<any>({});
|
||||
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
@ -120,10 +135,12 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
|
||||
const processUsageData = useCallback(
|
||||
(data: any): void => {
|
||||
const {
|
||||
details: { breakdown = [], total, billTotal },
|
||||
details: { breakdown = [], billTotal },
|
||||
billingPeriodStart,
|
||||
billingPeriodEnd,
|
||||
} = data?.payload || {};
|
||||
@ -141,8 +158,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
formattedUsageData.push({
|
||||
key: `${index}${i}`,
|
||||
name: i === 0 ? element?.type : '',
|
||||
unit: element?.unit,
|
||||
dataIngested: tier.quantity,
|
||||
dataIngested: `${tier.quantity} ${element?.unit}`,
|
||||
pricePerUnit: tier.unitPrice,
|
||||
cost: `$ ${tier.tierCost}`,
|
||||
});
|
||||
@ -152,7 +168,6 @@ export default function BillingContainer(): JSX.Element {
|
||||
}
|
||||
|
||||
setData(formattedUsageData);
|
||||
setTotalBillAmount(total);
|
||||
|
||||
if (!licensesData?.payload?.onTrial) {
|
||||
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
|
||||
@ -165,11 +180,13 @@ export default function BillingContainer(): JSX.Element {
|
||||
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
|
||||
setBillAmount(billTotal);
|
||||
}
|
||||
|
||||
setApiResponse(data?.payload || {});
|
||||
},
|
||||
[licensesData?.payload?.onTrial],
|
||||
);
|
||||
|
||||
const { isLoading } = useQuery(
|
||||
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
|
||||
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
|
||||
{
|
||||
queryFn: () => getUsage(activeLicense?.key || ''),
|
||||
@ -208,11 +225,6 @@ export default function BillingContainer(): JSX.Element {
|
||||
key: 'name',
|
||||
render: (text): JSX.Element => <div>{text}</div>,
|
||||
},
|
||||
{
|
||||
title: 'Unit',
|
||||
dataIndex: 'unit',
|
||||
key: 'unit',
|
||||
},
|
||||
{
|
||||
title: 'Data Ingested',
|
||||
dataIndex: 'dataIngested',
|
||||
@ -230,24 +242,6 @@ export default function BillingContainer(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const renderSummary = (): JSX.Element => (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0}>
|
||||
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
|
||||
Total
|
||||
</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={1}> </Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={2}> </Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={3}> </Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={4}>
|
||||
<Typography.Title level={3} style={{ fontWeight: 500, margin: ' 0px' }}>
|
||||
${totalBillAmount}
|
||||
</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
);
|
||||
|
||||
const renderTableSkeleton = (): JSX.Element => (
|
||||
<Table
|
||||
dataSource={dummyData}
|
||||
@ -336,78 +330,95 @@ export default function BillingContainer(): JSX.Element {
|
||||
updateCreditCard,
|
||||
]);
|
||||
|
||||
const BillingUsageGraphCallback = useCallback(
|
||||
() =>
|
||||
!isLoading && !isFetchingBillingData ? (
|
||||
<BillingUsageGraph data={apiResponse} billAmount={billAmount} />
|
||||
) : (
|
||||
<Card className="empty-graph-card" bordered={false}>
|
||||
<Spinner size="large" tip="Loading..." height="35vh" />
|
||||
</Card>
|
||||
),
|
||||
[apiResponse, billAmount, isLoading, isFetchingBillingData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="billing-container">
|
||||
<Row
|
||||
justify="space-between"
|
||||
align="middle"
|
||||
gutter={[16, 16]}
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
<Flex vertical style={{ marginBottom: 16 }}>
|
||||
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
|
||||
Billing
|
||||
</Typography.Text>
|
||||
<Typography.Text color={Color.BG_VANILLA_400}>
|
||||
Manage your billing information, invoices, and monitor costs.
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{ minHeight: 150, marginBottom: 16 }}
|
||||
className="page-info"
|
||||
>
|
||||
<Col span={20}>
|
||||
<Typography.Title level={4} ellipsis style={{ fontWeight: '300' }}>
|
||||
{headerText}
|
||||
</Typography.Title>
|
||||
|
||||
{licensesData?.payload?.onTrial &&
|
||||
licensesData?.payload?.trialConvertedToSubscription && (
|
||||
<Typography.Title
|
||||
level={5}
|
||||
ellipsis
|
||||
style={{ fontWeight: '300', color: '#49aa19' }}
|
||||
>
|
||||
We have received your card details, your billing will only start after
|
||||
the end of your free trial period.
|
||||
</Typography.Title>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Flex vertical>
|
||||
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
|
||||
{isCloudUserVal ? 'Enterprise Cloud' : 'Enterprise'}{' '}
|
||||
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
|
||||
</Typography.Title>
|
||||
{!isLoading && !isFetchingBillingData ? (
|
||||
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
|
||||
{daysRemaining} {daysRemainingStr}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Flex>
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
loading={isLoadingBilling || isLoadingManageBilling}
|
||||
disabled={isLoading}
|
||||
onClick={handleBilling}
|
||||
>
|
||||
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
|
||||
? 'Upgrade Plan'
|
||||
: 'Manage Billing'}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Flex>
|
||||
|
||||
<div className="billing-summary">
|
||||
<Typography.Title level={4} style={{ margin: '16px 0' }}>
|
||||
Current bill total
|
||||
</Typography.Title>
|
||||
{licensesData?.payload?.onTrial &&
|
||||
licensesData?.payload?.trialConvertedToSubscription && (
|
||||
<Typography.Text
|
||||
ellipsis
|
||||
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
|
||||
>
|
||||
We have received your card details, your billing will only start after
|
||||
the end of your free trial period.
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<Typography.Title
|
||||
level={3}
|
||||
style={{ margin: '16px 0', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{billCurrency}
|
||||
{billAmount}
|
||||
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
|
||||
</Typography.Title>
|
||||
{!isLoading && !isFetchingBillingData ? (
|
||||
<Alert
|
||||
message={headerText}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 12 }}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Typography.Paragraph style={{ margin: '16px 0' }}>
|
||||
{daysRemaining} {daysRemainingStr}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<BillingUsageGraphCallback />
|
||||
|
||||
<div className="billing-details">
|
||||
{!isLoading && (
|
||||
{!isLoading && !isFetchingBillingData && (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
pagination={false}
|
||||
summary={renderSummary}
|
||||
bordered={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoading && renderTableSkeleton()}
|
||||
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
||||
</div>
|
||||
|
||||
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
|
||||
|
@ -0,0 +1,29 @@
|
||||
.billing-graph-card {
|
||||
.ant-card-body {
|
||||
height: 40vh;
|
||||
.uplot-graph-container {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
.total-spent {
|
||||
font-family: 'SF Mono' monospace;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.total-spent-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.48px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.total-spent-title {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
import './BillingUsageGraph.styles.scss';
|
||||
import '../../../lib/uPlotLib/uPlotLib.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Card, Flex, Typography } from 'antd';
|
||||
import { getComponentForPanelType } from 'constants/panelTypes';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PropsTypePropsMap } from 'container/GridPanelSwitch/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import tooltipPlugin from 'lib/uPlotLib/plugins/tooltipPlugin';
|
||||
import getAxes from 'lib/uPlotLib/utils/getAxes';
|
||||
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { getXAxisScale } from 'lib/uPlotLib/utils/getXAxisScale';
|
||||
import { getYAxisScale } from 'lib/uPlotLib/utils/getYAxisScale';
|
||||
import { FC, useMemo, useRef } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
convertDataToMetricRangePayload,
|
||||
fillMissingValuesForQuantities,
|
||||
} from './utils';
|
||||
|
||||
interface BillingUsageGraphProps {
|
||||
data: any;
|
||||
billAmount: number;
|
||||
}
|
||||
const paths = (
|
||||
u: any,
|
||||
seriesIdx: number,
|
||||
idx0: number,
|
||||
idx1: number,
|
||||
extendGap: boolean,
|
||||
buildClip: boolean,
|
||||
): uPlot.Series.PathBuilder => {
|
||||
const s = u.series[seriesIdx];
|
||||
const style = s.drawStyle;
|
||||
const interp = s.lineInterpolation;
|
||||
|
||||
const renderer = getRenderer(style, interp);
|
||||
|
||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||
};
|
||||
|
||||
export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
|
||||
const { data, billAmount } = props;
|
||||
const graphCompatibleData = useMemo(
|
||||
() => convertDataToMetricRangePayload(data),
|
||||
[data],
|
||||
);
|
||||
const chartData = getUPlotChartData(graphCompatibleData);
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const { billingPeriodStart: startTime, billingPeriodEnd: endTime } = data;
|
||||
|
||||
const Component = getComponentForPanelType(PANEL_TYPES.BAR) as FC<
|
||||
PropsTypePropsMap[PANEL_TYPES]
|
||||
>;
|
||||
|
||||
const getGraphSeries = (color: string, label: string): any => ({
|
||||
drawStyle: 'bars',
|
||||
paths,
|
||||
lineInterpolation: 'spline',
|
||||
show: true,
|
||||
label,
|
||||
fill: color,
|
||||
stroke: color,
|
||||
width: 2,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
size: 5,
|
||||
show: false,
|
||||
stroke: color,
|
||||
},
|
||||
});
|
||||
|
||||
const uPlotSeries: any = useMemo(
|
||||
() => [
|
||||
{ label: 'Timestamp', stroke: 'purple' },
|
||||
getGraphSeries(
|
||||
'#7CEDBE',
|
||||
graphCompatibleData.data.result[0]?.legend as string,
|
||||
),
|
||||
getGraphSeries(
|
||||
'#4E74F8',
|
||||
graphCompatibleData.data.result[1]?.legend as string,
|
||||
),
|
||||
getGraphSeries(
|
||||
'#F24769',
|
||||
graphCompatibleData.data.result[2]?.legend as string,
|
||||
),
|
||||
],
|
||||
[graphCompatibleData.data.result],
|
||||
);
|
||||
|
||||
const axesOptions = getAxes(isDarkMode, '');
|
||||
|
||||
const optionsForChart: uPlot.Options = useMemo(
|
||||
() => ({
|
||||
id: 'billing-usage-breakdown',
|
||||
series: uPlotSeries,
|
||||
width: containerDimensions.width,
|
||||
height: containerDimensions.height - 30,
|
||||
axes: [
|
||||
{
|
||||
...axesOptions[0],
|
||||
grid: {
|
||||
...axesOptions.grid,
|
||||
show: false,
|
||||
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
|
||||
},
|
||||
},
|
||||
{
|
||||
...axesOptions[1],
|
||||
stroke: isDarkMode ? Color.BG_SLATE_200 : Color.BG_INK_400,
|
||||
},
|
||||
],
|
||||
scales: {
|
||||
x: {
|
||||
...getXAxisScale(startTime - 86400, endTime), // Minus 86400 from startTime to decrease a day to have a buffer start
|
||||
},
|
||||
y: {
|
||||
...getYAxisScale({
|
||||
series: graphCompatibleData?.data.newResult.data.result,
|
||||
yAxisUnit: '',
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
live: false,
|
||||
isolate: true,
|
||||
},
|
||||
cursor: {
|
||||
lock: false,
|
||||
focus: {
|
||||
prox: 1e6,
|
||||
bias: 1,
|
||||
},
|
||||
},
|
||||
focus: {
|
||||
alpha: 0.3,
|
||||
},
|
||||
padding: [32, 32, 16, 16],
|
||||
plugins: [
|
||||
tooltipPlugin(
|
||||
fillMissingValuesForQuantities(graphCompatibleData, chartData[0]),
|
||||
'',
|
||||
true,
|
||||
),
|
||||
],
|
||||
}),
|
||||
[
|
||||
axesOptions,
|
||||
chartData,
|
||||
containerDimensions.height,
|
||||
containerDimensions.width,
|
||||
endTime,
|
||||
graphCompatibleData,
|
||||
isDarkMode,
|
||||
startTime,
|
||||
uPlotSeries,
|
||||
],
|
||||
);
|
||||
|
||||
const numberFormatter = new Intl.NumberFormat('en-US');
|
||||
|
||||
return (
|
||||
<Card bordered={false} className="billing-graph-card">
|
||||
<Flex justify="space-between">
|
||||
<Flex vertical gap={6}>
|
||||
<Typography.Text className="total-spent-title">
|
||||
TOTAL SPENT
|
||||
</Typography.Text>
|
||||
<Typography.Text color={Color.BG_VANILLA_100} className="total-spent">
|
||||
${numberFormatter.format(billAmount)}
|
||||
</Typography.Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<div ref={graphRef} style={{ height: '100%', paddingBottom: 48 }}>
|
||||
<Component data={chartData} options={optionsForChart} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
export const convertDataToMetricRangePayload = (
|
||||
data: any,
|
||||
): MetricRangePayloadProps => {
|
||||
const emptyStateData = {
|
||||
data: {
|
||||
newResult: { data: { result: [], resultType: '' } },
|
||||
result: [],
|
||||
resultType: '',
|
||||
},
|
||||
};
|
||||
if (isEmpty(data)) {
|
||||
return emptyStateData;
|
||||
}
|
||||
const {
|
||||
details: { breakdown = [] },
|
||||
} = data || {};
|
||||
|
||||
if (isNull(breakdown) || breakdown.length === 0) {
|
||||
return emptyStateData;
|
||||
}
|
||||
|
||||
const payload = breakdown.map((info: any) => {
|
||||
const metric = info.type;
|
||||
const sortedBreakdownData = (info?.dayWiseBreakdown?.breakdown || []).sort(
|
||||
(a: any, b: any) => a.timestamp - b.timestamp,
|
||||
);
|
||||
const values = (sortedBreakdownData || []).map((categoryInfo: any) => [
|
||||
categoryInfo.timestamp,
|
||||
categoryInfo.total,
|
||||
]);
|
||||
const queryName = info.type;
|
||||
const legend = info.type;
|
||||
const { unit } = info;
|
||||
const quantity = sortedBreakdownData.map(
|
||||
(categoryInfo: any) => categoryInfo.quantity,
|
||||
);
|
||||
return { metric, values, queryName, legend, quantity, unit };
|
||||
});
|
||||
|
||||
const sortedData = payload.sort((a: any, b: any) => {
|
||||
const sumA = a.values.reduce((acc: any, val: any) => acc + val[1], 0);
|
||||
const avgA = a.values.length ? sumA / a.values.length : 0;
|
||||
const sumB = b.values.reduce((acc: any, val: any) => acc + val[1], 0);
|
||||
const avgB = b.values.length ? sumB / b.values.length : 0;
|
||||
|
||||
return sumA === sumB ? avgB - avgA : sumB - sumA;
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
newResult: { data: { result: sortedData, resultType: '' } },
|
||||
result: sortedData,
|
||||
resultType: '',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function fillMissingValuesForQuantities(
|
||||
data: any,
|
||||
timestampArray: number[],
|
||||
): MetricRangePayloadProps {
|
||||
const { result } = data.data;
|
||||
|
||||
const transformedResultArr: any[] = [];
|
||||
result.forEach((item: any) => {
|
||||
const timestampToQuantityMap: { [timestamp: number]: number } = {};
|
||||
item.values.forEach((val: number[], index: number) => {
|
||||
timestampToQuantityMap[val[0]] = item.quantity[index];
|
||||
});
|
||||
|
||||
const quantityArray = timestampArray.map(
|
||||
(timestamp: number) => timestampToQuantityMap[timestamp] ?? null,
|
||||
);
|
||||
transformedResultArr.push({ ...item, quantity: quantityArray });
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
newResult: { data: { result: transformedResultArr, resultType: '' } },
|
||||
result: transformedResultArr,
|
||||
resultType: '',
|
||||
},
|
||||
};
|
||||
}
|
@ -64,6 +64,16 @@ export interface OpsgenieChannel extends Channel {
|
||||
priority?: string;
|
||||
}
|
||||
|
||||
export interface EmailChannel extends Channel {
|
||||
// comma separated list of email addresses to send alerts to
|
||||
to: string;
|
||||
// HTML body of the email notification.
|
||||
html: string;
|
||||
// Further headers email header key/value pairs.
|
||||
// [ headers: { <string>: <tmpl_string>, ... } ]
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export const ValidatePagerChannel = (p: PagerChannel): string => {
|
||||
if (!p) {
|
||||
return 'Received unexpected input for this channel, please contact your administrator ';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OpsgenieChannel, PagerChannel } from './config';
|
||||
import { EmailChannel, OpsgenieChannel, PagerChannel } from './config';
|
||||
|
||||
export const PagerInitialConfig: Partial<PagerChannel> = {
|
||||
description: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
|
||||
@ -50,3 +50,399 @@ export const OpsgenieInitialConfig: Partial<OpsgenieChannel> = {
|
||||
priority:
|
||||
'{{ if eq (index .Alerts 0).Labels.severity "critical" }}P1{{ else if eq (index .Alerts 0).Labels.severity "warning" }}P2{{ else if eq (index .Alerts 0).Labels.severity "info" }}P3{{ else }}P4{{ end }}',
|
||||
};
|
||||
|
||||
export const EmailInitialConfig: Partial<EmailChannel> = {
|
||||
send_resolved: true,
|
||||
html: `<!--
|
||||
Credits: https://github.com/mailgun/transactional-email-templates
|
||||
-->
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>{{ template "__subject" . }}</title>
|
||||
<style>
|
||||
|
||||
/* -------------------------------------
|
||||
GLOBAL
|
||||
A very basic CSS reset
|
||||
------------------------------------- */
|
||||
* {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
/* Let's make sure all tables have defaults */
|
||||
table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.body-wrap {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.footer p,
|
||||
.footer a,
|
||||
.footer td {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
color: #000;
|
||||
margin: 40px 0 0;
|
||||
line-height: 1.2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 38px;*/
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 29px;*/
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
margin-left: 5px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
LINKS & BUTTONS
|
||||
------------------------------------- */
|
||||
a {
|
||||
color: #348eda;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
text-decoration: none;
|
||||
color: #FFF;
|
||||
background-color: #348eda;
|
||||
border: solid #348eda;
|
||||
border-width: 10px 20px;
|
||||
line-height: 2em;
|
||||
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 28px;*/
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.aligncenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alignright {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alignleft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
ALERTS
|
||||
Change the class depending on warning email, good email or bad email
|
||||
------------------------------------- */
|
||||
.alert {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.alert a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.alert.alert-warning {
|
||||
background-color: #E6522C;
|
||||
}
|
||||
|
||||
.alert.alert-bad {
|
||||
background-color: #D0021B;
|
||||
}
|
||||
|
||||
.alert.alert-good {
|
||||
background-color: #68B90F;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
INVOICE
|
||||
Styles for the billing table
|
||||
------------------------------------- */
|
||||
.invoice {
|
||||
margin: 40px auto;
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.invoice td {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.invoice .invoice-items {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invoice .invoice-items td {
|
||||
border-top: #eee 1px solid;
|
||||
}
|
||||
|
||||
.invoice .invoice-items .total td {
|
||||
border-top: 2px solid #333;
|
||||
border-bottom: 2px solid #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body itemscope itemtype="http://schema.org/EmailMessage">
|
||||
<table class="body-wrap">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container" width="600">
|
||||
<div class="content">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<td class="alert alert-warning">
|
||||
{{ else }}
|
||||
<td class="alert alert-good">
|
||||
{{ end }}
|
||||
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
|
||||
{{ .Name }}={{ .Value }}
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-wrap">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<strong>[{{ .Alerts.Firing | len }}] Firing</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Alerts.Firing }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<strong>Labels</strong><br />
|
||||
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
|
||||
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
|
||||
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
|
||||
<a href="{{ .GeneratorURL }}">Source</a><br />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<strong>[{{ .Alerts.Resolved | len }}] Resolved</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Alerts.Resolved }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<strong>Labels</strong><br />
|
||||
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
|
||||
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
|
||||
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
|
||||
<a href="{{ .GeneratorURL }}">Source</a><br />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`,
|
||||
};
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Form } from 'antd';
|
||||
import createEmail from 'api/channels/createEmail';
|
||||
import createMsTeamsApi from 'api/channels/createMsTeams';
|
||||
import createOpsgenie from 'api/channels/createOpsgenie';
|
||||
import createPagerApi from 'api/channels/createPager';
|
||||
import createSlackApi from 'api/channels/createSlack';
|
||||
import createWebhookApi from 'api/channels/createWebhook';
|
||||
import testEmail from 'api/channels/testEmail';
|
||||
import testMsTeamsApi from 'api/channels/testMsTeams';
|
||||
import testOpsGenie from 'api/channels/testOpsgenie';
|
||||
import testPagerApi from 'api/channels/testPager';
|
||||
@ -18,6 +20,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
ChannelType,
|
||||
EmailChannel,
|
||||
MsTeamsChannel,
|
||||
OpsgenieChannel,
|
||||
PagerChannel,
|
||||
@ -25,7 +28,11 @@ import {
|
||||
ValidatePagerChannel,
|
||||
WebhookChannel,
|
||||
} from './config';
|
||||
import { OpsgenieInitialConfig, PagerInitialConfig } from './defaults';
|
||||
import {
|
||||
EmailInitialConfig,
|
||||
OpsgenieInitialConfig,
|
||||
PagerInitialConfig,
|
||||
} from './defaults';
|
||||
import { isChannelType } from './utils';
|
||||
|
||||
function CreateAlertChannels({
|
||||
@ -42,7 +49,8 @@ function CreateAlertChannels({
|
||||
WebhookChannel &
|
||||
PagerChannel &
|
||||
MsTeamsChannel &
|
||||
OpsgenieChannel
|
||||
OpsgenieChannel &
|
||||
EmailChannel
|
||||
>
|
||||
>({
|
||||
text: `{{ range .Alerts -}}
|
||||
@ -94,6 +102,14 @@ function CreateAlertChannels({
|
||||
...OpsgenieInitialConfig,
|
||||
}));
|
||||
}
|
||||
|
||||
// reset config to email defaults
|
||||
if (value === ChannelType.Email && currentType !== value) {
|
||||
setSelectedConfig((selectedConfig) => ({
|
||||
...selectedConfig,
|
||||
...EmailInitialConfig,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[type, selectedConfig],
|
||||
);
|
||||
@ -293,6 +309,43 @@ function CreateAlertChannels({
|
||||
setSavingState(false);
|
||||
}, [prepareOpsgenieRequest, t, notifications]);
|
||||
|
||||
const prepareEmailRequest = useCallback(
|
||||
() => ({
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
to: selectedConfig?.to || '',
|
||||
html: selectedConfig?.html || '',
|
||||
headers: selectedConfig?.headers || {},
|
||||
}),
|
||||
[selectedConfig],
|
||||
);
|
||||
|
||||
const onEmailHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
try {
|
||||
const request = prepareEmailRequest();
|
||||
const response = await createEmail(request);
|
||||
if (response.statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('channel_creation_done'),
|
||||
});
|
||||
history.replace(ROUTES.ALL_CHANNELS);
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('channel_creation_failed'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('channel_creation_failed'),
|
||||
});
|
||||
}
|
||||
setSavingState(false);
|
||||
}, [prepareEmailRequest, t, notifications]);
|
||||
|
||||
const prepareMsTeamsRequest = useCallback(
|
||||
() => ({
|
||||
webhook_url: selectedConfig?.webhook_url || '',
|
||||
@ -339,6 +392,7 @@ function CreateAlertChannels({
|
||||
[ChannelType.Pagerduty]: onPagerHandler,
|
||||
[ChannelType.Opsgenie]: onOpsgenieHandler,
|
||||
[ChannelType.MsTeams]: onMsTeamsHandler,
|
||||
[ChannelType.Email]: onEmailHandler,
|
||||
};
|
||||
|
||||
if (isChannelType(value)) {
|
||||
@ -360,6 +414,7 @@ function CreateAlertChannels({
|
||||
onPagerHandler,
|
||||
onOpsgenieHandler,
|
||||
onMsTeamsHandler,
|
||||
onEmailHandler,
|
||||
notifications,
|
||||
t,
|
||||
],
|
||||
@ -392,6 +447,10 @@ function CreateAlertChannels({
|
||||
request = prepareOpsgenieRequest();
|
||||
response = await testOpsGenie(request);
|
||||
break;
|
||||
case ChannelType.Email:
|
||||
request = prepareEmailRequest();
|
||||
response = await testEmail(request);
|
||||
break;
|
||||
default:
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
@ -427,6 +486,7 @@ function CreateAlertChannels({
|
||||
prepareOpsgenieRequest,
|
||||
prepareSlackRequest,
|
||||
prepareMsTeamsRequest,
|
||||
prepareEmailRequest,
|
||||
notifications,
|
||||
],
|
||||
);
|
||||
@ -455,6 +515,7 @@ function CreateAlertChannels({
|
||||
...selectedConfig,
|
||||
...PagerInitialConfig,
|
||||
...OpsgenieInitialConfig,
|
||||
...EmailInitialConfig,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryPromQLData,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
AlertDef,
|
||||
@ -25,6 +25,7 @@ const defaultAnnotations = {
|
||||
|
||||
export const alertDefaults: AlertDef = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
version: ENTITY_VERSION_V4,
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
@ -78,7 +79,6 @@ export const logAlertDefaults: AlertDef = {
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
details: `${window.location.protocol}//${window.location.host}${ROUTES.LOGS_EXPLORER}`,
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
@ -109,7 +109,6 @@ export const traceAlertDefaults: AlertDef = {
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
details: `${window.location.protocol}//${window.location.host}/traces`,
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
@ -140,7 +139,6 @@ export const exceptionAlertDefaults: AlertDef = {
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
details: `${window.location.protocol}//${window.location.host}/exceptions`,
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Form, Row } from 'antd';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
@ -20,6 +22,10 @@ function CreateRules(): JSX.Element {
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const version = queryParams.get('version');
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const [formInstance] = Form.useForm();
|
||||
@ -37,7 +43,10 @@ function CreateRules(): JSX.Element {
|
||||
setInitValues(exceptionAlertDefaults);
|
||||
break;
|
||||
default:
|
||||
setInitValues(alertDefaults);
|
||||
setInitValues({
|
||||
...alertDefaults,
|
||||
version: version || ENTITY_VERSION_V4,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,6 +61,7 @@ function CreateRules(): JSX.Element {
|
||||
if (alertType) {
|
||||
onSelectType(alertType);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [compositeQuery]);
|
||||
|
||||
if (!initValues) {
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Form } from 'antd';
|
||||
import editEmail from 'api/channels/editEmail';
|
||||
import editMsTeamsApi from 'api/channels/editMsTeams';
|
||||
import editOpsgenie from 'api/channels/editOpsgenie';
|
||||
import editPagerApi from 'api/channels/editPager';
|
||||
import editSlackApi from 'api/channels/editSlack';
|
||||
import editWebhookApi from 'api/channels/editWebhook';
|
||||
import testEmail from 'api/channels/testEmail';
|
||||
import testMsTeamsApi from 'api/channels/testMsTeams';
|
||||
import testOpsgenie from 'api/channels/testOpsgenie';
|
||||
import testPagerApi from 'api/channels/testPager';
|
||||
@ -12,6 +14,7 @@ import testWebhookApi from 'api/channels/testWebhook';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ChannelType,
|
||||
EmailChannel,
|
||||
MsTeamsChannel,
|
||||
OpsgenieChannel,
|
||||
PagerChannel,
|
||||
@ -39,7 +42,8 @@ function EditAlertChannels({
|
||||
WebhookChannel &
|
||||
PagerChannel &
|
||||
MsTeamsChannel &
|
||||
OpsgenieChannel
|
||||
OpsgenieChannel &
|
||||
EmailChannel
|
||||
>
|
||||
>({
|
||||
...initialValue,
|
||||
@ -156,6 +160,36 @@ function EditAlertChannels({
|
||||
setSavingState(false);
|
||||
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
||||
|
||||
const prepareEmailRequest = useCallback(
|
||||
() => ({
|
||||
name: selectedConfig?.name || '',
|
||||
to: selectedConfig.to || '',
|
||||
html: selectedConfig.html || '',
|
||||
headers: selectedConfig.headers || {},
|
||||
id,
|
||||
}),
|
||||
[id, selectedConfig],
|
||||
);
|
||||
|
||||
const onEmailEditHandler = useCallback(async () => {
|
||||
setSavingState(true);
|
||||
const request = prepareEmailRequest();
|
||||
const response = await editEmail(request);
|
||||
if (response.statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('channel_edit_done'),
|
||||
});
|
||||
history.replace(ROUTES.ALL_CHANNELS);
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('channel_edit_failed'),
|
||||
});
|
||||
}
|
||||
setSavingState(false);
|
||||
}, [prepareEmailRequest, t, notifications]);
|
||||
|
||||
const preparePagerRequest = useCallback(
|
||||
() => ({
|
||||
name: selectedConfig.name || '',
|
||||
@ -300,6 +334,8 @@ function EditAlertChannels({
|
||||
onMsTeamsEditHandler();
|
||||
} else if (value === ChannelType.Opsgenie) {
|
||||
onOpsgenieEditHandler();
|
||||
} else if (value === ChannelType.Email) {
|
||||
onEmailEditHandler();
|
||||
}
|
||||
},
|
||||
[
|
||||
@ -308,6 +344,7 @@ function EditAlertChannels({
|
||||
onPagerEditHandler,
|
||||
onMsTeamsEditHandler,
|
||||
onOpsgenieEditHandler,
|
||||
onEmailEditHandler,
|
||||
],
|
||||
);
|
||||
|
||||
@ -338,6 +375,10 @@ function EditAlertChannels({
|
||||
request = prepareOpsgenieRequest();
|
||||
if (request) response = await testOpsgenie(request);
|
||||
break;
|
||||
case ChannelType.Email:
|
||||
request = prepareEmailRequest();
|
||||
if (request) response = await testEmail(request);
|
||||
break;
|
||||
default:
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
@ -373,6 +414,7 @@ function EditAlertChannels({
|
||||
prepareSlackRequest,
|
||||
prepareMsTeamsRequest,
|
||||
prepareOpsgenieRequest,
|
||||
prepareEmailRequest,
|
||||
notifications,
|
||||
],
|
||||
);
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { DndContext, DragEndEvent } from '@dnd-kit/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import ExplorerOptions, { ExplorerOptionsProps } from './ExplorerOptions';
|
||||
import {
|
||||
getExplorerToolBarVisibility,
|
||||
setExplorerToolBarVisibility,
|
||||
} from './utils';
|
||||
|
||||
type ExplorerOptionsWrapperProps = Omit<
|
||||
ExplorerOptionsProps,
|
||||
'isExplorerOptionDrop'
|
||||
>;
|
||||
|
||||
function ExplorerOptionWrapper({
|
||||
disabled,
|
||||
query,
|
||||
isLoading,
|
||||
onExport,
|
||||
sourcepage,
|
||||
}: ExplorerOptionsWrapperProps): JSX.Element {
|
||||
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const toolbarVisibility = getExplorerToolBarVisibility(sourcepage);
|
||||
setIsExplorerOptionHidden(!toolbarVisibility);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent): void => {
|
||||
const { active, over } = event;
|
||||
if (
|
||||
over !== null &&
|
||||
active.id === 'explorer-options-draggable' &&
|
||||
over.id === 'explorer-options-droppable'
|
||||
) {
|
||||
setIsExplorerOptionHidden(true);
|
||||
setExplorerToolBarVisibility(false, sourcepage);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DndContext onDragEnd={handleDragEnd}>
|
||||
<ExplorerOptions
|
||||
disabled={disabled}
|
||||
query={query}
|
||||
isLoading={isLoading}
|
||||
onExport={onExport}
|
||||
sourcepage={sourcepage}
|
||||
isExplorerOptionHidden={isExplorerOptionHidden}
|
||||
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
||||
/>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerOptionWrapper;
|
@ -1,6 +1,9 @@
|
||||
.hide-update {
|
||||
left: calc(50% - 41px) !important;
|
||||
}
|
||||
.explorer-update {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
bottom: 24px;
|
||||
left: calc(50% - 225px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -23,6 +26,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-divider {
|
||||
margin: 0;
|
||||
height: 28px;
|
||||
@ -40,7 +47,7 @@
|
||||
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(20px);
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
bottom: 24px;
|
||||
left: calc(50% + 240px);
|
||||
transform: translate(calc(-50% - 120px), 0);
|
||||
transition: left 0.2s linear;
|
||||
@ -55,6 +62,10 @@
|
||||
|
||||
.view-options,
|
||||
.actions {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -102,6 +113,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.app-content {
|
||||
|
@ -1,5 +1,7 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import './ExplorerOptions.styles.scss';
|
||||
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
Button,
|
||||
@ -13,6 +15,7 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import axios from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@ -30,12 +33,25 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||
import { Check, ConciergeBell, Disc3, Plus, X, XCircle } from 'lucide-react';
|
||||
import { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
CSSProperties,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import ExplorerOptionsDroppableArea from './ExplorerOptionsDroppableArea';
|
||||
import {
|
||||
DATASOURCE_VS_ROUTES,
|
||||
generateRGBAFromHex,
|
||||
@ -43,12 +59,17 @@ import {
|
||||
saveNewViewHandler,
|
||||
} from './utils';
|
||||
|
||||
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ExplorerOptions({
|
||||
disabled,
|
||||
isLoading,
|
||||
onExport,
|
||||
query,
|
||||
sourcepage,
|
||||
isExplorerOptionHidden = false,
|
||||
setIsExplorerOptionHidden,
|
||||
}: ExplorerOptionsProps): JSX.Element {
|
||||
const [isExport, setIsExport] = useState<boolean>(false);
|
||||
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
|
||||
@ -58,6 +79,7 @@ function ExplorerOptions({
|
||||
const history = useHistory();
|
||||
const ref = useRef<RefSelectProps>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [isDragEnabled, setIsDragEnabled] = useState(false);
|
||||
|
||||
const onModalToggle = useCallback((value: boolean) => {
|
||||
setIsExport(value);
|
||||
@ -71,6 +93,8 @@ function ExplorerOptions({
|
||||
setIsSaveModalOpen(false);
|
||||
};
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const onCreateAlertsHandler = useCallback(() => {
|
||||
history.push(
|
||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||
@ -247,10 +271,37 @@ function ExplorerOptions({
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
isDragging,
|
||||
} = useDraggable({
|
||||
id: 'explorer-options-draggable',
|
||||
disabled: isDragEnabled,
|
||||
});
|
||||
|
||||
const isEditDeleteSupported = allowedRoles.includes(role as string);
|
||||
|
||||
const style: React.CSSProperties | undefined = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x - 338}px, ${transform.y}px, 0)`,
|
||||
width: `${400 - transform.y * 6}px`,
|
||||
maxWidth: '440px', // initial width of the explorer options
|
||||
overflow: 'hidden',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isQueryUpdated && (
|
||||
<div className="explorer-update">
|
||||
{isQueryUpdated && !isExplorerOptionHidden && !isDragging && (
|
||||
<div
|
||||
className={cx(
|
||||
isEditDeleteSupported ? '' : 'hide-update',
|
||||
'explorer-update',
|
||||
)}
|
||||
>
|
||||
<Tooltip title="Clear this view" placement="top">
|
||||
<Button
|
||||
className="action-icon"
|
||||
@ -258,10 +309,13 @@ function ExplorerOptions({
|
||||
icon={<X size={14} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider type="vertical" />
|
||||
<Divider
|
||||
type="vertical"
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
/>
|
||||
<Tooltip title="Update this view" placement="top">
|
||||
<Button
|
||||
className="action-icon"
|
||||
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
||||
disabled={isViewUpdating}
|
||||
onClick={onUpdateQueryHandler}
|
||||
icon={<Disc3 size={14} />}
|
||||
@ -269,86 +323,105 @@ function ExplorerOptions({
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="explorer-options"
|
||||
style={{
|
||||
background: extraData
|
||||
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
||||
: 'transparent',
|
||||
backdropFilter: 'blur(20px)',
|
||||
}}
|
||||
>
|
||||
<div className="view-options">
|
||||
<Select<string, { key: string; value: string }>
|
||||
showSearch
|
||||
placeholder="Select a view"
|
||||
loading={viewsIsLoading || isRefetching}
|
||||
value={viewName || undefined}
|
||||
onSelect={handleSelect}
|
||||
style={{
|
||||
minWidth: 170,
|
||||
}}
|
||||
dropdownStyle={dropdownStyle}
|
||||
className="views-dropdown"
|
||||
allowClear={{
|
||||
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
|
||||
}}
|
||||
onClear={handleClearSelect}
|
||||
ref={ref}
|
||||
>
|
||||
{viewsData?.data?.data?.map((view) => {
|
||||
const extraData =
|
||||
view.extraData !== '' ? JSON.parse(view.extraData) : '';
|
||||
let bgColor = getRandomColor();
|
||||
if (extraData !== '') {
|
||||
bgColor = extraData.color;
|
||||
}
|
||||
return (
|
||||
<Select.Option key={view.uuid} value={view.name}>
|
||||
<div className="render-options">
|
||||
<span
|
||||
className="dot"
|
||||
style={{
|
||||
background: bgColor,
|
||||
boxShadow: `0px 0px 6px 0px ${bgColor}`,
|
||||
}}
|
||||
/>{' '}
|
||||
{view.name}
|
||||
</div>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
shape="round"
|
||||
onClick={handleSaveViewModalToggle}
|
||||
disabled={viewsIsLoading || isRefetching}
|
||||
>
|
||||
<Disc3 size={16} /> Save this view
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="actions">
|
||||
<Tooltip title="Create Alerts">
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="circle"
|
||||
onClick={onCreateAlertsHandler}
|
||||
{!isExplorerOptionHidden && (
|
||||
<div
|
||||
className="explorer-options"
|
||||
style={{
|
||||
background: extraData
|
||||
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
||||
: 'transparent',
|
||||
backdropFilter: 'blur(20px)',
|
||||
...style,
|
||||
}}
|
||||
ref={setNodeRef}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
>
|
||||
<div className="view-options">
|
||||
<Select<string, { key: string; value: string }>
|
||||
showSearch
|
||||
placeholder="Select a view"
|
||||
loading={viewsIsLoading || isRefetching}
|
||||
value={viewName || undefined}
|
||||
onSelect={handleSelect}
|
||||
style={{
|
||||
minWidth: 170,
|
||||
}}
|
||||
dropdownStyle={dropdownStyle}
|
||||
className="views-dropdown"
|
||||
allowClear={{
|
||||
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
|
||||
}}
|
||||
onDropdownVisibleChange={(open): void => {
|
||||
setIsDragEnabled(open);
|
||||
}}
|
||||
onClear={handleClearSelect}
|
||||
ref={ref}
|
||||
>
|
||||
<ConciergeBell size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{viewsData?.data?.data?.map((view) => {
|
||||
const extraData =
|
||||
view.extraData !== '' ? JSON.parse(view.extraData) : '';
|
||||
let bgColor = getRandomColor();
|
||||
if (extraData !== '') {
|
||||
bgColor = extraData.color;
|
||||
}
|
||||
return (
|
||||
<Select.Option key={view.uuid} value={view.name}>
|
||||
<div className="render-options">
|
||||
<span
|
||||
className="dot"
|
||||
style={{
|
||||
background: bgColor,
|
||||
boxShadow: `0px 0px 6px 0px ${bgColor}`,
|
||||
}}
|
||||
/>{' '}
|
||||
{view.name}
|
||||
</div>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
|
||||
<Tooltip title="Add to Dashboard">
|
||||
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
|
||||
<Plus size={16} />
|
||||
<Button
|
||||
shape="round"
|
||||
onClick={handleSaveViewModalToggle}
|
||||
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||
disabled={viewsIsLoading || isRefetching}
|
||||
>
|
||||
<Disc3 size={16} /> Save this view
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
||||
|
||||
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||
<Tooltip title="Create Alerts">
|
||||
<Button
|
||||
disabled={disabled}
|
||||
shape="circle"
|
||||
onClick={onCreateAlertsHandler}
|
||||
>
|
||||
<ConciergeBell size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Add to Dashboard">
|
||||
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ExplorerOptionsDroppableArea
|
||||
isExplorerOptionHidden={isExplorerOptionHidden}
|
||||
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
||||
sourcepage={sourcepage}
|
||||
isQueryUpdated={isQueryUpdated}
|
||||
handleClearSelect={handleClearSelect}
|
||||
onUpdateQueryHandler={onUpdateQueryHandler}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
className="save-view-modal"
|
||||
@ -406,8 +479,14 @@ export interface ExplorerOptionsProps {
|
||||
query: Query | null;
|
||||
disabled: boolean;
|
||||
sourcepage: DataSource;
|
||||
isExplorerOptionHidden?: boolean;
|
||||
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
ExplorerOptions.defaultProps = { isLoading: false };
|
||||
ExplorerOptions.defaultProps = {
|
||||
isLoading: false,
|
||||
isExplorerOptionHidden: false,
|
||||
setIsExplorerOptionHidden: undefined,
|
||||
};
|
||||
|
||||
export default ExplorerOptions;
|
||||
|
@ -0,0 +1,55 @@
|
||||
.explorer-option-droppable-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: -webkit-fill-available;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
// box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
|
||||
// backdrop-filter: blur(20px);
|
||||
|
||||
.explorer-actions-btn {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-right: 8px;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(20px);
|
||||
height: 24px !important;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-show-btn {
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: rgba(22, 24, 29, 0.40);
|
||||
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(20px);
|
||||
align-self: center;
|
||||
padding: 8px 12px;
|
||||
height: 24px !important;
|
||||
|
||||
.menu-bar {
|
||||
border-radius: 50px;
|
||||
background: var(--bg-slate-200);
|
||||
height: 4px;
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.explorer-option-droppable-container {
|
||||
|
||||
.explorer-show-btn {
|
||||
background: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import './ExplorerOptionsDroppableArea.styles.scss';
|
||||
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Disc3, X } from 'lucide-react';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { setExplorerToolBarVisibility } from './utils';
|
||||
|
||||
interface DroppableAreaProps {
|
||||
isQueryUpdated: boolean;
|
||||
isExplorerOptionHidden?: boolean;
|
||||
sourcepage: DataSource;
|
||||
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
||||
handleClearSelect: () => void;
|
||||
onUpdateQueryHandler: () => void;
|
||||
}
|
||||
|
||||
function ExplorerOptionsDroppableArea({
|
||||
isQueryUpdated,
|
||||
isExplorerOptionHidden,
|
||||
sourcepage,
|
||||
setIsExplorerOptionHidden,
|
||||
handleClearSelect,
|
||||
onUpdateQueryHandler,
|
||||
}: DroppableAreaProps): JSX.Element {
|
||||
const { setNodeRef } = useDroppable({
|
||||
id: 'explorer-options-droppable',
|
||||
});
|
||||
|
||||
const handleShowExplorerOption = (): void => {
|
||||
if (setIsExplorerOptionHidden) {
|
||||
setIsExplorerOptionHidden(false);
|
||||
setExplorerToolBarVisibility(true, sourcepage);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} className="explorer-option-droppable-container">
|
||||
{isExplorerOptionHidden && (
|
||||
<>
|
||||
{isQueryUpdated && (
|
||||
<div className="explorer-actions-btn">
|
||||
<Tooltip title="Clear this view">
|
||||
<Button
|
||||
onClick={handleClearSelect}
|
||||
className="action-btn"
|
||||
style={{ background: Color.BG_CHERRY_500 }}
|
||||
icon={<X size={14} color={Color.BG_INK_500} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Update this View">
|
||||
<Button
|
||||
onClick={onUpdateQueryHandler}
|
||||
className="action-btn"
|
||||
style={{ background: Color.BG_ROBIN_500 }}
|
||||
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
// style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }}
|
||||
className="explorer-show-btn"
|
||||
onClick={handleShowExplorerOption}
|
||||
>
|
||||
<div className="menu-bar" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ExplorerOptionsDroppableArea.defaultProps = {
|
||||
isExplorerOptionHidden: undefined,
|
||||
setIsExplorerOptionHidden: undefined,
|
||||
};
|
||||
|
||||
export default ExplorerOptionsDroppableArea;
|
@ -1,5 +1,6 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { showErrorNotification } from 'components/ExplorerCard/utils';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
@ -67,3 +68,54 @@ export const generateRGBAFromHex = (hex: string, opacity: number): string =>
|
||||
hex.slice(3, 5),
|
||||
16,
|
||||
)}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`;
|
||||
|
||||
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
|
||||
try {
|
||||
const showExplorerToolbar = localStorage.getItem(
|
||||
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
|
||||
);
|
||||
if (showExplorerToolbar === null) {
|
||||
const parsedShowExplorerToolbar: {
|
||||
[DataSource.LOGS]: boolean;
|
||||
[DataSource.TRACES]: boolean;
|
||||
[DataSource.METRICS]: boolean;
|
||||
} = {
|
||||
[DataSource.METRICS]: true,
|
||||
[DataSource.TRACES]: true,
|
||||
[DataSource.LOGS]: true,
|
||||
};
|
||||
localStorage.setItem(
|
||||
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
|
||||
JSON.stringify(parsedShowExplorerToolbar),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar || '{}');
|
||||
return parsedShowExplorerToolbar[dataSource];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const setExplorerToolBarVisibility = (
|
||||
value: boolean,
|
||||
dataSource: string,
|
||||
): void => {
|
||||
try {
|
||||
const showExplorerToolbar = localStorage.getItem(
|
||||
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
|
||||
);
|
||||
if (showExplorerToolbar) {
|
||||
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
|
||||
parsedShowExplorerToolbar[dataSource] = value;
|
||||
localStorage.setItem(
|
||||
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
|
||||
JSON.stringify(parsedShowExplorerToolbar),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
48
frontend/src/container/FormAlertChannels/Settings/Email.tsx
Normal file
48
frontend/src/container/FormAlertChannels/Settings/Email.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { EmailChannel } from '../../CreateAlertChannels/config';
|
||||
|
||||
function EmailForm({ setSelectedConfig }: EmailFormProps): JSX.Element {
|
||||
const { t } = useTranslation('channels');
|
||||
|
||||
const handleInputChange = (field: string) => (
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
): void => {
|
||||
setSelectedConfig((value) => ({
|
||||
...value,
|
||||
[field]: event.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="to"
|
||||
help={t('help_email_to')}
|
||||
label={t('field_email_to')}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
onChange={handleInputChange('to')}
|
||||
placeholder={t('placeholder_email_to')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* <Form.Item name="html" label={t('field_email_html')} required>
|
||||
<TextArea
|
||||
rows={4}
|
||||
onChange={handleInputChange('html')}
|
||||
placeholder={t('placeholder_email_html')}
|
||||
/>
|
||||
</Form.Item> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface EmailFormProps {
|
||||
setSelectedConfig: Dispatch<SetStateAction<Partial<EmailChannel>>>;
|
||||
}
|
||||
|
||||
export default EmailForm;
|
@ -5,6 +5,7 @@ import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ChannelType,
|
||||
EmailChannel,
|
||||
OpsgenieChannel,
|
||||
PagerChannel,
|
||||
SlackChannel,
|
||||
@ -16,6 +17,7 @@ import history from 'lib/history';
|
||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import EmailSettings from './Settings/Email';
|
||||
import MsTeamsSettings from './Settings/MsTeams';
|
||||
import OpsgenieSettings from './Settings/Opsgenie';
|
||||
import PagerSettings from './Settings/Pager';
|
||||
@ -69,6 +71,8 @@ function FormAlertChannels({
|
||||
return <MsTeamsSettings setSelectedConfig={setSelectedConfig} />;
|
||||
case ChannelType.Opsgenie:
|
||||
return <OpsgenieSettings setSelectedConfig={setSelectedConfig} />;
|
||||
case ChannelType.Email:
|
||||
return <EmailSettings setSelectedConfig={setSelectedConfig} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -105,6 +109,9 @@ function FormAlertChannels({
|
||||
<Select.Option value="opsgenie" key="opsgenie">
|
||||
Opsgenie
|
||||
</Select.Option>
|
||||
<Select.Option value="email" key="email">
|
||||
Email
|
||||
</Select.Option>
|
||||
{!isOssFeature?.active && (
|
||||
<Select.Option value="msteams" key="msteams">
|
||||
<div>
|
||||
@ -151,7 +158,13 @@ interface FormAlertChannelsProps {
|
||||
type: ChannelType;
|
||||
setSelectedConfig: Dispatch<
|
||||
SetStateAction<
|
||||
Partial<SlackChannel & WebhookChannel & PagerChannel & OpsgenieChannel>
|
||||
Partial<
|
||||
SlackChannel &
|
||||
WebhookChannel &
|
||||
PagerChannel &
|
||||
OpsgenieChannel &
|
||||
EmailChannel
|
||||
>
|
||||
>
|
||||
>;
|
||||
onTypeChangeHandler: (value: ChannelType) => void;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
@ -39,6 +40,7 @@ export interface ChartPreviewProps {
|
||||
yAxisUnit: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function ChartPreview({
|
||||
name,
|
||||
query,
|
||||
@ -94,6 +96,7 @@ function ChartPreview({
|
||||
allowSelectedIntervalForStepGen,
|
||||
},
|
||||
},
|
||||
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||
{
|
||||
queryKey: [
|
||||
'chartPreview',
|
||||
|
@ -0,0 +1,45 @@
|
||||
.create-alert-modal {
|
||||
.ant-modal-content {
|
||||
background-color: var(--bg-ink-300);
|
||||
.ant-modal-confirm-title {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-modal-confirm-content {
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-confirm-btns {
|
||||
button:nth-of-type(1) {
|
||||
background-color: var(--bg-slate-400);
|
||||
border: none;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ant-modal-content {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
.ant-modal-confirm-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
.ant-modal-confirm-content {
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-confirm-btns {
|
||||
button:nth-of-type(1) {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border: none;
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,14 +2,18 @@ import './QuerySection.styles.scss';
|
||||
|
||||
import { Button, Tabs, Tooltip } from 'antd';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { Atom, Play, Terminal } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
@ -22,6 +26,7 @@ function QuerySection({
|
||||
setQueryCategory,
|
||||
alertType,
|
||||
runQuery,
|
||||
alertDef,
|
||||
panelType,
|
||||
}: QuerySectionProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
@ -50,6 +55,11 @@ function QuerySection({
|
||||
queryVariant: 'static',
|
||||
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||
}}
|
||||
showFunctions={
|
||||
alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||
alertDef.version === ENTITY_VERSION_V4
|
||||
}
|
||||
version={alertDef.version || 'v3'}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -112,6 +122,17 @@ function QuerySection({
|
||||
[],
|
||||
);
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
useEffect(() => {
|
||||
registerShortcut(QBShortcuts.StageAndRunQuery, runQuery);
|
||||
|
||||
return (): void => {
|
||||
deregisterShortcut(QBShortcuts.StageAndRunQuery);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [runQuery]);
|
||||
|
||||
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
|
||||
switch (typ) {
|
||||
case AlertTypes.TRACES_BASED_ALERT:
|
||||
@ -197,6 +218,7 @@ interface QuerySectionProps {
|
||||
setQueryCategory: (n: EQueryType) => void;
|
||||
alertType: AlertTypes;
|
||||
runQuery: VoidFunction;
|
||||
alertDef: AlertDef;
|
||||
panelType: PANEL_TYPES;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
Checkbox,
|
||||
Form,
|
||||
InputNumber,
|
||||
InputNumberProps,
|
||||
@ -213,28 +214,66 @@ function RuleOptions({
|
||||
? renderPromRuleOptions()
|
||||
: renderThresholdRuleOpts()}
|
||||
|
||||
<Space direction="horizontal" align="center">
|
||||
<Form.Item noStyle name={['condition', 'target']}>
|
||||
<InputNumber
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Space direction="vertical" size="large">
|
||||
<Space direction="horizontal" align="center">
|
||||
<Form.Item noStyle name={['condition', 'target']}>
|
||||
<InputNumber
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item noStyle>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={categorySelectOptions}
|
||||
placeholder={t('field_unit')}
|
||||
value={alertDef.condition.targetUnit}
|
||||
onChange={onChangeAlertUnit}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={categorySelectOptions}
|
||||
placeholder={t('field_unit')}
|
||||
value={alertDef.condition.targetUnit}
|
||||
onChange={onChangeAlertUnit}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space direction="horizontal" align="center">
|
||||
<Form.Item noStyle name={['condition', 'alertOnAbsent']}>
|
||||
<Checkbox
|
||||
checked={alertDef?.condition?.alertOnAbsent}
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
alertOnAbsent: e.target.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Typography.Text>{t('text_alert_on_absent')}</Typography.Text>
|
||||
|
||||
<Form.Item noStyle name={['condition', 'absentFor']}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={alertDef?.condition?.absentFor}
|
||||
onChange={(value): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
absentFor: Number(value) || 0,
|
||||
},
|
||||
});
|
||||
}}
|
||||
type="number"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Typography.Text>{t('text_for')}</Typography.Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</FormContainer>
|
||||
</>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import './FormAlertRules.styles.scss';
|
||||
|
||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Col,
|
||||
@ -304,7 +306,7 @@ function FormAlertRules({
|
||||
panelType,
|
||||
]);
|
||||
|
||||
const isAlertAvialable = useIsFeatureDisabled(
|
||||
const isAlertAvailable = useIsFeatureDisabled(
|
||||
FeatureKeys.QUERY_BUILDER_ALERTS,
|
||||
);
|
||||
|
||||
@ -373,6 +375,7 @@ function FormAlertRules({
|
||||
centered: true,
|
||||
content,
|
||||
onOk: saveRule,
|
||||
className: 'create-alert-modal',
|
||||
});
|
||||
}, [t, saveRule, currentQuery]);
|
||||
|
||||
@ -458,8 +461,8 @@ function FormAlertRules({
|
||||
|
||||
const isAlertNameMissing = !formInstance.getFieldValue('alert');
|
||||
|
||||
const isAlertAvialableToSave =
|
||||
isAlertAvialable &&
|
||||
const isAlertAvailableToSave =
|
||||
isAlertAvailable &&
|
||||
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||
alertType !== AlertTypes.METRICS_BASED_ALERT;
|
||||
|
||||
@ -509,6 +512,7 @@ function FormAlertRules({
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
||||
runQuery={handleRunQuery}
|
||||
alertDef={alertDef}
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
|
||||
@ -521,7 +525,7 @@ function FormAlertRules({
|
||||
|
||||
{renderBasicInfo()}
|
||||
<ButtonContainer>
|
||||
<Tooltip title={isAlertAvialableToSave ? MESSAGE.ALERT : ''}>
|
||||
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
type="primary"
|
||||
@ -529,7 +533,7 @@ function FormAlertRules({
|
||||
icon={<SaveOutlined />}
|
||||
disabled={
|
||||
isAlertNameMissing ||
|
||||
isAlertAvialableToSave ||
|
||||
isAlertAvailableToSave ||
|
||||
!isChannelConfigurationValid
|
||||
}
|
||||
>
|
||||
|
@ -5,7 +5,7 @@ import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
||||
@ -29,6 +29,10 @@ function GraphManager({
|
||||
getDefaultTableDataSet(options, data),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTableDataSet(getDefaultTableDataSet(options, data));
|
||||
}, [data, options]);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
|
||||
|
@ -6,6 +6,7 @@ import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import {
|
||||
@ -20,7 +21,7 @@ import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariab
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
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';
|
||||
@ -28,7 +29,7 @@ import uPlot from 'uplot';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { getGraphVisibilityStateOnDataChange } from '../utils';
|
||||
import { getLocalStorageGraphVisibilityState } from '../utils';
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||
import GraphManager from './GraphManager';
|
||||
import { GraphContainer, TimeContainer } from './styles';
|
||||
@ -39,14 +40,13 @@ function FullView({
|
||||
fullViewOptions = true,
|
||||
onClickHandler,
|
||||
name,
|
||||
version,
|
||||
originalName,
|
||||
yAxisUnit,
|
||||
options,
|
||||
onDragSelect,
|
||||
isDependedDataLoaded = false,
|
||||
onToggleModelHandler,
|
||||
parentChartRef,
|
||||
parentGraphVisibilityState,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
@ -59,20 +59,6 @@ function FullView({
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
|
||||
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
options,
|
||||
isExpandedName: false,
|
||||
name: originalName,
|
||||
}),
|
||||
[options, originalName],
|
||||
);
|
||||
|
||||
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState(
|
||||
localStoredVisibilityStates,
|
||||
);
|
||||
|
||||
const getSelectedTime = useCallback(
|
||||
() =>
|
||||
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
|
||||
@ -91,17 +77,35 @@ function FullView({
|
||||
const response = useGetQueryRange(
|
||||
{
|
||||
selectedTime: selectedTime.enum,
|
||||
graphType: widget.panelTypes,
|
||||
graphType:
|
||||
widget.panelTypes === PANEL_TYPES.BAR
|
||||
? PANEL_TYPES.TIME_SERIES
|
||||
: widget.panelTypes,
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
},
|
||||
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
||||
{
|
||||
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
|
||||
enabled: !isDependedDataLoaded && widget.panelTypes !== PANEL_TYPES.LIST, // Internally both the list view panel has it's own query range api call, so we don't need to call it again
|
||||
},
|
||||
);
|
||||
|
||||
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
|
||||
boolean[]
|
||||
>(Array(response.data?.payload.data.result.length).fill(true));
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getLocalStorageGraphVisibilityState({
|
||||
apiResponse: response.data?.payload.data.result || [],
|
||||
name: originalName,
|
||||
});
|
||||
setGraphsVisibilityStates(localStoredVisibilityState);
|
||||
}, [originalName, response.data?.payload.data.result]);
|
||||
|
||||
const canModifyChart = useChartMutable({
|
||||
panelType: widget.panelTypes,
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
@ -144,6 +148,7 @@ function FullView({
|
||||
: 300;
|
||||
|
||||
const newChartOptions = getUPlotChartOptions({
|
||||
id: originalName,
|
||||
yAxisUnit: yAxisUnit || '',
|
||||
apiResponse: response.data?.payload,
|
||||
dimensions: {
|
||||
@ -171,8 +176,7 @@ function FullView({
|
||||
graphsVisibilityStates?.forEach((e, i) => {
|
||||
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||
});
|
||||
parentGraphVisibilityState(graphsVisibilityStates);
|
||||
}, [graphsVisibilityStates, parentGraphVisibilityState]);
|
||||
}, [graphsVisibilityStates]);
|
||||
|
||||
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
|
||||
|
||||
|
@ -50,14 +50,13 @@ export interface FullViewProps {
|
||||
fullViewOptions?: boolean;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
name: string;
|
||||
version?: string;
|
||||
originalName: string;
|
||||
options: uPlot.Options;
|
||||
yAxisUnit?: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
isDependedDataLoaded?: boolean;
|
||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||
parentChartRef: GraphManagerProps['lineChartRef'];
|
||||
parentGraphVisibilityState: Dispatch<SetStateAction<boolean[]>>;
|
||||
}
|
||||
|
||||
export interface GraphManagerProps extends UplotProps {
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
@ -55,6 +57,20 @@ export const getAbbreviatedLabel = (label: string): string => {
|
||||
return newLabel;
|
||||
};
|
||||
|
||||
export const showAllDataSetFromApiResponse = (
|
||||
apiResponse: QueryData[],
|
||||
): LegendEntryProps[] =>
|
||||
apiResponse.map(
|
||||
(item): LegendEntryProps => ({
|
||||
label: getLabelName(
|
||||
item.metric || {},
|
||||
item.queryName || '',
|
||||
item.legend || '',
|
||||
),
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
|
||||
export const showAllDataSet = (options: uPlot.Options): LegendEntryProps[] =>
|
||||
options.series
|
||||
.map(
|
||||
|
@ -32,13 +32,14 @@ import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView';
|
||||
import { Modal } from './styles';
|
||||
import { WidgetGraphComponentProps } from './types';
|
||||
import { getGraphVisibilityStateOnDataChange } from './utils';
|
||||
import { getLocalStorageGraphVisibilityState } from './utils';
|
||||
|
||||
function WidgetGraphComponent({
|
||||
widget,
|
||||
queryResponse,
|
||||
errorMessage,
|
||||
name,
|
||||
version,
|
||||
threshold,
|
||||
headerMenuList,
|
||||
isWarning,
|
||||
@ -62,20 +63,6 @@ function WidgetGraphComponent({
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryResponse.isSuccess) {
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getGraphVisibilityStateOnDataChange({
|
||||
options,
|
||||
isExpandedName: false,
|
||||
name,
|
||||
});
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryResponse.isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lineChartRef.current) return;
|
||||
|
||||
@ -218,6 +205,15 @@ function WidgetGraphComponent({
|
||||
const existingSearchParams = new URLSearchParams(search);
|
||||
existingSearchParams.delete(QueryParams.expandedWidgetId);
|
||||
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
|
||||
if (queryResponse.data?.payload) {
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getLocalStorageGraphVisibilityState({
|
||||
apiResponse: queryResponse.data.payload.data.result,
|
||||
name,
|
||||
});
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
history.push({
|
||||
pathname,
|
||||
search: createQueryParams(updatedQueryParams),
|
||||
@ -283,14 +279,13 @@ function WidgetGraphComponent({
|
||||
>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
version={version}
|
||||
originalName={name}
|
||||
widget={widget}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
parentChartRef={lineChartRef}
|
||||
parentGraphVisibilityState={setGraphVisibility}
|
||||
onDragSelect={onDragSelect}
|
||||
options={options}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
@ -28,6 +29,7 @@ import { getTimeRange } from 'utils/getTimeRange';
|
||||
import EmptyWidget from '../EmptyWidget';
|
||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||
import { GridCardGraphProps } from './types';
|
||||
import { getLocalStorageGraphVisibilityState } from './utils';
|
||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||
|
||||
function GridCardGraph({
|
||||
@ -39,6 +41,7 @@ function GridCardGraph({
|
||||
threshold,
|
||||
variables,
|
||||
fillSpans = false,
|
||||
version,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
@ -132,6 +135,7 @@ function GridCardGraph({
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(variables),
|
||||
},
|
||||
version || DEFAULT_ENTITY_VERSION,
|
||||
{
|
||||
queryKey: [
|
||||
maxTime,
|
||||
@ -183,6 +187,16 @@ function GridCardGraph({
|
||||
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getLocalStorageGraphVisibilityState({
|
||||
apiResponse: queryResponse.data?.payload.data.result || [],
|
||||
name,
|
||||
});
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}, [name, queryResponse.data?.payload.data.result]);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
@ -234,6 +248,7 @@ function GridCardGraph({
|
||||
errorMessage={errorMessage}
|
||||
isWarning={false}
|
||||
name={name}
|
||||
version={version}
|
||||
onDragSelect={onDragSelect}
|
||||
threshold={threshold}
|
||||
headerMenuList={menuList}
|
||||
@ -253,6 +268,7 @@ GridCardGraph.defaultProps = {
|
||||
isQueryEnabled: true,
|
||||
threshold: undefined,
|
||||
headerMenuList: [MenuItemKeys.View],
|
||||
version: 'v3',
|
||||
};
|
||||
|
||||
export default memo(GridCardGraph);
|
||||
|
@ -23,6 +23,7 @@ export interface WidgetGraphComponentProps extends UplotProps {
|
||||
>;
|
||||
errorMessage: string | undefined;
|
||||
name: string;
|
||||
version?: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
threshold?: ReactNode;
|
||||
@ -43,6 +44,7 @@ export interface GridCardGraphProps {
|
||||
isQueryEnabled: boolean;
|
||||
variables?: Dashboard['data']['variables'];
|
||||
fillSpans?: boolean;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
@ -1,14 +1,78 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { LegendEntryProps } from './FullView/types';
|
||||
import { showAllDataSet } from './FullView/utils';
|
||||
import {
|
||||
showAllDataSet,
|
||||
showAllDataSetFromApiResponse,
|
||||
} from './FullView/utils';
|
||||
import {
|
||||
GetGraphVisibilityStateOnLegendClickProps,
|
||||
GraphVisibilityLegendEntryProps,
|
||||
ToggleGraphsVisibilityInChartProps,
|
||||
} from './types';
|
||||
|
||||
export const getLocalStorageGraphVisibilityState = ({
|
||||
apiResponse,
|
||||
name,
|
||||
}: {
|
||||
apiResponse: QueryData[];
|
||||
name: string;
|
||||
}): GraphVisibilityLegendEntryProps => {
|
||||
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
|
||||
graphVisibilityStates: Array(apiResponse.length + 1).fill(true),
|
||||
legendEntry: [
|
||||
{
|
||||
label: 'Timestamp',
|
||||
show: true,
|
||||
},
|
||||
...showAllDataSetFromApiResponse(apiResponse),
|
||||
],
|
||||
};
|
||||
|
||||
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||
const legendGraphFromLocalStore = localStorage.getItem(
|
||||
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||
);
|
||||
let legendFromLocalStore: {
|
||||
name: string;
|
||||
dataIndex: LegendEntryProps[];
|
||||
}[] = [];
|
||||
|
||||
try {
|
||||
legendFromLocalStore = JSON.parse(legendGraphFromLocalStore || '[]');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error parsing GRAPH_VISIBILITY_STATES from local storage',
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
const newGraphVisibilityStates = Array(apiResponse.length + 1).fill(true);
|
||||
legendFromLocalStore.forEach((item) => {
|
||||
const newName = name;
|
||||
if (item.name === newName) {
|
||||
visibilityStateAndLegendEntry.legendEntry = item.dataIndex;
|
||||
apiResponse.forEach((datasets, i) => {
|
||||
const index = item.dataIndex.findIndex(
|
||||
(dataKey) =>
|
||||
dataKey.label ===
|
||||
getLabelName(datasets.metric, datasets.queryName, datasets.legend || ''),
|
||||
);
|
||||
if (index !== -1) {
|
||||
newGraphVisibilityStates[i + 1] = item.dataIndex[index].show;
|
||||
}
|
||||
});
|
||||
visibilityStateAndLegendEntry.graphVisibilityStates = newGraphVisibilityStates;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return visibilityStateAndLegendEntry;
|
||||
};
|
||||
|
||||
export const getGraphVisibilityStateOnDataChange = ({
|
||||
options,
|
||||
isExpandedName,
|
||||
|
@ -1,6 +1,6 @@
|
||||
.fullscreen-grid-container {
|
||||
overflow: auto;
|
||||
margin-top: 1rem;
|
||||
margin: 8px -8px;
|
||||
|
||||
.react-grid-layout {
|
||||
border: none !important;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import './GridCardLayout.styles.scss';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
@ -144,17 +145,19 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
onClick={handle.enter}
|
||||
icon={<FullscreenIcon size={16} />}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
{t('dashboard:full_view')}
|
||||
</Button>
|
||||
<Tooltip title="Open in Full Screen">
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
onClick={handle.enter}
|
||||
icon={<FullscreenIcon size={16} />}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={onAddPanelHandler}
|
||||
icon={<PlusOutlined />}
|
||||
data-testid="add-panel"
|
||||
@ -201,6 +204,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
fillSpans={currentWidget?.fillSpans}
|
||||
version={selectedDashboard?.data?.version}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
|
@ -80,7 +80,6 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)`
|
||||
export const ButtonContainer = styled(Space)`
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
export const Button = styled(ButtonComponent)`
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { getComponentForPanelType } from 'constants/panelTypes';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||
@ -50,11 +51,13 @@ const GridPanelSwitch = forwardRef<
|
||||
? {
|
||||
selectedLogsFields: selectedLogFields || [],
|
||||
query,
|
||||
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
|
||||
selectedTime,
|
||||
}
|
||||
: {
|
||||
selectedTracesFields: selectedTracesFields || [],
|
||||
query,
|
||||
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
|
||||
selectedTime,
|
||||
},
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
||||
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
@ -109,7 +110,6 @@ function DashboardsList(): JSX.Element {
|
||||
width: 30,
|
||||
key: DynamicColumnsKey.CreatedAt,
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
console.log({ a });
|
||||
const prev = new Date(a.createdAt).getTime();
|
||||
const next = new Date(b.createdAt).getTime();
|
||||
|
||||
@ -211,6 +211,7 @@ function DashboardsList(): JSX.Element {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V4,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
@ -304,52 +305,56 @@ function DashboardsList(): JSX.Element {
|
||||
loading={isFilteringDashboards}
|
||||
style={{ marginBottom: 16, marginTop: 16 }}
|
||||
defaultValue={searchString}
|
||||
autoFocus
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col
|
||||
span={6}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<ButtonContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to create dashboards`,
|
||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||
}}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
|
||||
<Dropdown
|
||||
menu={{ items: getMenuItems }}
|
||||
disabled={isDashboardListLoading}
|
||||
placement="bottomRight"
|
||||
{createNewDashboard && (
|
||||
<Col
|
||||
span={6}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<NewDashboardButton
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
data-testid="create-new-dashboard"
|
||||
loading={newDashboardState.loading}
|
||||
danger={newDashboardState.error}
|
||||
<ButtonContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to create dashboards`,
|
||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||
}}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
|
||||
<Dropdown
|
||||
menu={{ items: getMenuItems }}
|
||||
disabled={isDashboardListLoading}
|
||||
placement="bottomRight"
|
||||
>
|
||||
{getText()}
|
||||
</NewDashboardButton>
|
||||
</Dropdown>
|
||||
</Col>
|
||||
<NewDashboardButton
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
data-testid="create-new-dashboard"
|
||||
loading={newDashboardState.loading}
|
||||
danger={newDashboardState.error}
|
||||
>
|
||||
{getText()}
|
||||
</NewDashboardButton>
|
||||
</Dropdown>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
),
|
||||
[
|
||||
isDashboardListLoading,
|
||||
handleSearch,
|
||||
isFilteringDashboards,
|
||||
searchString,
|
||||
createNewDashboard,
|
||||
getMenuItems,
|
||||
newDashboardState.loading,
|
||||
newDashboardState.error,
|
||||
getText,
|
||||
searchString,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -122,12 +122,14 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
fields: selectedFields,
|
||||
linesPerRow: options.maxLines,
|
||||
appendTo: 'end',
|
||||
activeLogIndex,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
|
||||
<Virtuoso
|
||||
ref={ref}
|
||||
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||
data={logs}
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { LIVE_TAIL_GRAPH_INTERVAL } from 'constants/liveTail';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||
@ -41,6 +42,7 @@ function LiveLogsListChart({
|
||||
const { data, isFetching } = useGetExplorerQueryRange(
|
||||
listChartQuery,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
DEFAULT_ENTITY_VERSION,
|
||||
{
|
||||
enabled: isConnectionOpen,
|
||||
refetchInterval: LIVE_TAIL_GRAPH_INTERVAL,
|
||||
|
@ -0,0 +1,25 @@
|
||||
.context-log-renderer {
|
||||
.virtuoso-list {
|
||||
overflow-y: hidden !important;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-slate-200);
|
||||
}
|
||||
|
||||
.ant-row {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
import './ContextLogRenderer.styles.scss';
|
||||
|
||||
import { Skeleton } from 'antd';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import ShowButton from 'container/LogsContextList/ShowButton';
|
||||
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { useContextLogData } from './useContextLogData';
|
||||
|
||||
function ContextLogRenderer({
|
||||
isEdit,
|
||||
query,
|
||||
log,
|
||||
filters,
|
||||
}: ContextLogRendererProps): JSX.Element {
|
||||
const [prevLogPage, setPrevLogPage] = useState<number>(1);
|
||||
const [afterLogPage, setAfterLogPage] = useState<number>(1);
|
||||
const [logs, setLogs] = useState<ILog[]>([log]);
|
||||
|
||||
const {
|
||||
logs: previousLogs,
|
||||
isFetching: isPreviousLogsFetching,
|
||||
handleShowNextLines: handlePreviousLogsShowNextLine,
|
||||
} = useContextLogData({
|
||||
log,
|
||||
filters,
|
||||
isEdit,
|
||||
query,
|
||||
order: ORDERBY_FILTERS.ASC,
|
||||
page: prevLogPage,
|
||||
setPage: setPrevLogPage,
|
||||
});
|
||||
|
||||
const {
|
||||
logs: afterLogs,
|
||||
isFetching: isAfterLogsFetching,
|
||||
handleShowNextLines: handleAfterLogsShowNextLine,
|
||||
} = useContextLogData({
|
||||
log,
|
||||
filters,
|
||||
isEdit,
|
||||
query,
|
||||
order: ORDERBY_FILTERS.DESC,
|
||||
page: afterLogPage,
|
||||
setPage: setAfterLogPage,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLogs((prev) => [...previousLogs, ...prev]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [previousLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogs((prev) => [...prev, ...afterLogs]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [afterLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogs([log]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filters]);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, logTorender: ILog): JSX.Element => (
|
||||
<RawLogView
|
||||
isActiveLog={logTorender.id === log.id}
|
||||
isReadOnly
|
||||
isTextOverflowEllipsisDisabled
|
||||
key={logTorender.id}
|
||||
data={logTorender}
|
||||
linesPerRow={1}
|
||||
/>
|
||||
),
|
||||
[log.id],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="context-log-renderer">
|
||||
<ShowButton
|
||||
isLoading={isPreviousLogsFetching}
|
||||
isDisabled={false}
|
||||
order={ORDERBY_FILTERS.ASC}
|
||||
onClick={handlePreviousLogsShowNextLine}
|
||||
/>
|
||||
{isPreviousLogsFetching && (
|
||||
<Skeleton
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Virtuoso
|
||||
className="virtuoso-list"
|
||||
initialTopMostItemIndex={0}
|
||||
data={logs}
|
||||
itemContent={getItemContent}
|
||||
style={{ height: `calc(${logs.length} * 32px)` }}
|
||||
/>
|
||||
{isAfterLogsFetching && (
|
||||
<Skeleton
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ShowButton
|
||||
isLoading={isAfterLogsFetching}
|
||||
isDisabled={false}
|
||||
order={ORDERBY_FILTERS.DESC}
|
||||
onClick={handleAfterLogsShowNextLine}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ContextLogRendererProps {
|
||||
isEdit: boolean;
|
||||
query: Query;
|
||||
log: ILog;
|
||||
filters: TagFilter | null;
|
||||
}
|
||||
|
||||
export default ContextLogRenderer;
|
@ -1,3 +1,23 @@
|
||||
.log-context-container {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user