mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-02 23:00:42 +08:00
commit
d0feff00a7
@ -133,7 +133,7 @@ services:
|
|||||||
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: signoz/alertmanager:0.23.4
|
image: signoz/alertmanager:0.23.5
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/alertmanager:/data
|
- ./data/alertmanager:/data
|
||||||
command:
|
command:
|
||||||
@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.40.0
|
image: signoz/query-service:0.41.0
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
@ -186,7 +186,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.40.0
|
image: signoz/frontend:0.41.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@ -199,7 +199,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.88.14
|
image: signoz/signoz-otel-collector:0.88.15
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@ -237,7 +237,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.88.14
|
image: signoz/signoz-schema-migrator:0.88.15
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
@ -54,7 +54,7 @@ services:
|
|||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
container_name: signoz-alertmanager
|
container_name: signoz-alertmanager
|
||||||
image: signoz/alertmanager:0.23.4
|
image: signoz/alertmanager:0.23.5
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/alertmanager:/data
|
- ./data/alertmanager:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -66,7 +66,7 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.14}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.15}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@ -81,7 +81,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
otel-collector:
|
otel-collector:
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.88.14
|
image: signoz/signoz-otel-collector:0.88.15
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
@ -149,7 +149,7 @@ services:
|
|||||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.4}
|
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
|
||||||
container_name: signoz-alertmanager
|
container_name: signoz-alertmanager
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/alertmanager:/data
|
- ./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`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.40.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.41.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@ -203,7 +203,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.40.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.41.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -215,7 +215,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.14}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.15}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@ -229,7 +229,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.14}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.15}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"go.signoz.io/signoz/ee/query-service/license"
|
"go.signoz.io/signoz/ee/query-service/license"
|
||||||
"go.signoz.io/signoz/ee/query-service/usage"
|
"go.signoz.io/signoz/ee/query-service/usage"
|
||||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
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/app/logparsingpipeline"
|
||||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||||
@ -31,6 +32,7 @@ type APIHandlerOptions struct {
|
|||||||
UsageManager *usage.Manager
|
UsageManager *usage.Manager
|
||||||
FeatureFlags baseint.FeatureLookup
|
FeatureFlags baseint.FeatureLookup
|
||||||
LicenseManager *license.Manager
|
LicenseManager *license.Manager
|
||||||
|
IntegrationsController *integrations.Controller
|
||||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||||
Cache cache.Cache
|
Cache cache.Cache
|
||||||
// Querier Influx Interval
|
// Querier Influx Interval
|
||||||
@ -56,6 +58,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
|||||||
AppDao: opts.AppDao,
|
AppDao: opts.AppDao,
|
||||||
RuleManager: opts.RulesManager,
|
RuleManager: opts.RulesManager,
|
||||||
FeatureFlags: opts.FeatureFlags,
|
FeatureFlags: opts.FeatureFlags,
|
||||||
|
IntegrationsController: opts.IntegrationsController,
|
||||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||||
Cache: opts.Cache,
|
Cache: opts.Cache,
|
||||||
FluxInterval: opts.FluxInterval,
|
FluxInterval: opts.FluxInterval,
|
||||||
|
@ -12,6 +12,20 @@ import (
|
|||||||
"go.uber.org/zap"
|
"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 {
|
type tierBreakdown struct {
|
||||||
UnitPrice float64 `json:"unitPrice"`
|
UnitPrice float64 `json:"unitPrice"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
@ -24,6 +38,7 @@ type usageResponse struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Unit string `json:"unit"`
|
Unit string `json:"unit"`
|
||||||
Tiers []tierBreakdown `json:"tiers"`
|
Tiers []tierBreakdown `json:"tiers"`
|
||||||
|
DayWiseBreakdown DayWiseBreakdown `json:"dayWiseBreakdown"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type details struct {
|
type details struct {
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||||
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
|
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/logparsingpipeline"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
||||||
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
||||||
@ -171,13 +172,22 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initiate opamp
|
// initiate opamp
|
||||||
_, err = opAmpModel.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
_, err = opAmpModel.InitDB(localDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// ingestion pipelines manager
|
||||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(localDB, "sqlite")
|
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
||||||
|
localDB, "sqlite", integrationsController.GetPipelinesForInstalledIntegrations,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -233,6 +243,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
UsageManager: usageManager,
|
UsageManager: usageManager,
|
||||||
FeatureFlags: lm,
|
FeatureFlags: lm,
|
||||||
LicenseManager: lm,
|
LicenseManager: lm,
|
||||||
|
IntegrationsController: integrationsController,
|
||||||
LogsParsingPipelineController: logParsingPipelineController,
|
LogsParsingPipelineController: logParsingPipelineController,
|
||||||
Cache: c,
|
Cache: c,
|
||||||
FluxInterval: fluxInterval,
|
FluxInterval: fluxInterval,
|
||||||
@ -278,6 +289,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
|||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
|
r.Use(baseapp.LogCommentEnricher)
|
||||||
r.Use(setTimeoutMiddleware)
|
r.Use(setTimeoutMiddleware)
|
||||||
r.Use(s.analyticsMiddleware)
|
r.Use(s.analyticsMiddleware)
|
||||||
r.Use(loggingMiddlewarePrivate)
|
r.Use(loggingMiddlewarePrivate)
|
||||||
@ -310,6 +322,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
}
|
}
|
||||||
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
||||||
|
|
||||||
|
r.Use(baseapp.LogCommentEnricher)
|
||||||
r.Use(setTimeoutMiddleware)
|
r.Use(setTimeoutMiddleware)
|
||||||
r.Use(s.analyticsMiddleware)
|
r.Use(s.analyticsMiddleware)
|
||||||
r.Use(loggingMiddleware)
|
r.Use(loggingMiddleware)
|
||||||
@ -317,6 +330,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
apiHandler.RegisterRoutes(r, am)
|
apiHandler.RegisterRoutes(r, am)
|
||||||
apiHandler.RegisterMetricsRoutes(r, am)
|
apiHandler.RegisterMetricsRoutes(r, am)
|
||||||
apiHandler.RegisterLogsRoutes(r, am)
|
apiHandler.RegisterLogsRoutes(r, am)
|
||||||
|
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||||
apiHandler.RegisterQueryRangeV4Routes(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["queryType"] = postData.CompositeQuery.QueryType
|
||||||
data["panelType"] = postData.CompositeQuery.PanelType
|
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,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: basemodel.AlertChannelEmail,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
Name: basemodel.AlertChannelMsTeams,
|
Name: basemodel.AlertChannelMsTeams,
|
||||||
Active: false,
|
Active: false,
|
||||||
@ -177,6 +184,13 @@ var ProPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: basemodel.AlertChannelEmail,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
Name: basemodel.AlertChannelMsTeams,
|
Name: basemodel.AlertChannelMsTeams,
|
||||||
Active: true,
|
Active: true,
|
||||||
@ -264,6 +278,13 @@ var EnterprisePlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: basemodel.AlertChannelEmail,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
Name: basemodel.AlertChannelMsTeams,
|
Name: basemodel.AlertChannelMsTeams,
|
||||||
Active: true,
|
Active: true,
|
||||||
|
@ -107,6 +107,7 @@
|
|||||||
"react-virtuoso": "4.0.3",
|
"react-virtuoso": "4.0.3",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
|
"rehype-raw": "7.0.0",
|
||||||
"stream": "^0.0.2",
|
"stream": "^0.0.2",
|
||||||
"style-loader": "1.3.0",
|
"style-loader": "1.3.0",
|
||||||
"styled-components": "^5.3.11",
|
"styled-components": "^5.3.11",
|
||||||
@ -203,6 +204,7 @@
|
|||||||
"jest-styled-components": "^7.0.8",
|
"jest-styled-components": "^7.0.8",
|
||||||
"lint-staged": "^12.5.0",
|
"lint-staged": "^12.5.0",
|
||||||
"msw": "1.3.2",
|
"msw": "1.3.2",
|
||||||
|
"npm-run-all": "latest",
|
||||||
"portfinder-sync": "^0.0.2",
|
"portfinder-sync": "^0.0.2",
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"raw-loader": "4.0.2",
|
"raw-loader": "4.0.2",
|
||||||
@ -216,8 +218,7 @@
|
|||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"typescript-plugin-css-modules": "5.0.1",
|
"typescript-plugin-css-modules": "5.0.1",
|
||||||
"webpack-bundle-analyzer": "^4.5.0",
|
"webpack-bundle-analyzer": "^4.5.0",
|
||||||
"webpack-cli": "^4.9.2",
|
"webpack-cli": "^4.9.2"
|
||||||
"npm-run-all": "latest"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.(js|jsx|ts|tsx)": [
|
"*.(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": "Exceptions-based Alert",
|
||||||
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
||||||
"field_unit": "Threshold unit",
|
"field_unit": "Threshold unit",
|
||||||
|
"text_alert_on_absent": "Send a notification if data is missing for",
|
||||||
|
"text_for": "minutes",
|
||||||
"selected_query_placeholder": "Select query"
|
"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_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.",
|
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||||
"your_graph_build_with": "Your graph built with",
|
"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": "Exceptions-based Alert",
|
||||||
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
||||||
"field_unit": "Threshold unit",
|
"field_unit": "Threshold unit",
|
||||||
|
"text_alert_on_absent": "Send a notification if data is missing for",
|
||||||
|
"text_for": "minutes",
|
||||||
"selected_query_placeholder": "Select query"
|
"selected_query_placeholder": "Select query"
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,12 @@
|
|||||||
"field_opsgenie_api_key": "API Key",
|
"field_opsgenie_api_key": "API Key",
|
||||||
"field_opsgenie_description": "Description",
|
"field_opsgenie_description": "Description",
|
||||||
"placeholder_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_username": "User Name (optional)",
|
||||||
"field_webhook_password": "Password (optional)",
|
"field_webhook_password": "Password (optional)",
|
||||||
"field_pager_routing_key": "Routing Key",
|
"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_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.",
|
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||||
"your_graph_build_with": "Your graph built with",
|
"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_METRICS": "SigNoz | Service Metrics",
|
||||||
"SERVICE_MAP": "SigNoz | Service Map",
|
"SERVICE_MAP": "SigNoz | Service Map",
|
||||||
"GET_STARTED": "SigNoz | Get Started",
|
"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": "SigNoz | Trace",
|
||||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||||
@ -40,8 +44,9 @@
|
|||||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||||
"SUPPORT": "SigNoz | Support",
|
"SUPPORT": "SigNoz | Support",
|
||||||
"LOGS_SAVE_VIEWS": "SigNoz | Logs Save Views",
|
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
|
||||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Save Views",
|
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
||||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
"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
|
# create temporary tsconfig which includes only passed files
|
||||||
str="{
|
str="{
|
||||||
\"extends\": \"./tsconfig.json\",
|
\"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
|
echo $str > tsconfig.tmp
|
||||||
|
|
||||||
|
@ -190,3 +190,18 @@ export const WorkspaceBlocked = Loadable(
|
|||||||
export const ShortcutsPage = Loadable(
|
export const ShortcutsPage = Loadable(
|
||||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
() => 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 ROUTES from 'constants/routes';
|
||||||
import Shortcuts from 'pages/Shortcuts/Shortcuts';
|
|
||||||
import WorkspaceBlocked from 'pages/WorkspaceLocked';
|
|
||||||
import { RouteProps } from 'react-router-dom';
|
import { RouteProps } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,6 +14,8 @@ import {
|
|||||||
EditRulesPage,
|
EditRulesPage,
|
||||||
ErrorDetails,
|
ErrorDetails,
|
||||||
IngestionSettings,
|
IngestionSettings,
|
||||||
|
InstalledIntegrations,
|
||||||
|
IntegrationsMarketPlace,
|
||||||
LicensePage,
|
LicensePage,
|
||||||
ListAllALertsPage,
|
ListAllALertsPage,
|
||||||
LiveLogs,
|
LiveLogs,
|
||||||
@ -35,6 +35,7 @@ import {
|
|||||||
ServiceMetricsPage,
|
ServiceMetricsPage,
|
||||||
ServicesTablePage,
|
ServicesTablePage,
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
|
ShortcutsPage,
|
||||||
SignupPage,
|
SignupPage,
|
||||||
SomethingWentWrong,
|
SomethingWentWrong,
|
||||||
StatusPage,
|
StatusPage,
|
||||||
@ -45,6 +46,7 @@ import {
|
|||||||
TracesSaveViews,
|
TracesSaveViews,
|
||||||
UnAuthorized,
|
UnAuthorized,
|
||||||
UsageExplorerPage,
|
UsageExplorerPage,
|
||||||
|
WorkspaceBlocked,
|
||||||
} from './pageComponents';
|
} from './pageComponents';
|
||||||
|
|
||||||
const routes: AppRoutes[] = [
|
const routes: AppRoutes[] = [
|
||||||
@ -57,7 +59,7 @@ const routes: AppRoutes[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.GET_STARTED,
|
path: ROUTES.GET_STARTED,
|
||||||
exact: true,
|
exact: false,
|
||||||
component: Onboarding,
|
component: Onboarding,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'GET_STARTED',
|
key: 'GET_STARTED',
|
||||||
@ -331,10 +333,24 @@ const routes: AppRoutes[] = [
|
|||||||
{
|
{
|
||||||
path: ROUTES.SHORTCUTS,
|
path: ROUTES.SHORTCUTS,
|
||||||
exact: true,
|
exact: true,
|
||||||
component: Shortcuts,
|
component: ShortcutsPage,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'SHORTCUTS',
|
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 = {
|
export const SUPPORT_ROUTE: AppRoutes = {
|
||||||
@ -358,6 +374,8 @@ export const oldRoutes = [
|
|||||||
'/logs/old-logs-explorer',
|
'/logs/old-logs-explorer',
|
||||||
'/logs-explorer',
|
'/logs-explorer',
|
||||||
'/logs-explorer/live',
|
'/logs-explorer/live',
|
||||||
|
'/logs-save-views',
|
||||||
|
'/traces-save-views',
|
||||||
'/settings/api-keys',
|
'/settings/api-keys',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -366,6 +384,8 @@ export const oldNewRoutesMapping: Record<string, string> = {
|
|||||||
'/logs/old-logs-explorer': '/logs/old-logs-explorer',
|
'/logs/old-logs-explorer': '/logs/old-logs-explorer',
|
||||||
'/logs-explorer': '/logs/logs-explorer',
|
'/logs-explorer': '/logs/logs-explorer',
|
||||||
'/logs-explorer/live': '/logs/logs-explorer/live',
|
'/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',
|
'/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 apiV2 = '/api/v2/';
|
||||||
export const apiV3 = '/api/v3/';
|
export const apiV3 = '/api/v3/';
|
||||||
|
export const apiV4 = '/api/v4/';
|
||||||
export const apiAlertManager = '/api/alertmanager';
|
export const apiAlertManager = '/api/alertmanager';
|
||||||
|
|
||||||
export default apiV1;
|
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 { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1';
|
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4 } from './apiV1';
|
||||||
import { Logout } from './utils';
|
import { Logout } from './utils';
|
||||||
|
|
||||||
const interceptorsResponse = (
|
const interceptorsResponse = (
|
||||||
@ -114,6 +114,7 @@ ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
|||||||
export const ApiV3Instance = axios.create({
|
export const ApiV3Instance = axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
|
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
ApiV3Instance.interceptors.response.use(
|
ApiV3Instance.interceptors.response.use(
|
||||||
interceptorsResponse,
|
interceptorsResponse,
|
||||||
interceptorRejected,
|
interceptorRejected,
|
||||||
@ -121,6 +122,18 @@ ApiV3Instance.interceptors.response.use(
|
|||||||
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
|
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(
|
AxiosAlertManagerInstance.interceptors.response.use(
|
||||||
interceptorsResponse,
|
interceptorsResponse,
|
||||||
interceptorRejected,
|
interceptorRejected,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ApiV3Instance as axios } from 'api';
|
import { ApiV3Instance, ApiV4Instance } from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
MetricRangePayloadV3,
|
MetricRangePayloadV3,
|
||||||
@ -9,10 +10,23 @@ import {
|
|||||||
|
|
||||||
export const getMetricsQueryRange = async (
|
export const getMetricsQueryRange = async (
|
||||||
props: QueryRangePayload,
|
props: QueryRangePayload,
|
||||||
|
version: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||||
try {
|
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 {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
@ -115,6 +115,9 @@ function CustomTimePicker({
|
|||||||
|
|
||||||
const handleOpenChange = (newOpen: boolean): void => {
|
const handleOpenChange = (newOpen: boolean): void => {
|
||||||
setOpen(newOpen);
|
setOpen(newOpen);
|
||||||
|
if (!newOpen) {
|
||||||
|
setCustomDTPickerVisible?.(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedHandleInputChange = debounce((inputValue): void => {
|
const debouncedHandleInputChange = debounce((inputValue): void => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import './CustomTimePicker.styles.scss';
|
import './CustomTimePicker.styles.scss';
|
||||||
|
|
||||||
import { Button, DatePicker } from 'antd';
|
import { Button } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
@ -9,12 +9,10 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
RelativeDurationSuggestionOptions,
|
RelativeDurationSuggestionOptions,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
|
||||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import RangePickerModal from './RangePickerModal';
|
||||||
|
|
||||||
interface CustomTimePickerPopoverContentProps {
|
interface CustomTimePickerPopoverContentProps {
|
||||||
options: any[];
|
options: any[];
|
||||||
@ -40,35 +38,12 @@ function CustomTimePickerPopoverContent({
|
|||||||
handleGoLive,
|
handleGoLive,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||||
const { RangePicker } = DatePicker;
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
|
||||||
(state) => state.globalTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||||
pathname,
|
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 {
|
function getTimeChips(options: Option[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="relative-date-time-section">
|
<div className="relative-date-time-section">
|
||||||
@ -105,26 +80,32 @@ function CustomTimePickerPopoverContent({
|
|||||||
}}
|
}}
|
||||||
className={cx(
|
className={cx(
|
||||||
'date-time-options-btn',
|
'date-time-options-btn',
|
||||||
selectedTime === option.value && 'active',
|
customDateTimeVisible
|
||||||
|
? option.value === 'custom' && 'active'
|
||||||
|
: selectedTime === option.value && 'active',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative-date-time">
|
<div
|
||||||
|
className={cx(
|
||||||
|
'relative-date-time',
|
||||||
|
selectedTime === 'custom' || customDateTimeVisible
|
||||||
|
? 'date-picker'
|
||||||
|
: 'relative-times',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{selectedTime === 'custom' || customDateTimeVisible ? (
|
{selectedTime === 'custom' || customDateTimeVisible ? (
|
||||||
<RangePicker
|
<RangePickerModal
|
||||||
disabledDate={disabledDate}
|
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||||
allowClear
|
setIsOpen={setIsOpen}
|
||||||
onCalendarChange={onModalOkHandler}
|
onCustomDateHandler={onCustomDateHandler}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
selectedTime={selectedTime}
|
||||||
{...(selectedTime === 'custom' && {
|
|
||||||
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div className="relative-times-container">
|
||||||
<div className="time-heading">RELATIVE TIMES</div>
|
<div className="time-heading">RELATIVE TIMES</div>
|
||||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||||
</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 {
|
.ant-drawer-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import dayjs from 'dayjs';
|
|||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
// utils
|
// utils
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
@ -19,9 +20,8 @@ import { ILog } from 'types/api/logs/log';
|
|||||||
// components
|
// components
|
||||||
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
||||||
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||||
import LogStateIndicator, {
|
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||||
LogType,
|
import { getLogIndicatorType } from '../LogStateIndicator/utils';
|
||||||
} from '../LogStateIndicator/LogStateIndicator';
|
|
||||||
// styles
|
// styles
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
@ -114,6 +114,8 @@ function ListLogView({
|
|||||||
onClearActiveLog: handleClearActiveContextLog,
|
onClearActiveLog: handleClearActiveContextLog,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const handlerClearActiveContextLog = useCallback(
|
const handlerClearActiveContextLog = useCallback(
|
||||||
(event: React.MouseEvent | React.KeyboardEvent) => {
|
(event: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -149,7 +151,7 @@ function ListLogView({
|
|||||||
[flattenLogData.timestamp],
|
[flattenLogData.timestamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
const logType = logData?.attributes_string?.log_level || LogType.INFO;
|
const logType = getLogIndicatorType(logData);
|
||||||
|
|
||||||
const handleMouseEnter = (): void => {
|
const handleMouseEnter = (): void => {
|
||||||
setHasActionButtons(true);
|
setHasActionButtons(true);
|
||||||
@ -163,6 +165,7 @@ function ListLogView({
|
|||||||
<>
|
<>
|
||||||
<Container
|
<Container
|
||||||
$isActiveLog={isHighlighted}
|
$isActiveLog={isHighlighted}
|
||||||
|
$isDarkMode={isDarkMode}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
onClick={handleDetailedView}
|
onClick={handleDetailedView}
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { getActiveLogBackground } from 'utils/logs';
|
|
||||||
|
|
||||||
export const Container = styled(Card)<{
|
export const Container = styled(Card)<{
|
||||||
$isActiveLog: boolean;
|
$isActiveLog: boolean;
|
||||||
|
$isDarkMode: boolean;
|
||||||
}>`
|
}>`
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 0.3rem 0.6rem;
|
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)`
|
export const Text = styled(Typography.Text)`
|
||||||
|
@ -10,15 +10,27 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
&.INFO {
|
&.INFO {
|
||||||
background-color: #1d212d;
|
background-color: var(--bg-slate-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.WARNING {
|
&.WARNING, &.WARN {
|
||||||
background-color: #ffcd56;
|
background-color: var(--bg-amber-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ERROR {
|
&.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';
|
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 = {
|
export const LogType = {
|
||||||
INFO: 'INFO',
|
INFO: 'INFO',
|
||||||
WARNING: 'WARNING',
|
WARNING: 'WARNING',
|
||||||
ERROR: 'ERROR',
|
ERROR: 'ERROR',
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
function LogStateIndicator({
|
function LogStateIndicator({
|
||||||
type,
|
type,
|
||||||
isActive,
|
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';
|
} from 'react';
|
||||||
|
|
||||||
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||||
import LogStateIndicator, {
|
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||||
LogType,
|
import { getLogIndicatorType } from '../LogStateIndicator/utils';
|
||||||
} from '../LogStateIndicator/LogStateIndicator';
|
|
||||||
// styles
|
// styles
|
||||||
import { RawLogContent, RawLogViewContainer } from './styles';
|
import { RawLogContent, RawLogViewContainer } from './styles';
|
||||||
import { RawLogViewProps } from './types';
|
import { RawLogViewProps } from './types';
|
||||||
@ -64,7 +63,7 @@ function RawLogView({
|
|||||||
|
|
||||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
||||||
|
|
||||||
const logType = data?.attributes_string?.log_level || LogType.INFO;
|
const logType = getLogIndicatorType(data);
|
||||||
|
|
||||||
const updatedSelecedFields = useMemo(
|
const updatedSelecedFields = useMemo(
|
||||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||||
@ -164,7 +163,11 @@ function RawLogView({
|
|||||||
>
|
>
|
||||||
<LogStateIndicator
|
<LogStateIndicator
|
||||||
type={logType}
|
type={logType}
|
||||||
isActive={activeLog?.id === data.id || activeContextLog?.id === data.id}
|
isActive={
|
||||||
|
activeLog?.id === data.id ||
|
||||||
|
activeContextLog?.id === data.id ||
|
||||||
|
isActiveLog
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RawLogContent
|
<RawLogContent
|
||||||
|
@ -30,6 +30,14 @@ export const RawLogViewContainer = styled(Row)<{
|
|||||||
$isActiveLog
|
$isActiveLog
|
||||||
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
||||||
: getDefaultLogBackground($isReadOnly, $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)`
|
export const ExpandIconWrapper = styled(Col)`
|
||||||
|
@ -14,12 +14,12 @@ export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
|||||||
lineHeight: '18px',
|
lineHeight: '18px',
|
||||||
letterSpacing: '-0.07px',
|
letterSpacing: '-0.07px',
|
||||||
marginBottom: '0px',
|
marginBottom: '0px',
|
||||||
|
minWidth: '10rem',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultTableStyle: CSSProperties = {
|
export const defaultTableStyle: CSSProperties = {
|
||||||
minWidth: '40rem',
|
minWidth: '40rem',
|
||||||
maxWidth: '40rem',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultListViewPanelStyle: CSSProperties = {
|
export const defaultListViewPanelStyle: CSSProperties = {
|
||||||
|
@ -23,6 +23,7 @@ export type UseTableViewProps = {
|
|||||||
onOpenLogsContext?: (log: ILog) => void;
|
onOpenLogsContext?: (log: ILog) => void;
|
||||||
onClickExpand?: (log: ILog) => void;
|
onClickExpand?: (log: ILog) => void;
|
||||||
activeLog?: ILog | null;
|
activeLog?: ILog | null;
|
||||||
|
activeLogIndex?: number;
|
||||||
activeContextLog?: ILog | null;
|
activeContextLog?: ILog | null;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
} & LogsTableViewProps;
|
} & LogsTableViewProps;
|
||||||
|
@ -7,12 +7,10 @@ import dayjs from 'dayjs';
|
|||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { defaultTo } from 'lodash-es';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import LogStateIndicator, {
|
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
|
||||||
LogType,
|
import { getLogIndicatorTypeForTable } from '../LogStateIndicator/utils';
|
||||||
} from '../LogStateIndicator/LogStateIndicator';
|
|
||||||
import {
|
import {
|
||||||
defaultListViewPanelStyle,
|
defaultListViewPanelStyle,
|
||||||
defaultTableStyle,
|
defaultTableStyle,
|
||||||
@ -84,7 +82,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
children: (
|
children: (
|
||||||
<div className="table-timestamp">
|
<div className="table-timestamp">
|
||||||
<LogStateIndicator
|
<LogStateIndicator
|
||||||
type={defaultTo(item.log_level, LogType.INFO) as string}
|
type={getLogIndicatorTypeForTable(item)}
|
||||||
isActive={
|
isActive={
|
||||||
activeLog?.id === item.id || activeContextLog?.id === item.id
|
activeLog?.id === item.id || activeContextLog?.id === item.id
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,6 @@ export default function LogsFormatOptionsMenu({
|
|||||||
setAddNewColumn(!addNewColumn);
|
setAddNewColumn(!addNewColumn);
|
||||||
};
|
};
|
||||||
|
|
||||||
// console.log('optionsMenuConfig', config);
|
|
||||||
|
|
||||||
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
|
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
|
||||||
if (
|
if (
|
||||||
maxLinesPerRow &&
|
maxLinesPerRow &&
|
||||||
@ -221,8 +219,6 @@ export default function LogsFormatOptionsMenu({
|
|||||||
className="column-name"
|
className="column-name"
|
||||||
key={value}
|
key={value}
|
||||||
onClick={(eve): void => {
|
onClick={(eve): void => {
|
||||||
console.log('coluimn name', label, value);
|
|
||||||
|
|
||||||
eve.stopPropagation();
|
eve.stopPropagation();
|
||||||
|
|
||||||
if (addColumn && addColumn?.onSelect) {
|
if (addColumn && addColumn?.onSelect) {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
/* eslint-disable no-restricted-syntax */
|
/* eslint-disable no-restricted-syntax */
|
||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { CodeProps } from 'react-markdown/lib/ast-to-react';
|
import { CodeProps } from 'react-markdown/lib/ast-to-react';
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
|
||||||
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
|
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
|
||||||
|
|
||||||
@ -74,6 +76,10 @@ const interpolateMarkdown = (
|
|||||||
return interpolatedContent;
|
return interpolatedContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function CustomTag({ color }: { color: string }): JSX.Element {
|
||||||
|
return <h1 style={{ color }}>This is custom element</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
function MarkdownRenderer({
|
function MarkdownRenderer({
|
||||||
markdownContent,
|
markdownContent,
|
||||||
variables,
|
variables,
|
||||||
@ -85,12 +91,14 @@ function MarkdownRenderer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
rehypePlugins={[rehypeRaw as any]}
|
||||||
components={{
|
components={{
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
a: Link,
|
a: Link,
|
||||||
pre: Pre,
|
pre: Pre,
|
||||||
code: Code,
|
code: Code,
|
||||||
|
customtag: CustomTag,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{interpolatedMarkdown}
|
{interpolatedMarkdown}
|
||||||
|
@ -13,3 +13,6 @@ export const SIGNOZ_UPGRADE_PLAN_URL =
|
|||||||
'https://upgrade.signoz.io/upgrade-from-app';
|
'https://upgrade.signoz.io/upgrade-from-app';
|
||||||
|
|
||||||
export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
|
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',
|
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||||
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
||||||
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
|
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
|
||||||
|
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,11 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import {
|
import {
|
||||||
logsAggregateOperatorOptions,
|
logsAggregateOperatorOptions,
|
||||||
metricAggregateOperatorOptions,
|
metricAggregateOperatorOptions,
|
||||||
|
metricsGaugeAggregateOperatorOptions,
|
||||||
|
metricsGaugeSpaceAggregateOperatorOptions,
|
||||||
|
metricsHistogramSpaceAggregateOperatorOptions,
|
||||||
|
metricsSumAggregateOperatorOptions,
|
||||||
|
metricsSumSpaceAggregateOperatorOptions,
|
||||||
tracesAggregateOperatorOptions,
|
tracesAggregateOperatorOptions,
|
||||||
} from './queryBuilderOperators';
|
} from './queryBuilderOperators';
|
||||||
|
|
||||||
@ -74,6 +79,18 @@ export const mapOfOperators = {
|
|||||||
traces: tracesAggregateOperatorOptions,
|
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[]> = {
|
export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = {
|
||||||
metrics: [
|
metrics: [
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
@ -148,6 +165,9 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||||
aggregateOperator: MetricAggregateOperator.COUNT,
|
aggregateOperator: MetricAggregateOperator.COUNT,
|
||||||
aggregateAttribute: initialAutocompleteData,
|
aggregateAttribute: initialAutocompleteData,
|
||||||
|
timeAggregation: MetricAggregateOperator.RATE,
|
||||||
|
spaceAggregation: MetricAggregateOperator.SUM,
|
||||||
|
functions: [],
|
||||||
filters: { items: [], op: 'AND' },
|
filters: { items: [], op: 'AND' },
|
||||||
expression: createNewBuilderItemName({
|
expression: createNewBuilderItemName({
|
||||||
existNames: [],
|
existNames: [],
|
||||||
@ -160,7 +180,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
orderBy: [],
|
orderBy: [],
|
||||||
groupBy: [],
|
groupBy: [],
|
||||||
legend: '',
|
legend: '',
|
||||||
reduceTo: 'sum',
|
reduceTo: 'avg',
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
|
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
|
||||||
@ -268,6 +288,14 @@ export enum PANEL_TYPES {
|
|||||||
EMPTY_WIDGET = 'EMPTY_WIDGET',
|
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 type IQueryBuilderState = 'search';
|
||||||
|
|
||||||
export const QUERY_BUILDER_SEARCH_VALUES = {
|
export const QUERY_BUILDER_SEARCH_VALUES = {
|
||||||
|
@ -302,3 +302,126 @@ export const logsAggregateOperatorOptions: SelectOption<string, string>[] = [
|
|||||||
label: 'Rate_max',
|
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',
|
TRACE_DETAIL: '/trace/:id',
|
||||||
TRACES_EXPLORER: '/traces-explorer',
|
TRACES_EXPLORER: '/traces-explorer',
|
||||||
GET_STARTED: '/get-started',
|
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',
|
USAGE_EXPLORER: '/usage-explorer',
|
||||||
APPLICATION: '/services',
|
APPLICATION: '/services',
|
||||||
ALL_DASHBOARD: '/dashboard',
|
ALL_DASHBOARD: '/dashboard',
|
||||||
@ -42,10 +47,13 @@ const ROUTES = {
|
|||||||
TRACE_EXPLORER: '/trace-explorer',
|
TRACE_EXPLORER: '/trace-explorer',
|
||||||
BILLING: '/billing',
|
BILLING: '/billing',
|
||||||
SUPPORT: '/support',
|
SUPPORT: '/support',
|
||||||
LOGS_SAVE_VIEWS: '/logs-save-views',
|
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
||||||
TRACES_SAVE_VIEWS: '/traces-save-views',
|
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||||
WORKSPACE_LOCKED: '/workspace-locked',
|
WORKSPACE_LOCKED: '/workspace-locked',
|
||||||
SHORTCUTS: '/shortcuts',
|
SHORTCUTS: '/shortcuts',
|
||||||
|
INTEGRATIONS_BASE: '/integrations',
|
||||||
|
INTEGRATIONS_INSTALLED: '/integrations/installed',
|
||||||
|
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default ROUTES;
|
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 routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||||
const pageTitle = t(routeKey);
|
const pageTitle = t(routeKey);
|
||||||
const renderFullScreen =
|
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);
|
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
||||||
|
|
||||||
|
@ -1,13 +1,29 @@
|
|||||||
.billing-container {
|
.billing-container {
|
||||||
padding: 16px 0;
|
padding-top: 36px;
|
||||||
width: 100%;
|
width: 65%;
|
||||||
|
|
||||||
.billing-summary {
|
.billing-summary {
|
||||||
margin: 24px 8px;
|
margin: 24px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.billing-details {
|
.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 {
|
.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 {
|
.ant-skeleton.ant-skeleton-element.ant-skeleton-active {
|
||||||
@ -34,3 +59,20 @@
|
|||||||
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
|
.ant-skeleton.ant-skeleton-element .ant-skeleton-input {
|
||||||
min-width: 100% !important;
|
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';
|
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', () => {
|
describe('BillingContainer', () => {
|
||||||
test('Component should render', async () => {
|
test('Component should render', async () => {
|
||||||
act(() => {
|
act(() => {
|
||||||
render(<BillingContainer />);
|
render(<BillingContainer />);
|
||||||
});
|
});
|
||||||
const unit = screen.getAllByText(/unit/i);
|
|
||||||
expect(unit[1]).toBeInTheDocument();
|
|
||||||
const dataInjection = screen.getByRole('columnheader', {
|
const dataInjection = screen.getByRole('columnheader', {
|
||||||
name: /data ingested/i,
|
name: /data ingested/i,
|
||||||
});
|
});
|
||||||
@ -32,24 +55,15 @@ describe('BillingContainer', () => {
|
|||||||
});
|
});
|
||||||
expect(cost).toBeInTheDocument();
|
expect(cost).toBeInTheDocument();
|
||||||
|
|
||||||
const total = screen.getByRole('cell', {
|
|
||||||
name: /total/i,
|
|
||||||
});
|
|
||||||
expect(total).toBeInTheDocument();
|
|
||||||
|
|
||||||
const manageBilling = screen.getByRole('button', {
|
const manageBilling = screen.getByRole('button', {
|
||||||
name: /manage billing/i,
|
name: /manage billing/i,
|
||||||
});
|
});
|
||||||
expect(manageBilling).toBeInTheDocument();
|
expect(manageBilling).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar = screen.getByRole('cell', {
|
const dollar = screen.getByText(/\$0/i);
|
||||||
name: /\$0/i,
|
|
||||||
});
|
|
||||||
expect(dollar).toBeInTheDocument();
|
expect(dollar).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = screen.getByRole('heading', {
|
const currentBill = screen.getByText('Billing');
|
||||||
name: /current bill total/i,
|
|
||||||
});
|
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,9 +75,7 @@ describe('BillingContainer', () => {
|
|||||||
const freeTrailText = await screen.findByText('Free Trial');
|
const freeTrailText = await screen.findByText('Free Trial');
|
||||||
expect(freeTrailText).toBeInTheDocument();
|
expect(freeTrailText).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = await screen.findByRole('heading', {
|
const currentBill = screen.getByText('Billing');
|
||||||
name: /current bill total/i,
|
|
||||||
});
|
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findByText(/\$0/i);
|
const dollar0 = await screen.findByText(/\$0/i);
|
||||||
@ -102,9 +114,7 @@ describe('BillingContainer', () => {
|
|||||||
render(<BillingContainer />);
|
render(<BillingContainer />);
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentBill = await screen.findByRole('heading', {
|
const currentBill = screen.getByText('Billing');
|
||||||
name: /current bill total/i,
|
|
||||||
});
|
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findByText(/\$0/i);
|
const dollar0 = await screen.findByText(/\$0/i);
|
||||||
@ -137,45 +147,30 @@ describe('BillingContainer', () => {
|
|||||||
res(ctx.status(200), ctx.json(notOfTrailResponse)),
|
res(ctx.status(200), ctx.json(notOfTrailResponse)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
render(<BillingContainer />);
|
const { findByText } = render(<BillingContainer />);
|
||||||
|
|
||||||
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
|
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
|
||||||
billingSuccessResponse.data.billingPeriodStart,
|
billingSuccessResponse.data.billingPeriodStart,
|
||||||
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
|
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
|
||||||
|
|
||||||
const billingPeriod = await screen.findByRole('heading', {
|
const billingPeriod = await findByText(billingPeriodText);
|
||||||
name: new RegExp(billingPeriodText, 'i'),
|
|
||||||
});
|
|
||||||
expect(billingPeriod).toBeInTheDocument();
|
expect(billingPeriod).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = await screen.findByRole('heading', {
|
const currentBill = screen.getByText('Billing');
|
||||||
name: /current bill total/i,
|
|
||||||
});
|
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar0 = await screen.findAllByText(/\$1278.3/i);
|
const dollar0 = await screen.findByText(/\$1,278.3/i);
|
||||||
expect(dollar0[0]).toBeInTheDocument();
|
expect(dollar0).toBeInTheDocument();
|
||||||
expect(dollar0.length).toBe(2);
|
|
||||||
|
|
||||||
const metricsRow = await screen.findByRole('row', {
|
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();
|
expect(metricsRow).toBeInTheDocument();
|
||||||
|
|
||||||
const logRow = await screen.findByRole('row', {
|
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();
|
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 () => {
|
test('Should render corrent day remaining in billing period', async () => {
|
||||||
|
@ -2,11 +2,24 @@
|
|||||||
import './BillingContainer.styles.scss';
|
import './BillingContainer.styles.scss';
|
||||||
|
|
||||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
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 { ColumnsType } from 'antd/es/table';
|
||||||
import updateCreditCardApi from 'api/billing/checkout';
|
import updateCreditCardApi from 'api/billing/checkout';
|
||||||
import getUsage from 'api/billing/getUsage';
|
import getUsage from 'api/billing/getUsage';
|
||||||
import manageCreditCardApi from 'api/billing/manage';
|
import manageCreditCardApi from 'api/billing/manage';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||||
@ -22,8 +35,11 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
|||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
import { License } from 'types/api/licenses/def';
|
import { License } from 'types/api/licenses/def';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { isCloudUser } from 'utils/app';
|
||||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||||
|
|
||||||
|
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
||||||
|
|
||||||
interface DataType {
|
interface DataType {
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -104,12 +120,11 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
const daysRemainingStr = 'days remaining in your billing period.';
|
const daysRemainingStr = 'days remaining in your billing period.';
|
||||||
const [headerText, setHeaderText] = useState('');
|
const [headerText, setHeaderText] = useState('');
|
||||||
const [billAmount, setBillAmount] = useState(0);
|
const [billAmount, setBillAmount] = useState(0);
|
||||||
const [totalBillAmount, setTotalBillAmount] = useState(0);
|
|
||||||
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
||||||
const [daysRemaining, setDaysRemaining] = useState(0);
|
const [daysRemaining, setDaysRemaining] = useState(0);
|
||||||
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const billCurrency = '$';
|
const [apiResponse, setApiResponse] = useState<any>({});
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
const { trackEvent } = useAnalytics();
|
||||||
|
|
||||||
@ -120,10 +135,12 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
const handleError = useAxiosError();
|
const handleError = useAxiosError();
|
||||||
|
|
||||||
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
const processUsageData = useCallback(
|
const processUsageData = useCallback(
|
||||||
(data: any): void => {
|
(data: any): void => {
|
||||||
const {
|
const {
|
||||||
details: { breakdown = [], total, billTotal },
|
details: { breakdown = [], billTotal },
|
||||||
billingPeriodStart,
|
billingPeriodStart,
|
||||||
billingPeriodEnd,
|
billingPeriodEnd,
|
||||||
} = data?.payload || {};
|
} = data?.payload || {};
|
||||||
@ -141,8 +158,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
formattedUsageData.push({
|
formattedUsageData.push({
|
||||||
key: `${index}${i}`,
|
key: `${index}${i}`,
|
||||||
name: i === 0 ? element?.type : '',
|
name: i === 0 ? element?.type : '',
|
||||||
unit: element?.unit,
|
dataIngested: `${tier.quantity} ${element?.unit}`,
|
||||||
dataIngested: tier.quantity,
|
|
||||||
pricePerUnit: tier.unitPrice,
|
pricePerUnit: tier.unitPrice,
|
||||||
cost: `$ ${tier.tierCost}`,
|
cost: `$ ${tier.tierCost}`,
|
||||||
});
|
});
|
||||||
@ -152,7 +168,6 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(formattedUsageData);
|
setData(formattedUsageData);
|
||||||
setTotalBillAmount(total);
|
|
||||||
|
|
||||||
if (!licensesData?.payload?.onTrial) {
|
if (!licensesData?.payload?.onTrial) {
|
||||||
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
|
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
|
||||||
@ -165,11 +180,13 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
|
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
|
||||||
setBillAmount(billTotal);
|
setBillAmount(billTotal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setApiResponse(data?.payload || {});
|
||||||
},
|
},
|
||||||
[licensesData?.payload?.onTrial],
|
[licensesData?.payload?.onTrial],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isLoading } = useQuery(
|
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
|
||||||
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
|
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
|
||||||
{
|
{
|
||||||
queryFn: () => getUsage(activeLicense?.key || ''),
|
queryFn: () => getUsage(activeLicense?.key || ''),
|
||||||
@ -208,11 +225,6 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
render: (text): JSX.Element => <div>{text}</div>,
|
render: (text): JSX.Element => <div>{text}</div>,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Unit',
|
|
||||||
dataIndex: 'unit',
|
|
||||||
key: 'unit',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Data Ingested',
|
title: 'Data Ingested',
|
||||||
dataIndex: 'dataIngested',
|
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 => (
|
const renderTableSkeleton = (): JSX.Element => (
|
||||||
<Table
|
<Table
|
||||||
dataSource={dummyData}
|
dataSource={dummyData}
|
||||||
@ -336,78 +330,95 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
updateCreditCard,
|
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 (
|
return (
|
||||||
<div className="billing-container">
|
<div className="billing-container">
|
||||||
<Row
|
<Flex vertical style={{ marginBottom: 16 }}>
|
||||||
justify="space-between"
|
<Typography.Text style={{ fontWeight: 500, fontSize: 18 }}>
|
||||||
align="middle"
|
Billing
|
||||||
gutter={[16, 16]}
|
</Typography.Text>
|
||||||
style={{
|
<Typography.Text color={Color.BG_VANILLA_400}>
|
||||||
margin: 0,
|
Manage your billing information, invoices, and monitor costs.
|
||||||
}}
|
</Typography.Text>
|
||||||
>
|
</Flex>
|
||||||
<Col span={20}>
|
|
||||||
<Typography.Title level={4} ellipsis style={{ fontWeight: '300' }}>
|
|
||||||
{headerText}
|
|
||||||
</Typography.Title>
|
|
||||||
|
|
||||||
{licensesData?.payload?.onTrial &&
|
<Card
|
||||||
licensesData?.payload?.trialConvertedToSubscription && (
|
bordered={false}
|
||||||
<Typography.Title
|
style={{ minHeight: 150, marginBottom: 16 }}
|
||||||
level={5}
|
className="page-info"
|
||||||
ellipsis
|
|
||||||
style={{ fontWeight: '300', color: '#49aa19' }}
|
|
||||||
>
|
>
|
||||||
We have received your card details, your billing will only start after
|
<Flex justify="space-between" align="center">
|
||||||
the end of your free trial period.
|
<Flex vertical>
|
||||||
|
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
|
||||||
|
{isCloudUserVal ? 'Enterprise Cloud' : 'Enterprise'}{' '}
|
||||||
|
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
)}
|
{!isLoading && !isFetchingBillingData ? (
|
||||||
</Col>
|
<Typography.Text style={{ fontSize: 12, color: Color.BG_VANILLA_400 }}>
|
||||||
|
{daysRemaining} {daysRemainingStr}
|
||||||
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
|
</Flex>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="middle"
|
size="middle"
|
||||||
loading={isLoadingBilling || isLoadingManageBilling}
|
loading={isLoadingBilling || isLoadingManageBilling}
|
||||||
|
disabled={isLoading}
|
||||||
onClick={handleBilling}
|
onClick={handleBilling}
|
||||||
>
|
>
|
||||||
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
|
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
|
||||||
? 'Upgrade Plan'
|
? 'Upgrade Plan'
|
||||||
: 'Manage Billing'}
|
: 'Manage Billing'}
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Flex>
|
||||||
</Row>
|
|
||||||
|
|
||||||
<div className="billing-summary">
|
{licensesData?.payload?.onTrial &&
|
||||||
<Typography.Title level={4} style={{ margin: '16px 0' }}>
|
licensesData?.payload?.trialConvertedToSubscription && (
|
||||||
Current bill total
|
<Typography.Text
|
||||||
</Typography.Title>
|
ellipsis
|
||||||
|
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
|
||||||
<Typography.Title
|
|
||||||
level={3}
|
|
||||||
style={{ margin: '16px 0', display: 'flex', alignItems: 'center' }}
|
|
||||||
>
|
>
|
||||||
{billCurrency}
|
We have received your card details, your billing will only start after
|
||||||
{billAmount}
|
the end of your free trial period.
|
||||||
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
|
</Typography.Text>
|
||||||
</Typography.Title>
|
)}
|
||||||
|
|
||||||
<Typography.Paragraph style={{ margin: '16px 0' }}>
|
{!isLoading && !isFetchingBillingData ? (
|
||||||
{daysRemaining} {daysRemainingStr}
|
<Alert
|
||||||
</Typography.Paragraph>
|
message={headerText}
|
||||||
</div>
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginTop: 12 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Skeleton.Input active style={{ height: 20, marginTop: 20 }} />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<BillingUsageGraphCallback />
|
||||||
|
|
||||||
<div className="billing-details">
|
<div className="billing-details">
|
||||||
{!isLoading && (
|
{!isLoading && !isFetchingBillingData && (
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
summary={renderSummary}
|
bordered={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && renderTableSkeleton()}
|
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
|
{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;
|
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 => {
|
export const ValidatePagerChannel = (p: PagerChannel): string => {
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return 'Received unexpected input for this channel, please contact your administrator ';
|
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> = {
|
export const PagerInitialConfig: Partial<PagerChannel> = {
|
||||||
description: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
|
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:
|
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 }}',
|
'{{ 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 { Form } from 'antd';
|
||||||
|
import createEmail from 'api/channels/createEmail';
|
||||||
import createMsTeamsApi from 'api/channels/createMsTeams';
|
import createMsTeamsApi from 'api/channels/createMsTeams';
|
||||||
import createOpsgenie from 'api/channels/createOpsgenie';
|
import createOpsgenie from 'api/channels/createOpsgenie';
|
||||||
import createPagerApi from 'api/channels/createPager';
|
import createPagerApi from 'api/channels/createPager';
|
||||||
import createSlackApi from 'api/channels/createSlack';
|
import createSlackApi from 'api/channels/createSlack';
|
||||||
import createWebhookApi from 'api/channels/createWebhook';
|
import createWebhookApi from 'api/channels/createWebhook';
|
||||||
|
import testEmail from 'api/channels/testEmail';
|
||||||
import testMsTeamsApi from 'api/channels/testMsTeams';
|
import testMsTeamsApi from 'api/channels/testMsTeams';
|
||||||
import testOpsGenie from 'api/channels/testOpsgenie';
|
import testOpsGenie from 'api/channels/testOpsgenie';
|
||||||
import testPagerApi from 'api/channels/testPager';
|
import testPagerApi from 'api/channels/testPager';
|
||||||
@ -18,6 +20,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ChannelType,
|
ChannelType,
|
||||||
|
EmailChannel,
|
||||||
MsTeamsChannel,
|
MsTeamsChannel,
|
||||||
OpsgenieChannel,
|
OpsgenieChannel,
|
||||||
PagerChannel,
|
PagerChannel,
|
||||||
@ -25,7 +28,11 @@ import {
|
|||||||
ValidatePagerChannel,
|
ValidatePagerChannel,
|
||||||
WebhookChannel,
|
WebhookChannel,
|
||||||
} from './config';
|
} from './config';
|
||||||
import { OpsgenieInitialConfig, PagerInitialConfig } from './defaults';
|
import {
|
||||||
|
EmailInitialConfig,
|
||||||
|
OpsgenieInitialConfig,
|
||||||
|
PagerInitialConfig,
|
||||||
|
} from './defaults';
|
||||||
import { isChannelType } from './utils';
|
import { isChannelType } from './utils';
|
||||||
|
|
||||||
function CreateAlertChannels({
|
function CreateAlertChannels({
|
||||||
@ -42,7 +49,8 @@ function CreateAlertChannels({
|
|||||||
WebhookChannel &
|
WebhookChannel &
|
||||||
PagerChannel &
|
PagerChannel &
|
||||||
MsTeamsChannel &
|
MsTeamsChannel &
|
||||||
OpsgenieChannel
|
OpsgenieChannel &
|
||||||
|
EmailChannel
|
||||||
>
|
>
|
||||||
>({
|
>({
|
||||||
text: `{{ range .Alerts -}}
|
text: `{{ range .Alerts -}}
|
||||||
@ -94,6 +102,14 @@ function CreateAlertChannels({
|
|||||||
...OpsgenieInitialConfig,
|
...OpsgenieInitialConfig,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset config to email defaults
|
||||||
|
if (value === ChannelType.Email && currentType !== value) {
|
||||||
|
setSelectedConfig((selectedConfig) => ({
|
||||||
|
...selectedConfig,
|
||||||
|
...EmailInitialConfig,
|
||||||
|
}));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[type, selectedConfig],
|
[type, selectedConfig],
|
||||||
);
|
);
|
||||||
@ -293,6 +309,43 @@ function CreateAlertChannels({
|
|||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}, [prepareOpsgenieRequest, t, notifications]);
|
}, [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(
|
const prepareMsTeamsRequest = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
webhook_url: selectedConfig?.webhook_url || '',
|
webhook_url: selectedConfig?.webhook_url || '',
|
||||||
@ -339,6 +392,7 @@ function CreateAlertChannels({
|
|||||||
[ChannelType.Pagerduty]: onPagerHandler,
|
[ChannelType.Pagerduty]: onPagerHandler,
|
||||||
[ChannelType.Opsgenie]: onOpsgenieHandler,
|
[ChannelType.Opsgenie]: onOpsgenieHandler,
|
||||||
[ChannelType.MsTeams]: onMsTeamsHandler,
|
[ChannelType.MsTeams]: onMsTeamsHandler,
|
||||||
|
[ChannelType.Email]: onEmailHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isChannelType(value)) {
|
if (isChannelType(value)) {
|
||||||
@ -360,6 +414,7 @@ function CreateAlertChannels({
|
|||||||
onPagerHandler,
|
onPagerHandler,
|
||||||
onOpsgenieHandler,
|
onOpsgenieHandler,
|
||||||
onMsTeamsHandler,
|
onMsTeamsHandler,
|
||||||
|
onEmailHandler,
|
||||||
notifications,
|
notifications,
|
||||||
t,
|
t,
|
||||||
],
|
],
|
||||||
@ -392,6 +447,10 @@ function CreateAlertChannels({
|
|||||||
request = prepareOpsgenieRequest();
|
request = prepareOpsgenieRequest();
|
||||||
response = await testOpsGenie(request);
|
response = await testOpsGenie(request);
|
||||||
break;
|
break;
|
||||||
|
case ChannelType.Email:
|
||||||
|
request = prepareEmailRequest();
|
||||||
|
response = await testEmail(request);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
@ -427,6 +486,7 @@ function CreateAlertChannels({
|
|||||||
prepareOpsgenieRequest,
|
prepareOpsgenieRequest,
|
||||||
prepareSlackRequest,
|
prepareSlackRequest,
|
||||||
prepareMsTeamsRequest,
|
prepareMsTeamsRequest,
|
||||||
|
prepareEmailRequest,
|
||||||
notifications,
|
notifications,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -455,6 +515,7 @@ function CreateAlertChannels({
|
|||||||
...selectedConfig,
|
...selectedConfig,
|
||||||
...PagerInitialConfig,
|
...PagerInitialConfig,
|
||||||
...OpsgenieInitialConfig,
|
...OpsgenieInitialConfig,
|
||||||
|
...EmailInitialConfig,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import {
|
import {
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
initialQueryPromQLData,
|
initialQueryPromQLData,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import {
|
import {
|
||||||
AlertDef,
|
AlertDef,
|
||||||
@ -25,6 +25,7 @@ const defaultAnnotations = {
|
|||||||
|
|
||||||
export const alertDefaults: AlertDef = {
|
export const alertDefaults: AlertDef = {
|
||||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||||
|
version: ENTITY_VERSION_V4,
|
||||||
condition: {
|
condition: {
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
builderQueries: {
|
builderQueries: {
|
||||||
@ -78,7 +79,6 @@ export const logAlertDefaults: AlertDef = {
|
|||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
details: `${window.location.protocol}//${window.location.host}${ROUTES.LOGS_EXPLORER}`,
|
|
||||||
},
|
},
|
||||||
annotations: defaultAnnotations,
|
annotations: defaultAnnotations,
|
||||||
evalWindow: defaultEvalWindow,
|
evalWindow: defaultEvalWindow,
|
||||||
@ -109,7 +109,6 @@ export const traceAlertDefaults: AlertDef = {
|
|||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
details: `${window.location.protocol}//${window.location.host}/traces`,
|
|
||||||
},
|
},
|
||||||
annotations: defaultAnnotations,
|
annotations: defaultAnnotations,
|
||||||
evalWindow: defaultEvalWindow,
|
evalWindow: defaultEvalWindow,
|
||||||
@ -140,7 +139,6 @@ export const exceptionAlertDefaults: AlertDef = {
|
|||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
details: `${window.location.protocol}//${window.location.host}/exceptions`,
|
|
||||||
},
|
},
|
||||||
annotations: defaultAnnotations,
|
annotations: defaultAnnotations,
|
||||||
evalWindow: defaultEvalWindow,
|
evalWindow: defaultEvalWindow,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { Form, Row } from 'antd';
|
import { Form, Row } from 'antd';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
import FormAlertRules from 'container/FormAlertRules';
|
||||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import { AlertDef } from 'types/api/alerts/def';
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
|
|
||||||
@ -20,6 +22,10 @@ function CreateRules(): JSX.Element {
|
|||||||
AlertTypes.METRICS_BASED_ALERT,
|
AlertTypes.METRICS_BASED_ALERT,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const version = queryParams.get('version');
|
||||||
|
|
||||||
const compositeQuery = useGetCompositeQueryParam();
|
const compositeQuery = useGetCompositeQueryParam();
|
||||||
|
|
||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
@ -37,7 +43,10 @@ function CreateRules(): JSX.Element {
|
|||||||
setInitValues(exceptionAlertDefaults);
|
setInitValues(exceptionAlertDefaults);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
setInitValues(alertDefaults);
|
setInitValues({
|
||||||
|
...alertDefaults,
|
||||||
|
version: version || ENTITY_VERSION_V4,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,6 +61,7 @@ function CreateRules(): JSX.Element {
|
|||||||
if (alertType) {
|
if (alertType) {
|
||||||
onSelectType(alertType);
|
onSelectType(alertType);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [compositeQuery]);
|
}, [compositeQuery]);
|
||||||
|
|
||||||
if (!initValues) {
|
if (!initValues) {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
|
import editEmail from 'api/channels/editEmail';
|
||||||
import editMsTeamsApi from 'api/channels/editMsTeams';
|
import editMsTeamsApi from 'api/channels/editMsTeams';
|
||||||
import editOpsgenie from 'api/channels/editOpsgenie';
|
import editOpsgenie from 'api/channels/editOpsgenie';
|
||||||
import editPagerApi from 'api/channels/editPager';
|
import editPagerApi from 'api/channels/editPager';
|
||||||
import editSlackApi from 'api/channels/editSlack';
|
import editSlackApi from 'api/channels/editSlack';
|
||||||
import editWebhookApi from 'api/channels/editWebhook';
|
import editWebhookApi from 'api/channels/editWebhook';
|
||||||
|
import testEmail from 'api/channels/testEmail';
|
||||||
import testMsTeamsApi from 'api/channels/testMsTeams';
|
import testMsTeamsApi from 'api/channels/testMsTeams';
|
||||||
import testOpsgenie from 'api/channels/testOpsgenie';
|
import testOpsgenie from 'api/channels/testOpsgenie';
|
||||||
import testPagerApi from 'api/channels/testPager';
|
import testPagerApi from 'api/channels/testPager';
|
||||||
@ -12,6 +14,7 @@ import testWebhookApi from 'api/channels/testWebhook';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import {
|
import {
|
||||||
ChannelType,
|
ChannelType,
|
||||||
|
EmailChannel,
|
||||||
MsTeamsChannel,
|
MsTeamsChannel,
|
||||||
OpsgenieChannel,
|
OpsgenieChannel,
|
||||||
PagerChannel,
|
PagerChannel,
|
||||||
@ -39,7 +42,8 @@ function EditAlertChannels({
|
|||||||
WebhookChannel &
|
WebhookChannel &
|
||||||
PagerChannel &
|
PagerChannel &
|
||||||
MsTeamsChannel &
|
MsTeamsChannel &
|
||||||
OpsgenieChannel
|
OpsgenieChannel &
|
||||||
|
EmailChannel
|
||||||
>
|
>
|
||||||
>({
|
>({
|
||||||
...initialValue,
|
...initialValue,
|
||||||
@ -156,6 +160,36 @@ function EditAlertChannels({
|
|||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
}, [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(
|
const preparePagerRequest = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
name: selectedConfig.name || '',
|
name: selectedConfig.name || '',
|
||||||
@ -300,6 +334,8 @@ function EditAlertChannels({
|
|||||||
onMsTeamsEditHandler();
|
onMsTeamsEditHandler();
|
||||||
} else if (value === ChannelType.Opsgenie) {
|
} else if (value === ChannelType.Opsgenie) {
|
||||||
onOpsgenieEditHandler();
|
onOpsgenieEditHandler();
|
||||||
|
} else if (value === ChannelType.Email) {
|
||||||
|
onEmailEditHandler();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@ -308,6 +344,7 @@ function EditAlertChannels({
|
|||||||
onPagerEditHandler,
|
onPagerEditHandler,
|
||||||
onMsTeamsEditHandler,
|
onMsTeamsEditHandler,
|
||||||
onOpsgenieEditHandler,
|
onOpsgenieEditHandler,
|
||||||
|
onEmailEditHandler,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -338,6 +375,10 @@ function EditAlertChannels({
|
|||||||
request = prepareOpsgenieRequest();
|
request = prepareOpsgenieRequest();
|
||||||
if (request) response = await testOpsgenie(request);
|
if (request) response = await testOpsgenie(request);
|
||||||
break;
|
break;
|
||||||
|
case ChannelType.Email:
|
||||||
|
request = prepareEmailRequest();
|
||||||
|
if (request) response = await testEmail(request);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Error',
|
message: 'Error',
|
||||||
@ -373,6 +414,7 @@ function EditAlertChannels({
|
|||||||
prepareSlackRequest,
|
prepareSlackRequest,
|
||||||
prepareMsTeamsRequest,
|
prepareMsTeamsRequest,
|
||||||
prepareOpsgenieRequest,
|
prepareOpsgenieRequest,
|
||||||
|
prepareEmailRequest,
|
||||||
notifications,
|
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 {
|
.explorer-update {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 16px;
|
bottom: 24px;
|
||||||
left: calc(50% - 225px);
|
left: calc(50% - 225px);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -23,6 +26,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-divider {
|
.ant-divider {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@ -40,7 +47,7 @@
|
|||||||
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
|
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 16px;
|
bottom: 24px;
|
||||||
left: calc(50% + 240px);
|
left: calc(50% + 240px);
|
||||||
transform: translate(calc(-50% - 120px), 0);
|
transform: translate(calc(-50% - 120px), 0);
|
||||||
transition: left 0.2s linear;
|
transition: left 0.2s linear;
|
||||||
@ -55,6 +62,10 @@
|
|||||||
|
|
||||||
.view-options,
|
.view-options,
|
||||||
.actions {
|
.actions {
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -102,6 +113,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import './ExplorerOptions.styles.scss';
|
import './ExplorerOptions.styles.scss';
|
||||||
|
|
||||||
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -13,6 +15,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import cx from 'classnames';
|
||||||
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
@ -30,12 +33,25 @@ import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||||
import { Check, ConciergeBell, Disc3, Plus, X, XCircle } from 'lucide-react';
|
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 { useHistory } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
|
import ExplorerOptionsDroppableArea from './ExplorerOptionsDroppableArea';
|
||||||
import {
|
import {
|
||||||
DATASOURCE_VS_ROUTES,
|
DATASOURCE_VS_ROUTES,
|
||||||
generateRGBAFromHex,
|
generateRGBAFromHex,
|
||||||
@ -43,12 +59,17 @@ import {
|
|||||||
saveNewViewHandler,
|
saveNewViewHandler,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
|
const allowedRoles = [USER_ROLES.ADMIN, USER_ROLES.AUTHOR, USER_ROLES.EDITOR];
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function ExplorerOptions({
|
function ExplorerOptions({
|
||||||
disabled,
|
disabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
onExport,
|
onExport,
|
||||||
query,
|
query,
|
||||||
sourcepage,
|
sourcepage,
|
||||||
|
isExplorerOptionHidden = false,
|
||||||
|
setIsExplorerOptionHidden,
|
||||||
}: ExplorerOptionsProps): JSX.Element {
|
}: ExplorerOptionsProps): JSX.Element {
|
||||||
const [isExport, setIsExport] = useState<boolean>(false);
|
const [isExport, setIsExport] = useState<boolean>(false);
|
||||||
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
|
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
|
||||||
@ -58,6 +79,7 @@ function ExplorerOptions({
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const ref = useRef<RefSelectProps>(null);
|
const ref = useRef<RefSelectProps>(null);
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const [isDragEnabled, setIsDragEnabled] = useState(false);
|
||||||
|
|
||||||
const onModalToggle = useCallback((value: boolean) => {
|
const onModalToggle = useCallback((value: boolean) => {
|
||||||
setIsExport(value);
|
setIsExport(value);
|
||||||
@ -71,6 +93,8 @@ function ExplorerOptions({
|
|||||||
setIsSaveModalOpen(false);
|
setIsSaveModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCallback(() => {
|
const onCreateAlertsHandler = useCallback(() => {
|
||||||
history.push(
|
history.push(
|
||||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||||
@ -247,10 +271,37 @@ function ExplorerOptions({
|
|||||||
[isDarkMode],
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isQueryUpdated && (
|
{isQueryUpdated && !isExplorerOptionHidden && !isDragging && (
|
||||||
<div className="explorer-update">
|
<div
|
||||||
|
className={cx(
|
||||||
|
isEditDeleteSupported ? '' : 'hide-update',
|
||||||
|
'explorer-update',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Tooltip title="Clear this view" placement="top">
|
<Tooltip title="Clear this view" placement="top">
|
||||||
<Button
|
<Button
|
||||||
className="action-icon"
|
className="action-icon"
|
||||||
@ -258,10 +309,13 @@ function ExplorerOptions({
|
|||||||
icon={<X size={14} />}
|
icon={<X size={14} />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Divider type="vertical" />
|
<Divider
|
||||||
|
type="vertical"
|
||||||
|
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||||
|
/>
|
||||||
<Tooltip title="Update this view" placement="top">
|
<Tooltip title="Update this view" placement="top">
|
||||||
<Button
|
<Button
|
||||||
className="action-icon"
|
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
|
||||||
disabled={isViewUpdating}
|
disabled={isViewUpdating}
|
||||||
onClick={onUpdateQueryHandler}
|
onClick={onUpdateQueryHandler}
|
||||||
icon={<Disc3 size={14} />}
|
icon={<Disc3 size={14} />}
|
||||||
@ -269,6 +323,7 @@ function ExplorerOptions({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!isExplorerOptionHidden && (
|
||||||
<div
|
<div
|
||||||
className="explorer-options"
|
className="explorer-options"
|
||||||
style={{
|
style={{
|
||||||
@ -276,7 +331,11 @@ function ExplorerOptions({
|
|||||||
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
||||||
: 'transparent',
|
: 'transparent',
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(20px)',
|
||||||
|
...style,
|
||||||
}}
|
}}
|
||||||
|
ref={setNodeRef}
|
||||||
|
{...listeners}
|
||||||
|
{...attributes}
|
||||||
>
|
>
|
||||||
<div className="view-options">
|
<div className="view-options">
|
||||||
<Select<string, { key: string; value: string }>
|
<Select<string, { key: string; value: string }>
|
||||||
@ -293,6 +352,9 @@ function ExplorerOptions({
|
|||||||
allowClear={{
|
allowClear={{
|
||||||
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
|
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
|
||||||
}}
|
}}
|
||||||
|
onDropdownVisibleChange={(open): void => {
|
||||||
|
setIsDragEnabled(open);
|
||||||
|
}}
|
||||||
onClear={handleClearSelect}
|
onClear={handleClearSelect}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
@ -323,15 +385,16 @@ function ExplorerOptions({
|
|||||||
<Button
|
<Button
|
||||||
shape="round"
|
shape="round"
|
||||||
onClick={handleSaveViewModalToggle}
|
onClick={handleSaveViewModalToggle}
|
||||||
|
className={isEditDeleteSupported ? '' : 'hidden'}
|
||||||
disabled={viewsIsLoading || isRefetching}
|
disabled={viewsIsLoading || isRefetching}
|
||||||
>
|
>
|
||||||
<Disc3 size={16} /> Save this view
|
<Disc3 size={16} /> Save this view
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
||||||
|
|
||||||
<div className="actions">
|
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||||
<Tooltip title="Create Alerts">
|
<Tooltip title="Create Alerts">
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -349,6 +412,16 @@ function ExplorerOptions({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ExplorerOptionsDroppableArea
|
||||||
|
isExplorerOptionHidden={isExplorerOptionHidden}
|
||||||
|
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
||||||
|
sourcepage={sourcepage}
|
||||||
|
isQueryUpdated={isQueryUpdated}
|
||||||
|
handleClearSelect={handleClearSelect}
|
||||||
|
onUpdateQueryHandler={onUpdateQueryHandler}
|
||||||
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
className="save-view-modal"
|
className="save-view-modal"
|
||||||
@ -406,8 +479,14 @@ export interface ExplorerOptionsProps {
|
|||||||
query: Query | null;
|
query: Query | null;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
sourcepage: DataSource;
|
sourcepage: DataSource;
|
||||||
|
isExplorerOptionHidden?: boolean;
|
||||||
|
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
ExplorerOptions.defaultProps = { isLoading: false };
|
ExplorerOptions.defaultProps = {
|
||||||
|
isLoading: false,
|
||||||
|
isExplorerOptionHidden: false,
|
||||||
|
setIsExplorerOptionHidden: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export default ExplorerOptions;
|
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 { Color } from '@signozhq/design-tokens';
|
||||||
import { showErrorNotification } from 'components/ExplorerCard/utils';
|
import { showErrorNotification } from 'components/ExplorerCard/utils';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
@ -67,3 +68,54 @@ export const generateRGBAFromHex = (hex: string, opacity: number): string =>
|
|||||||
hex.slice(3, 5),
|
hex.slice(3, 5),
|
||||||
16,
|
16,
|
||||||
)}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`;
|
)}, ${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 ROUTES from 'constants/routes';
|
||||||
import {
|
import {
|
||||||
ChannelType,
|
ChannelType,
|
||||||
|
EmailChannel,
|
||||||
OpsgenieChannel,
|
OpsgenieChannel,
|
||||||
PagerChannel,
|
PagerChannel,
|
||||||
SlackChannel,
|
SlackChannel,
|
||||||
@ -16,6 +17,7 @@ import history from 'lib/history';
|
|||||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import EmailSettings from './Settings/Email';
|
||||||
import MsTeamsSettings from './Settings/MsTeams';
|
import MsTeamsSettings from './Settings/MsTeams';
|
||||||
import OpsgenieSettings from './Settings/Opsgenie';
|
import OpsgenieSettings from './Settings/Opsgenie';
|
||||||
import PagerSettings from './Settings/Pager';
|
import PagerSettings from './Settings/Pager';
|
||||||
@ -69,6 +71,8 @@ function FormAlertChannels({
|
|||||||
return <MsTeamsSettings setSelectedConfig={setSelectedConfig} />;
|
return <MsTeamsSettings setSelectedConfig={setSelectedConfig} />;
|
||||||
case ChannelType.Opsgenie:
|
case ChannelType.Opsgenie:
|
||||||
return <OpsgenieSettings setSelectedConfig={setSelectedConfig} />;
|
return <OpsgenieSettings setSelectedConfig={setSelectedConfig} />;
|
||||||
|
case ChannelType.Email:
|
||||||
|
return <EmailSettings setSelectedConfig={setSelectedConfig} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -105,6 +109,9 @@ function FormAlertChannels({
|
|||||||
<Select.Option value="opsgenie" key="opsgenie">
|
<Select.Option value="opsgenie" key="opsgenie">
|
||||||
Opsgenie
|
Opsgenie
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
|
<Select.Option value="email" key="email">
|
||||||
|
Email
|
||||||
|
</Select.Option>
|
||||||
{!isOssFeature?.active && (
|
{!isOssFeature?.active && (
|
||||||
<Select.Option value="msteams" key="msteams">
|
<Select.Option value="msteams" key="msteams">
|
||||||
<div>
|
<div>
|
||||||
@ -151,7 +158,13 @@ interface FormAlertChannelsProps {
|
|||||||
type: ChannelType;
|
type: ChannelType;
|
||||||
setSelectedConfig: Dispatch<
|
setSelectedConfig: Dispatch<
|
||||||
SetStateAction<
|
SetStateAction<
|
||||||
Partial<SlackChannel & WebhookChannel & PagerChannel & OpsgenieChannel>
|
Partial<
|
||||||
|
SlackChannel &
|
||||||
|
WebhookChannel &
|
||||||
|
PagerChannel &
|
||||||
|
OpsgenieChannel &
|
||||||
|
EmailChannel
|
||||||
|
>
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
onTypeChangeHandler: (value: ChannelType) => void;
|
onTypeChangeHandler: (value: ChannelType) => void;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
@ -39,6 +40,7 @@ export interface ChartPreviewProps {
|
|||||||
yAxisUnit: string;
|
yAxisUnit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function ChartPreview({
|
function ChartPreview({
|
||||||
name,
|
name,
|
||||||
query,
|
query,
|
||||||
@ -94,6 +96,7 @@ function ChartPreview({
|
|||||||
allowSelectedIntervalForStepGen,
|
allowSelectedIntervalForStepGen,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'chartPreview',
|
'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 { Button, Tabs, Tooltip } from 'antd';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { Atom, Play, Terminal } from 'lucide-react';
|
import { Atom, Play, Terminal } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
@ -22,6 +26,7 @@ function QuerySection({
|
|||||||
setQueryCategory,
|
setQueryCategory,
|
||||||
alertType,
|
alertType,
|
||||||
runQuery,
|
runQuery,
|
||||||
|
alertDef,
|
||||||
panelType,
|
panelType,
|
||||||
}: QuerySectionProps): JSX.Element {
|
}: QuerySectionProps): JSX.Element {
|
||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
@ -50,6 +55,11 @@ function QuerySection({
|
|||||||
queryVariant: 'static',
|
queryVariant: 'static',
|
||||||
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
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 => {
|
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
|
||||||
switch (typ) {
|
switch (typ) {
|
||||||
case AlertTypes.TRACES_BASED_ALERT:
|
case AlertTypes.TRACES_BASED_ALERT:
|
||||||
@ -197,6 +218,7 @@ interface QuerySectionProps {
|
|||||||
setQueryCategory: (n: EQueryType) => void;
|
setQueryCategory: (n: EQueryType) => void;
|
||||||
alertType: AlertTypes;
|
alertType: AlertTypes;
|
||||||
runQuery: VoidFunction;
|
runQuery: VoidFunction;
|
||||||
|
alertDef: AlertDef;
|
||||||
panelType: PANEL_TYPES;
|
panelType: PANEL_TYPES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Checkbox,
|
||||||
Form,
|
Form,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
InputNumberProps,
|
InputNumberProps,
|
||||||
@ -213,6 +214,7 @@ function RuleOptions({
|
|||||||
? renderPromRuleOptions()
|
? renderPromRuleOptions()
|
||||||
: renderThresholdRuleOpts()}
|
: renderThresholdRuleOpts()}
|
||||||
|
|
||||||
|
<Space direction="vertical" size="large">
|
||||||
<Space direction="horizontal" align="center">
|
<Space direction="horizontal" align="center">
|
||||||
<Form.Item noStyle name={['condition', 'target']}>
|
<Form.Item noStyle name={['condition', 'target']}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
@ -236,6 +238,43 @@ function RuleOptions({
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</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>
|
</FormContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import './FormAlertRules.styles.scss';
|
||||||
|
|
||||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Col,
|
Col,
|
||||||
@ -304,7 +306,7 @@ function FormAlertRules({
|
|||||||
panelType,
|
panelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isAlertAvialable = useIsFeatureDisabled(
|
const isAlertAvailable = useIsFeatureDisabled(
|
||||||
FeatureKeys.QUERY_BUILDER_ALERTS,
|
FeatureKeys.QUERY_BUILDER_ALERTS,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -373,6 +375,7 @@ function FormAlertRules({
|
|||||||
centered: true,
|
centered: true,
|
||||||
content,
|
content,
|
||||||
onOk: saveRule,
|
onOk: saveRule,
|
||||||
|
className: 'create-alert-modal',
|
||||||
});
|
});
|
||||||
}, [t, saveRule, currentQuery]);
|
}, [t, saveRule, currentQuery]);
|
||||||
|
|
||||||
@ -458,8 +461,8 @@ function FormAlertRules({
|
|||||||
|
|
||||||
const isAlertNameMissing = !formInstance.getFieldValue('alert');
|
const isAlertNameMissing = !formInstance.getFieldValue('alert');
|
||||||
|
|
||||||
const isAlertAvialableToSave =
|
const isAlertAvailableToSave =
|
||||||
isAlertAvialable &&
|
isAlertAvailable &&
|
||||||
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||||
alertType !== AlertTypes.METRICS_BASED_ALERT;
|
alertType !== AlertTypes.METRICS_BASED_ALERT;
|
||||||
|
|
||||||
@ -509,6 +512,7 @@ function FormAlertRules({
|
|||||||
setQueryCategory={onQueryCategoryChange}
|
setQueryCategory={onQueryCategoryChange}
|
||||||
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
||||||
runQuery={handleRunQuery}
|
runQuery={handleRunQuery}
|
||||||
|
alertDef={alertDef}
|
||||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -521,7 +525,7 @@ function FormAlertRules({
|
|||||||
|
|
||||||
{renderBasicInfo()}
|
{renderBasicInfo()}
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<Tooltip title={isAlertAvialableToSave ? MESSAGE.ALERT : ''}>
|
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
loading={loading || false}
|
loading={loading || false}
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -529,7 +533,7 @@ function FormAlertRules({
|
|||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
disabled={
|
disabled={
|
||||||
isAlertNameMissing ||
|
isAlertNameMissing ||
|
||||||
isAlertAvialableToSave ||
|
isAlertAvailableToSave ||
|
||||||
!isChannelConfigurationValid
|
!isChannelConfigurationValid
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -5,7 +5,7 @@ import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
|||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
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 { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||||
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
||||||
@ -29,6 +29,10 @@ function GraphManager({
|
|||||||
getDefaultTableDataSet(options, data),
|
getDefaultTableDataSet(options, data),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTableDataSet(getDefaultTableDataSet(options, data));
|
||||||
|
}, [data, options]);
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const { isDashboardLocked } = useDashboard();
|
const { isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import cx from 'classnames';
|
|||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
import {
|
import {
|
||||||
@ -20,7 +21,7 @@ import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariab
|
|||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -28,7 +29,7 @@ import uPlot from 'uplot';
|
|||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
import { getGraphVisibilityStateOnDataChange } from '../utils';
|
import { getLocalStorageGraphVisibilityState } from '../utils';
|
||||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||||
import GraphManager from './GraphManager';
|
import GraphManager from './GraphManager';
|
||||||
import { GraphContainer, TimeContainer } from './styles';
|
import { GraphContainer, TimeContainer } from './styles';
|
||||||
@ -39,14 +40,13 @@ function FullView({
|
|||||||
fullViewOptions = true,
|
fullViewOptions = true,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
name,
|
name,
|
||||||
|
version,
|
||||||
originalName,
|
originalName,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
options,
|
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
isDependedDataLoaded = false,
|
isDependedDataLoaded = false,
|
||||||
onToggleModelHandler,
|
onToggleModelHandler,
|
||||||
parentChartRef,
|
parentChartRef,
|
||||||
parentGraphVisibilityState,
|
|
||||||
}: FullViewProps): JSX.Element {
|
}: FullViewProps): JSX.Element {
|
||||||
const { selectedTime: globalSelectedTime } = useSelector<
|
const { selectedTime: globalSelectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@ -59,20 +59,6 @@ function FullView({
|
|||||||
|
|
||||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
|
|
||||||
() =>
|
|
||||||
getGraphVisibilityStateOnDataChange({
|
|
||||||
options,
|
|
||||||
isExpandedName: false,
|
|
||||||
name: originalName,
|
|
||||||
}),
|
|
||||||
[options, originalName],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState(
|
|
||||||
localStoredVisibilityStates,
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSelectedTime = useCallback(
|
const getSelectedTime = useCallback(
|
||||||
() =>
|
() =>
|
||||||
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
|
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
|
||||||
@ -91,17 +77,35 @@ function FullView({
|
|||||||
const response = useGetQueryRange(
|
const response = useGetQueryRange(
|
||||||
{
|
{
|
||||||
selectedTime: selectedTime.enum,
|
selectedTime: selectedTime.enum,
|
||||||
graphType: widget.panelTypes,
|
graphType:
|
||||||
|
widget.panelTypes === PANEL_TYPES.BAR
|
||||||
|
? PANEL_TYPES.TIME_SERIES
|
||||||
|
: widget.panelTypes,
|
||||||
query: updatedQuery,
|
query: updatedQuery,
|
||||||
globalSelectedInterval: globalSelectedTime,
|
globalSelectedInterval: globalSelectedTime,
|
||||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||||
},
|
},
|
||||||
|
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
|
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
|
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({
|
const canModifyChart = useChartMutable({
|
||||||
panelType: widget.panelTypes,
|
panelType: widget.panelTypes,
|
||||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||||
@ -144,6 +148,7 @@ function FullView({
|
|||||||
: 300;
|
: 300;
|
||||||
|
|
||||||
const newChartOptions = getUPlotChartOptions({
|
const newChartOptions = getUPlotChartOptions({
|
||||||
|
id: originalName,
|
||||||
yAxisUnit: yAxisUnit || '',
|
yAxisUnit: yAxisUnit || '',
|
||||||
apiResponse: response.data?.payload,
|
apiResponse: response.data?.payload,
|
||||||
dimensions: {
|
dimensions: {
|
||||||
@ -171,8 +176,7 @@ function FullView({
|
|||||||
graphsVisibilityStates?.forEach((e, i) => {
|
graphsVisibilityStates?.forEach((e, i) => {
|
||||||
fullViewChartRef?.current?.toggleGraph(i, e);
|
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||||
});
|
});
|
||||||
parentGraphVisibilityState(graphsVisibilityStates);
|
}, [graphsVisibilityStates]);
|
||||||
}, [graphsVisibilityStates, parentGraphVisibilityState]);
|
|
||||||
|
|
||||||
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
|
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
|
||||||
|
|
||||||
|
@ -50,14 +50,13 @@ export interface FullViewProps {
|
|||||||
fullViewOptions?: boolean;
|
fullViewOptions?: boolean;
|
||||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
name: string;
|
name: string;
|
||||||
|
version?: string;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
options: uPlot.Options;
|
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
isDependedDataLoaded?: boolean;
|
isDependedDataLoaded?: boolean;
|
||||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||||
parentChartRef: GraphManagerProps['lineChartRef'];
|
parentChartRef: GraphManagerProps['lineChartRef'];
|
||||||
parentGraphVisibilityState: Dispatch<SetStateAction<boolean[]>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphManagerProps extends UplotProps {
|
export interface GraphManagerProps extends UplotProps {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import getLabelName from 'lib/getLabelName';
|
||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -55,6 +57,20 @@ export const getAbbreviatedLabel = (label: string): string => {
|
|||||||
return newLabel;
|
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[] =>
|
export const showAllDataSet = (options: uPlot.Options): LegendEntryProps[] =>
|
||||||
options.series
|
options.series
|
||||||
.map(
|
.map(
|
||||||
|
@ -32,13 +32,14 @@ import WidgetHeader from '../WidgetHeader';
|
|||||||
import FullView from './FullView';
|
import FullView from './FullView';
|
||||||
import { Modal } from './styles';
|
import { Modal } from './styles';
|
||||||
import { WidgetGraphComponentProps } from './types';
|
import { WidgetGraphComponentProps } from './types';
|
||||||
import { getGraphVisibilityStateOnDataChange } from './utils';
|
import { getLocalStorageGraphVisibilityState } from './utils';
|
||||||
|
|
||||||
function WidgetGraphComponent({
|
function WidgetGraphComponent({
|
||||||
widget,
|
widget,
|
||||||
queryResponse,
|
queryResponse,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
name,
|
name,
|
||||||
|
version,
|
||||||
threshold,
|
threshold,
|
||||||
headerMenuList,
|
headerMenuList,
|
||||||
isWarning,
|
isWarning,
|
||||||
@ -62,20 +63,6 @@ function WidgetGraphComponent({
|
|||||||
const lineChartRef = useRef<ToggleGraphProps>();
|
const lineChartRef = useRef<ToggleGraphProps>();
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!lineChartRef.current) return;
|
if (!lineChartRef.current) return;
|
||||||
|
|
||||||
@ -218,6 +205,15 @@ function WidgetGraphComponent({
|
|||||||
const existingSearchParams = new URLSearchParams(search);
|
const existingSearchParams = new URLSearchParams(search);
|
||||||
existingSearchParams.delete(QueryParams.expandedWidgetId);
|
existingSearchParams.delete(QueryParams.expandedWidgetId);
|
||||||
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
|
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({
|
history.push({
|
||||||
pathname,
|
pathname,
|
||||||
search: createQueryParams(updatedQueryParams),
|
search: createQueryParams(updatedQueryParams),
|
||||||
@ -283,14 +279,13 @@ function WidgetGraphComponent({
|
|||||||
>
|
>
|
||||||
<FullView
|
<FullView
|
||||||
name={`${name}expanded`}
|
name={`${name}expanded`}
|
||||||
|
version={version}
|
||||||
originalName={name}
|
originalName={name}
|
||||||
widget={widget}
|
widget={widget}
|
||||||
yAxisUnit={widget.yAxisUnit}
|
yAxisUnit={widget.yAxisUnit}
|
||||||
onToggleModelHandler={onToggleModelHandler}
|
onToggleModelHandler={onToggleModelHandler}
|
||||||
parentChartRef={lineChartRef}
|
parentChartRef={lineChartRef}
|
||||||
parentGraphVisibilityState={setGraphVisibility}
|
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
options={options}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
@ -28,6 +29,7 @@ import { getTimeRange } from 'utils/getTimeRange';
|
|||||||
import EmptyWidget from '../EmptyWidget';
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||||
import { GridCardGraphProps } from './types';
|
import { GridCardGraphProps } from './types';
|
||||||
|
import { getLocalStorageGraphVisibilityState } from './utils';
|
||||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||||
|
|
||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
@ -39,6 +41,7 @@ function GridCardGraph({
|
|||||||
threshold,
|
threshold,
|
||||||
variables,
|
variables,
|
||||||
fillSpans = false,
|
fillSpans = false,
|
||||||
|
version,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
@ -132,6 +135,7 @@ function GridCardGraph({
|
|||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
variables: getDashboardVariables(variables),
|
variables: getDashboardVariables(variables),
|
||||||
},
|
},
|
||||||
|
version || DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [
|
||||||
maxTime,
|
maxTime,
|
||||||
@ -183,6 +187,16 @@ function GridCardGraph({
|
|||||||
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
|
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(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getUPlotChartOptions({
|
getUPlotChartOptions({
|
||||||
@ -234,6 +248,7 @@ function GridCardGraph({
|
|||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
isWarning={false}
|
isWarning={false}
|
||||||
name={name}
|
name={name}
|
||||||
|
version={version}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
threshold={threshold}
|
threshold={threshold}
|
||||||
headerMenuList={menuList}
|
headerMenuList={menuList}
|
||||||
@ -253,6 +268,7 @@ GridCardGraph.defaultProps = {
|
|||||||
isQueryEnabled: true,
|
isQueryEnabled: true,
|
||||||
threshold: undefined,
|
threshold: undefined,
|
||||||
headerMenuList: [MenuItemKeys.View],
|
headerMenuList: [MenuItemKeys.View],
|
||||||
|
version: 'v3',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(GridCardGraph);
|
export default memo(GridCardGraph);
|
||||||
|
@ -23,6 +23,7 @@ export interface WidgetGraphComponentProps extends UplotProps {
|
|||||||
>;
|
>;
|
||||||
errorMessage: string | undefined;
|
errorMessage: string | undefined;
|
||||||
name: string;
|
name: string;
|
||||||
|
version?: string;
|
||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
threshold?: ReactNode;
|
threshold?: ReactNode;
|
||||||
@ -43,6 +44,7 @@ export interface GridCardGraphProps {
|
|||||||
isQueryEnabled: boolean;
|
isQueryEnabled: boolean;
|
||||||
variables?: Dashboard['data']['variables'];
|
variables?: Dashboard['data']['variables'];
|
||||||
fillSpans?: boolean;
|
fillSpans?: boolean;
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||||
|
@ -1,14 +1,78 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import getLabelName from 'lib/getLabelName';
|
||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
import { LegendEntryProps } from './FullView/types';
|
import { LegendEntryProps } from './FullView/types';
|
||||||
import { showAllDataSet } from './FullView/utils';
|
import {
|
||||||
|
showAllDataSet,
|
||||||
|
showAllDataSetFromApiResponse,
|
||||||
|
} from './FullView/utils';
|
||||||
import {
|
import {
|
||||||
GetGraphVisibilityStateOnLegendClickProps,
|
GetGraphVisibilityStateOnLegendClickProps,
|
||||||
GraphVisibilityLegendEntryProps,
|
GraphVisibilityLegendEntryProps,
|
||||||
ToggleGraphsVisibilityInChartProps,
|
ToggleGraphsVisibilityInChartProps,
|
||||||
} from './types';
|
} 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 = ({
|
export const getGraphVisibilityStateOnDataChange = ({
|
||||||
options,
|
options,
|
||||||
isExpandedName,
|
isExpandedName,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.fullscreen-grid-container {
|
.fullscreen-grid-container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin-top: 1rem;
|
margin: 8px -8px;
|
||||||
|
|
||||||
.react-grid-layout {
|
.react-grid-layout {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import './GridCardLayout.styles.scss';
|
import './GridCardLayout.styles.scss';
|
||||||
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
@ -144,17 +145,19 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
|
<Tooltip title="Open in Full Screen">
|
||||||
<Button
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
loading={updateDashboardMutation.isLoading}
|
loading={updateDashboardMutation.isLoading}
|
||||||
onClick={handle.enter}
|
onClick={handle.enter}
|
||||||
icon={<FullscreenIcon size={16} />}
|
icon={<FullscreenIcon size={16} />}
|
||||||
disabled={updateDashboardMutation.isLoading}
|
disabled={updateDashboardMutation.isLoading}
|
||||||
>
|
/>
|
||||||
{t('dashboard:full_view')}
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
|
|
||||||
{!isDashboardLocked && addPanelPermission && (
|
{!isDashboardLocked && addPanelPermission && (
|
||||||
<Button
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
onClick={onAddPanelHandler}
|
onClick={onAddPanelHandler}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
data-testid="add-panel"
|
data-testid="add-panel"
|
||||||
@ -201,6 +204,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
|||||||
headerMenuList={widgetActions}
|
headerMenuList={widgetActions}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
fillSpans={currentWidget?.fillSpans}
|
fillSpans={currentWidget?.fillSpans}
|
||||||
|
version={selectedDashboard?.data?.version}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
|
@ -80,7 +80,6 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)`
|
|||||||
export const ButtonContainer = styled(Space)`
|
export const ButtonContainer = styled(Space)`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
margin-top: 1rem;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Button = styled(ButtonComponent)`
|
export const Button = styled(ButtonComponent)`
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { getComponentForPanelType } from 'constants/panelTypes';
|
import { getComponentForPanelType } from 'constants/panelTypes';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||||
@ -50,11 +51,13 @@ const GridPanelSwitch = forwardRef<
|
|||||||
? {
|
? {
|
||||||
selectedLogsFields: selectedLogFields || [],
|
selectedLogsFields: selectedLogFields || [],
|
||||||
query,
|
query,
|
||||||
|
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
|
||||||
selectedTime,
|
selectedTime,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
selectedTracesFields: selectedTracesFields || [],
|
selectedTracesFields: selectedTracesFields || [],
|
||||||
query,
|
query,
|
||||||
|
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
|
||||||
selectedTime,
|
selectedTime,
|
||||||
},
|
},
|
||||||
[PANEL_TYPES.TRACE]: null,
|
[PANEL_TYPES.TRACE]: null,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
||||||
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
@ -109,7 +110,6 @@ function DashboardsList(): JSX.Element {
|
|||||||
width: 30,
|
width: 30,
|
||||||
key: DynamicColumnsKey.CreatedAt,
|
key: DynamicColumnsKey.CreatedAt,
|
||||||
sorter: (a: Data, b: Data): number => {
|
sorter: (a: Data, b: Data): number => {
|
||||||
console.log({ a });
|
|
||||||
const prev = new Date(a.createdAt).getTime();
|
const prev = new Date(a.createdAt).getTime();
|
||||||
const next = new Date(b.createdAt).getTime();
|
const next = new Date(b.createdAt).getTime();
|
||||||
|
|
||||||
@ -211,6 +211,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
ns: 'dashboard',
|
ns: 'dashboard',
|
||||||
}),
|
}),
|
||||||
uploadedGrafana: false,
|
uploadedGrafana: false,
|
||||||
|
version: ENTITY_VERSION_V4,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
@ -304,9 +305,11 @@ function DashboardsList(): JSX.Element {
|
|||||||
loading={isFilteringDashboards}
|
loading={isFilteringDashboards}
|
||||||
style={{ marginBottom: 16, marginTop: 16 }}
|
style={{ marginBottom: 16, marginTop: 16 }}
|
||||||
defaultValue={searchString}
|
defaultValue={searchString}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
{createNewDashboard && (
|
||||||
<Col
|
<Col
|
||||||
span={6}
|
span={6}
|
||||||
style={{
|
style={{
|
||||||
@ -339,17 +342,19 @@ function DashboardsList(): JSX.Element {
|
|||||||
</NewDashboardButton>
|
</NewDashboardButton>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Col>
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
isDashboardListLoading,
|
isDashboardListLoading,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
isFilteringDashboards,
|
isFilteringDashboards,
|
||||||
|
searchString,
|
||||||
|
createNewDashboard,
|
||||||
getMenuItems,
|
getMenuItems,
|
||||||
newDashboardState.loading,
|
newDashboardState.loading,
|
||||||
newDashboardState.error,
|
newDashboardState.error,
|
||||||
getText,
|
getText,
|
||||||
searchString,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -122,12 +122,14 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
|||||||
fields: selectedFields,
|
fields: selectedFields,
|
||||||
linesPerRow: options.maxLines,
|
linesPerRow: options.maxLines,
|
||||||
appendTo: 'end',
|
appendTo: 'end',
|
||||||
|
activeLogIndex,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
|
<Card style={{ width: '100%' }} bodyStyle={CARD_BODY_STYLE}>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
|
||||||
data={logs}
|
data={logs}
|
||||||
totalCount={logs.length}
|
totalCount={logs.length}
|
||||||
itemContent={getItemContent}
|
itemContent={getItemContent}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { LIVE_TAIL_GRAPH_INTERVAL } from 'constants/liveTail';
|
import { LIVE_TAIL_GRAPH_INTERVAL } from 'constants/liveTail';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||||
@ -41,6 +42,7 @@ function LiveLogsListChart({
|
|||||||
const { data, isFetching } = useGetExplorerQueryRange(
|
const { data, isFetching } = useGetExplorerQueryRange(
|
||||||
listChartQuery,
|
listChartQuery,
|
||||||
PANEL_TYPES.TIME_SERIES,
|
PANEL_TYPES.TIME_SERIES,
|
||||||
|
DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
enabled: isConnectionOpen,
|
enabled: isConnectionOpen,
|
||||||
refetchInterval: LIVE_TAIL_GRAPH_INTERVAL,
|
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 {
|
.log-context-container {
|
||||||
border: 1px solid var(--bg-slate-400);
|
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