mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-28 05:41:58 +08:00
commit
08f3b089f4
7
.github/workflows/build.yaml
vendored
7
.github/workflows/build.yaml
vendored
@ -8,6 +8,13 @@ on:
|
|||||||
- release/v*
|
- release/v*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
check-no-ee-references:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Run check
|
||||||
|
run: make check-no-ee-references
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# It Comments out the Line Query-Service & Frontend Section of deploy/docker/clickhouse-setup/docker-compose.yaml
|
|
||||||
# Update the Line Numbers when deploy/docker/clickhouse-setup/docker-compose.yaml chnages.
|
|
||||||
# Docs Ref.: https://github.com/SigNoz/signoz/blob/main/CONTRIBUTING.md#contribute-to-frontend-with-docker-installation-of-signoz
|
|
||||||
|
|
||||||
sed -i 38,62's/.*/# &/' .././deploy/docker/clickhouse-setup/docker-compose.yaml
|
|
9
Makefile
9
Makefile
@ -178,6 +178,15 @@ clear-swarm-ch:
|
|||||||
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
|
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
|
||||||
sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*"
|
sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*"
|
||||||
|
|
||||||
|
check-no-ee-references:
|
||||||
|
@echo "Checking for 'ee' package references in 'pkg' directory..."
|
||||||
|
@if grep -R --include="*.go" '.*/ee/.*' pkg/; then \
|
||||||
|
echo "Error: Found references to 'ee' packages in 'pkg' directory"; \
|
||||||
|
exit 1; \
|
||||||
|
else \
|
||||||
|
echo "No references to 'ee' packages found in 'pkg' directory"; \
|
||||||
|
fi
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./pkg/query-service/app/metrics/...
|
go test ./pkg/query-service/app/metrics/...
|
||||||
go test ./pkg/query-service/cache/...
|
go test ./pkg/query-service/cache/...
|
||||||
|
@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.53.0
|
image: signoz/query-service:0.54.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.53.0
|
image: signoz/frontend:0.54.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.102.7
|
image: signoz/signoz-otel-collector:0.102.8
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@ -238,7 +238,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.102.7
|
image: signoz/signoz-schema-migrator:0.102.8
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
@ -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.102.7}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.8}
|
||||||
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.102.7
|
image: signoz/signoz-otel-collector:0.102.8
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
@ -164,7 +164,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# 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.53.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.54.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@ -204,7 +204,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.53.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.54.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -216,7 +216,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.102.7}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.8}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@ -230,7 +230,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.7}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.8}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
@ -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.53.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.54.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.53.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.54.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.102.7}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.8}
|
||||||
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.102.7}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.8}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
"go.signoz.io/signoz/ee/query-service/dao"
|
"go.signoz.io/signoz/ee/query-service/dao"
|
||||||
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
||||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||||
|
"go.signoz.io/signoz/ee/query-service/rules"
|
||||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
"go.signoz.io/signoz/pkg/query-service/migrate"
|
"go.signoz.io/signoz/pkg/query-service/migrate"
|
||||||
"go.signoz.io/signoz/pkg/query-service/model"
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
@ -52,7 +53,7 @@ import (
|
|||||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||||
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
|
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
|
||||||
rules "go.signoz.io/signoz/pkg/query-service/rules"
|
baserules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||||
"go.signoz.io/signoz/pkg/query-service/utils"
|
"go.signoz.io/signoz/pkg/query-service/utils"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -81,7 +82,7 @@ type ServerOptions struct {
|
|||||||
// Server runs HTTP api service
|
// Server runs HTTP api service
|
||||||
type Server struct {
|
type Server struct {
|
||||||
serverOptions *ServerOptions
|
serverOptions *ServerOptions
|
||||||
ruleManager *rules.Manager
|
ruleManager *baserules.Manager
|
||||||
|
|
||||||
// public http router
|
// public http router
|
||||||
httpConn net.Listener
|
httpConn net.Listener
|
||||||
@ -727,7 +728,7 @@ func makeRulesManager(
|
|||||||
db *sqlx.DB,
|
db *sqlx.DB,
|
||||||
ch baseint.Reader,
|
ch baseint.Reader,
|
||||||
disableRules bool,
|
disableRules bool,
|
||||||
fm baseint.FeatureLookup) (*rules.Manager, error) {
|
fm baseint.FeatureLookup) (*baserules.Manager, error) {
|
||||||
|
|
||||||
// create engine
|
// create engine
|
||||||
pqle, err := pqle.FromConfigPath(promConfigPath)
|
pqle, err := pqle.FromConfigPath(promConfigPath)
|
||||||
@ -743,12 +744,9 @@ func makeRulesManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create manager opts
|
// create manager opts
|
||||||
managerOpts := &rules.ManagerOptions{
|
managerOpts := &baserules.ManagerOptions{
|
||||||
NotifierOpts: notifierOpts,
|
NotifierOpts: notifierOpts,
|
||||||
Queriers: &rules.Queriers{
|
PqlEngine: pqle,
|
||||||
PqlEngine: pqle,
|
|
||||||
Ch: ch.GetConn(),
|
|
||||||
},
|
|
||||||
RepoURL: ruleRepoURL,
|
RepoURL: ruleRepoURL,
|
||||||
DBConn: db,
|
DBConn: db,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
@ -757,10 +755,12 @@ func makeRulesManager(
|
|||||||
FeatureFlags: fm,
|
FeatureFlags: fm,
|
||||||
Reader: ch,
|
Reader: ch,
|
||||||
EvalDelay: baseconst.GetEvalDelay(),
|
EvalDelay: baseconst.GetEvalDelay(),
|
||||||
|
|
||||||
|
PrepareTaskFunc: rules.PrepareTaskFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
// create Manager
|
// create Manager
|
||||||
manager, err := rules.NewManager(managerOpts)
|
manager, err := baserules.NewManager(managerOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("rule manager error: %v", err)
|
return nil, fmt.Errorf("rule manager error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,6 @@ const (
|
|||||||
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
||||||
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
||||||
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
||||||
var SpanRenderLimitStr = GetOrDefaultEnv("SPAN_RENDER_LIMIT", "2500")
|
|
||||||
var MaxSpansInTraceStr = GetOrDefaultEnv("MAX_SPANS_IN_TRACE", "250000")
|
|
||||||
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
|
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
|
||||||
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
|
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ const Onboarding = "ONBOARDING"
|
|||||||
const ChatSupport = "CHAT_SUPPORT"
|
const ChatSupport = "CHAT_SUPPORT"
|
||||||
const Gateway = "GATEWAY"
|
const Gateway = "GATEWAY"
|
||||||
const PremiumSupport = "PREMIUM_SUPPORT"
|
const PremiumSupport = "PREMIUM_SUPPORT"
|
||||||
|
const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2"
|
||||||
|
|
||||||
var BasicPlan = basemodel.FeatureSet{
|
var BasicPlan = basemodel.FeatureSet{
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
@ -127,6 +128,13 @@ var BasicPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: QueryBuilderSearchV2,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ProPlan = basemodel.FeatureSet{
|
var ProPlan = basemodel.FeatureSet{
|
||||||
@ -235,6 +243,13 @@ var ProPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: QueryBuilderSearchV2,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var EnterprisePlan = basemodel.FeatureSet{
|
var EnterprisePlan = basemodel.FeatureSet{
|
||||||
@ -357,4 +372,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: QueryBuilderSearchV2,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
69
ee/query-service/rules/manager.go
Normal file
69
ee/query-service/rules/manager.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
baserules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
|
||||||
|
|
||||||
|
rules := make([]baserules.Rule, 0)
|
||||||
|
var task baserules.Task
|
||||||
|
|
||||||
|
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
|
||||||
|
if opts.Rule.RuleType == baserules.RuleTypeThreshold {
|
||||||
|
// create a threshold rule
|
||||||
|
tr, err := baserules.NewThresholdRule(
|
||||||
|
ruleId,
|
||||||
|
opts.Rule,
|
||||||
|
opts.FF,
|
||||||
|
opts.Reader,
|
||||||
|
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return task, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, tr)
|
||||||
|
|
||||||
|
// create ch rule task for evalution
|
||||||
|
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||||
|
|
||||||
|
} else if opts.Rule.RuleType == baserules.RuleTypeProm {
|
||||||
|
|
||||||
|
// create promql rule
|
||||||
|
pr, err := baserules.NewPromRule(
|
||||||
|
ruleId,
|
||||||
|
opts.Rule,
|
||||||
|
opts.Logger,
|
||||||
|
opts.Reader,
|
||||||
|
opts.ManagerOpts.PqlEngine,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return task, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, pr)
|
||||||
|
|
||||||
|
// create promql rule task for evalution
|
||||||
|
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("unsupported rule type. Supported types: %s, %s", baserules.RuleTypeProm, baserules.RuleTypeThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
return task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTask returns an appropriate group for
|
||||||
|
// rule type
|
||||||
|
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {
|
||||||
|
if taskType == baserules.TaskTypeCh {
|
||||||
|
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||||
|
}
|
||||||
|
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||||
|
}
|
@ -51,7 +51,7 @@
|
|||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
"antd": "5.11.0",
|
"antd": "5.11.0",
|
||||||
"antd-table-saveas-excel": "2.2.1",
|
"antd-table-saveas-excel": "2.2.1",
|
||||||
"axios": "1.6.4",
|
"axios": "1.7.4",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^29.6.4",
|
"babel-jest": "^29.6.4",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
@ -88,7 +88,7 @@
|
|||||||
"lucide-react": "0.379.0",
|
"lucide-react": "0.379.0",
|
||||||
"mini-css-extract-plugin": "2.4.5",
|
"mini-css-extract-plugin": "2.4.5",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
"posthog-js": "1.142.1",
|
"posthog-js": "1.160.3",
|
||||||
"rc-tween-one": "3.0.6",
|
"rc-tween-one": "3.0.6",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-addons-update": "15.6.3",
|
"react-addons-update": "15.6.3",
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"breadcrumb": "Messaging Queues",
|
||||||
|
"header": "Kafka / Overview",
|
||||||
|
"overview": {
|
||||||
|
"title": "Start sending data in as little as 20 minutes",
|
||||||
|
"subtitle": "Connect and Monitor Your Data Streams"
|
||||||
|
},
|
||||||
|
"configureConsumer": {
|
||||||
|
"title": "Configure Consumer",
|
||||||
|
"description": "Add consumer data sources to gain insights and enhance monitoring.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"configureProducer": {
|
||||||
|
"title": "Configure Producer",
|
||||||
|
"description": "Add producer data sources to gain insights and enhance monitoring.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"monitorKafka": {
|
||||||
|
"title": "Monitor kafka",
|
||||||
|
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"summarySection": {
|
||||||
|
"viewDetailsButton": "View Details"
|
||||||
|
},
|
||||||
|
"confirmModal": {
|
||||||
|
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
|
||||||
|
"okText": "Proceed"
|
||||||
|
}
|
||||||
|
}
|
@ -38,5 +38,7 @@
|
|||||||
"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",
|
||||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||||
|
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||||
|
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview"
|
||||||
}
|
}
|
||||||
|
22
frontend/public/locales/en-GB/workspaceLocked.json
Normal file
22
frontend/public/locales/en-GB/workspaceLocked.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"trialPlanExpired": "Trial Plan Expired",
|
||||||
|
"gotQuestions": "Got Questions?",
|
||||||
|
"contactUs": "Contact Us",
|
||||||
|
"upgradeToContinue": "Upgrade to Continue",
|
||||||
|
"upgradeNow": "Upgrade now to keep enjoying all the great features you’ve been using.",
|
||||||
|
"yourDataIsSafe": "Your data is safe with us until",
|
||||||
|
"actNow": "Act now to avoid any disruptions and continue where you left off.",
|
||||||
|
"contactAdmin": "Contact your admin to proceed with the upgrade.",
|
||||||
|
"continueMyJourney": "Continue My Journey",
|
||||||
|
"needMoreTime": "Need More Time?",
|
||||||
|
"extendTrial": "Extend Trial",
|
||||||
|
"extendTrialMsgPart1": "If you have a specific reason why you were not able to finish your PoC in the trial period, please write to us on",
|
||||||
|
"extendTrialMsgPart2": "with the reason. Sometimes we can extend trial by a few days on a case by case basis",
|
||||||
|
"whyChooseSignoz": "Why choose Signoz",
|
||||||
|
"enterpriseGradeObservability": "Enterprise-grade Observability",
|
||||||
|
"observabilityDescription": "Get access to observability at any scale with advanced security and compliance.",
|
||||||
|
"continueToUpgrade": "Continue to Upgrade",
|
||||||
|
"youAreInGoodCompany": "You are in good company",
|
||||||
|
"faqs": "FAQs",
|
||||||
|
"somethingWentWrong": "Something went wrong"
|
||||||
|
}
|
30
frontend/public/locales/en/messagingQueuesKafkaOverview.json
Normal file
30
frontend/public/locales/en/messagingQueuesKafkaOverview.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"breadcrumb": "Messaging Queues",
|
||||||
|
"header": "Kafka / Overview",
|
||||||
|
"overview": {
|
||||||
|
"title": "Start sending data in as little as 20 minutes",
|
||||||
|
"subtitle": "Connect and Monitor Your Data Streams"
|
||||||
|
},
|
||||||
|
"configureConsumer": {
|
||||||
|
"title": "Configure Consumer",
|
||||||
|
"description": "Add consumer data sources to gain insights and enhance monitoring.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"configureProducer": {
|
||||||
|
"title": "Configure Producer",
|
||||||
|
"description": "Add producer data sources to gain insights and enhance monitoring.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"monitorKafka": {
|
||||||
|
"title": "Monitor kafka",
|
||||||
|
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
|
||||||
|
"button": "Get Started"
|
||||||
|
},
|
||||||
|
"summarySection": {
|
||||||
|
"viewDetailsButton": "View Details"
|
||||||
|
},
|
||||||
|
"confirmModal": {
|
||||||
|
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
|
||||||
|
"okText": "Proceed"
|
||||||
|
}
|
||||||
|
}
|
@ -50,5 +50,7 @@
|
|||||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||||
"SHORTCUTS": "SigNoz | Shortcuts",
|
"SHORTCUTS": "SigNoz | Shortcuts",
|
||||||
"INTEGRATIONS": "SigNoz | Integrations",
|
"INTEGRATIONS": "SigNoz | Integrations",
|
||||||
|
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||||
|
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
|
||||||
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
|
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
|
||||||
}
|
}
|
||||||
|
22
frontend/public/locales/en/workspaceLocked.json
Normal file
22
frontend/public/locales/en/workspaceLocked.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"trialPlanExpired": "Trial Plan Expired",
|
||||||
|
"gotQuestions": "Got Questions?",
|
||||||
|
"contactUs": "Contact Us",
|
||||||
|
"upgradeToContinue": "Upgrade to Continue",
|
||||||
|
"upgradeNow": "Upgrade now to keep enjoying all the great features you’ve been using.",
|
||||||
|
"yourDataIsSafe": "Your data is safe with us until",
|
||||||
|
"actNow": "Act now to avoid any disruptions and continue where you left off.",
|
||||||
|
"contactAdmin": "Contact your admin to proceed with the upgrade.",
|
||||||
|
"continueMyJourney": "Continue My Journey",
|
||||||
|
"needMoreTime": "Need More Time?",
|
||||||
|
"extendTrial": "Extend Trial",
|
||||||
|
"extendTrialMsgPart1": "If you have a specific reason why you were not able to finish your PoC in the trial period, please write to us on",
|
||||||
|
"extendTrialMsgPart2": "with the reason. Sometimes we can extend trial by a few days on a case by case basis",
|
||||||
|
"whyChooseSignoz": "Why choose Signoz",
|
||||||
|
"enterpriseGradeObservability": "Enterprise-grade Observability",
|
||||||
|
"observabilityDescription": "Get access to observability at any scale with advanced security and compliance.",
|
||||||
|
"continueToUpgrade": "Continue to Upgrade",
|
||||||
|
"youAreInGoodCompany": "You are in good company",
|
||||||
|
"faqs": "FAQs",
|
||||||
|
"somethingWentWrong": "Something went wrong"
|
||||||
|
}
|
@ -19,6 +19,7 @@ import { ResourceProvider } from 'hooks/useResourceAttribute';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { identity, pick, pickBy } from 'lodash-es';
|
import { identity, pick, pickBy } from 'lodash-es';
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
|
import AlertRuleProvider from 'providers/Alert';
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
@ -236,22 +237,24 @@ function App(): JSX.Element {
|
|||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<AppLayout>
|
<AlertRuleProvider>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<AppLayout>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Switch>
|
||||||
<Route
|
{routes.map(({ path, component, exact }) => (
|
||||||
key={`${path}`}
|
<Route
|
||||||
exact={exact}
|
key={`${path}`}
|
||||||
path={path}
|
exact={exact}
|
||||||
component={component}
|
path={path}
|
||||||
/>
|
component={component}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
</AlertRuleProvider>
|
||||||
</KeyboardHotkeysProvider>
|
</KeyboardHotkeysProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
|
@ -92,6 +92,14 @@ export const CreateNewAlerts = Loadable(
|
|||||||
() => import(/* webpackChunkName: "Create Alerts" */ 'pages/CreateAlert'),
|
() => import(/* webpackChunkName: "Create Alerts" */ 'pages/CreateAlert'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const AlertHistory = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "Alert History" */ 'pages/AlertList'),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AlertOverview = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "Alert Overview" */ 'pages/AlertList'),
|
||||||
|
);
|
||||||
|
|
||||||
export const CreateAlertChannelAlerts = Loadable(
|
export const CreateAlertChannelAlerts = Loadable(
|
||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
|
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
|
||||||
|
@ -2,6 +2,8 @@ import ROUTES from 'constants/routes';
|
|||||||
import { RouteProps } from 'react-router-dom';
|
import { RouteProps } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AlertHistory,
|
||||||
|
AlertOverview,
|
||||||
AllAlertChannels,
|
AllAlertChannels,
|
||||||
AllErrors,
|
AllErrors,
|
||||||
APIKeys,
|
APIKeys,
|
||||||
@ -171,6 +173,20 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'ALERTS_NEW',
|
key: 'ALERTS_NEW',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.ALERT_HISTORY,
|
||||||
|
exact: true,
|
||||||
|
component: AlertHistory,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'ALERT_HISTORY',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.ALERT_OVERVIEW,
|
||||||
|
exact: true,
|
||||||
|
component: AlertOverview,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'ALERT_OVERVIEW',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.TRACE,
|
path: ROUTES.TRACE,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
@ -1,26 +1,20 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/create';
|
import { PayloadProps, Props } from 'types/api/alerts/create';
|
||||||
|
|
||||||
const create = async (
|
const create = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.post('/rules', {
|
||||||
const response = await axios.post('/rules', {
|
...props.data,
|
||||||
...props.data,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default create;
|
export default create;
|
||||||
|
@ -1,24 +1,18 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/delete';
|
import { PayloadProps, Props } from 'types/api/alerts/delete';
|
||||||
|
|
||||||
const deleteAlerts = async (
|
const deleteAlerts = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.delete(`/rules/${props.id}`);
|
||||||
const response = await axios.delete(`/rules/${props.id}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data.rules,
|
payload: response.data.data.rules,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default deleteAlerts;
|
export default deleteAlerts;
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/get';
|
import { PayloadProps, Props } from 'types/api/alerts/get';
|
||||||
|
|
||||||
const get = async (
|
const get = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.get(`/rules/${props.id}`);
|
||||||
const response = await axios.get(`/rules/${props.id}`);
|
return {
|
||||||
|
statusCode: 200,
|
||||||
return {
|
error: null,
|
||||||
statusCode: 200,
|
message: response.data.status,
|
||||||
error: null,
|
payload: response.data,
|
||||||
message: response.data.status,
|
};
|
||||||
payload: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default get;
|
export default get;
|
||||||
|
@ -1,26 +1,20 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/patch';
|
import { PayloadProps, Props } from 'types/api/alerts/patch';
|
||||||
|
|
||||||
const patch = async (
|
const patch = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.patch(`/rules/${props.id}`, {
|
||||||
const response = await axios.patch(`/rules/${props.id}`, {
|
...props.data,
|
||||||
...props.data,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default patch;
|
export default patch;
|
||||||
|
@ -1,26 +1,20 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||||
|
|
||||||
const put = async (
|
const put = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
const response = await axios.put(`/rules/${props.id}`, {
|
||||||
const response = await axios.put(`/rules/${props.id}`, {
|
...props.data,
|
||||||
...props.data,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default put;
|
export default put;
|
||||||
|
28
frontend/src/api/alerts/ruleStats.ts
Normal file
28
frontend/src/api/alerts/ruleStats.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { AlertRuleStatsPayload } from 'types/api/alerts/def';
|
||||||
|
import { RuleStatsProps } from 'types/api/alerts/ruleStats';
|
||||||
|
|
||||||
|
const ruleStats = async (
|
||||||
|
props: RuleStatsProps,
|
||||||
|
): Promise<SuccessResponse<AlertRuleStatsPayload> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/rules/${props.id}/history/stats`, {
|
||||||
|
start: props.start,
|
||||||
|
end: props.end,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ruleStats;
|
33
frontend/src/api/alerts/timelineGraph.ts
Normal file
33
frontend/src/api/alerts/timelineGraph.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { AlertRuleTimelineGraphResponsePayload } from 'types/api/alerts/def';
|
||||||
|
import { GetTimelineGraphRequestProps } from 'types/api/alerts/timelineGraph';
|
||||||
|
|
||||||
|
const timelineGraph = async (
|
||||||
|
props: GetTimelineGraphRequestProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<AlertRuleTimelineGraphResponsePayload> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`/rules/${props.id}/history/overall_status`,
|
||||||
|
{
|
||||||
|
start: props.start,
|
||||||
|
end: props.end,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default timelineGraph;
|
36
frontend/src/api/alerts/timelineTable.ts
Normal file
36
frontend/src/api/alerts/timelineTable.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { AlertRuleTimelineTableResponsePayload } from 'types/api/alerts/def';
|
||||||
|
import { GetTimelineTableRequestProps } from 'types/api/alerts/timelineTable';
|
||||||
|
|
||||||
|
const timelineTable = async (
|
||||||
|
props: GetTimelineTableRequestProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<AlertRuleTimelineTableResponsePayload> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/rules/${props.id}/history/timeline`, {
|
||||||
|
start: props.start,
|
||||||
|
end: props.end,
|
||||||
|
offset: props.offset,
|
||||||
|
limit: props.limit,
|
||||||
|
order: props.order,
|
||||||
|
state: props.state,
|
||||||
|
// TODO(shaheer): implement filters
|
||||||
|
filters: props.filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default timelineTable;
|
33
frontend/src/api/alerts/topContributors.ts
Normal file
33
frontend/src/api/alerts/topContributors.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { AlertRuleTopContributorsPayload } from 'types/api/alerts/def';
|
||||||
|
import { TopContributorsProps } from 'types/api/alerts/topContributors';
|
||||||
|
|
||||||
|
const topContributors = async (
|
||||||
|
props: TopContributorsProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<AlertRuleTopContributorsPayload> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`/rules/${props.id}/history/top_contributors`,
|
||||||
|
{
|
||||||
|
start: props.start,
|
||||||
|
end: props.end,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default topContributors;
|
41
frontend/src/assets/AlertHistory/ConfigureIcon.tsx
Normal file
41
frontend/src/assets/AlertHistory/ConfigureIcon.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
interface ConfigureIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfigureIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
}: ConfigureIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="#C0C1C3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="1.333"
|
||||||
|
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke="#C0C1C3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth="1.333"
|
||||||
|
d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3m5.417 7.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigureIcon.defaultProps = {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
fill: 'none',
|
||||||
|
};
|
||||||
|
export default ConfigureIcon;
|
65
frontend/src/assets/AlertHistory/LogsIcon.tsx
Normal file
65
frontend/src/assets/AlertHistory/LogsIcon.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
interface LogsIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
strokeColor?: string;
|
||||||
|
strokeWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogsIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
strokeColor,
|
||||||
|
strokeWidth,
|
||||||
|
}: LogsIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M2.917 3.208v7.875"
|
||||||
|
/>
|
||||||
|
<ellipse
|
||||||
|
cx="6.417"
|
||||||
|
cy="3.208"
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
rx="3.5"
|
||||||
|
ry="1.458"
|
||||||
|
/>
|
||||||
|
<ellipse cx="6.417" cy="3.165" fill={strokeColor} rx="0.875" ry="0.365" />
|
||||||
|
<path
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M9.917 11.083c0 .645-1.567 1.167-3.5 1.167s-3.5-.522-3.5-1.167"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M5.25 6.417v1.117c0 .028.02.053.049.057l1.652.276A.058.058 0 017 7.924v1.993"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke={strokeColor}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M9.917 3.208v3.103c0 .046.05.074.089.05L12.182 5a.058.058 0 01.088.035l.264 1.059a.058.058 0 01-.013.053l-2.59 2.877a.058.058 0 00-.014.04v2.018"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogsIcon.defaultProps = {
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
fill: 'none',
|
||||||
|
strokeColor: '#C0C1C3',
|
||||||
|
strokeWidth: 1.167,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsIcon;
|
39
frontend/src/assets/AlertHistory/SeverityCriticalIcon.tsx
Normal file
39
frontend/src/assets/AlertHistory/SeverityCriticalIcon.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
interface SeverityCriticalIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeverityCriticalIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
}: SeverityCriticalIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M.99707.666056.99707 2.99939M.99707 5.33337H.991237M3.00293.666056 3.00293 2.99939M3.00293 5.33337H2.9971M5.00879.666056V2.99939M5.00879 5.33337H5.00296"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth="1.16667"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeverityCriticalIcon.defaultProps = {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#F56C87',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeverityCriticalIcon;
|
42
frontend/src/assets/AlertHistory/SeverityErrorIcon.tsx
Normal file
42
frontend/src/assets/AlertHistory/SeverityErrorIcon.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
interface SeverityErrorIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
strokeWidth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeverityErrorIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
strokeWidth,
|
||||||
|
}: SeverityErrorIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1.00781.957845 1.00781 2.99951M1.00781 5.04175H1.00228"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeverityErrorIcon.defaultProps = {
|
||||||
|
width: 2,
|
||||||
|
height: 6,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#F56C87',
|
||||||
|
strokeWidth: '1.02083',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeverityErrorIcon;
|
46
frontend/src/assets/AlertHistory/SeverityInfoIcon.tsx
Normal file
46
frontend/src/assets/AlertHistory/SeverityInfoIcon.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
interface SeverityInfoIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeverityInfoIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
}: SeverityInfoIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
rx="3.5"
|
||||||
|
fill={stroke}
|
||||||
|
fillOpacity=".2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7 9.33346V7.00012M7 4.66675H7.00583"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth="1.16667"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeverityInfoIcon.defaultProps = {
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#7190F9',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeverityInfoIcon;
|
42
frontend/src/assets/AlertHistory/SeverityWarningIcon.tsx
Normal file
42
frontend/src/assets/AlertHistory/SeverityWarningIcon.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
interface SeverityWarningIconProps {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
strokeWidth?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SeverityWarningIcon({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill,
|
||||||
|
stroke,
|
||||||
|
strokeWidth,
|
||||||
|
}: SeverityWarningIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill={fill}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1.00732.957845 1.00732 2.99951M1.00732 5.04175H1.00179"
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeverityWarningIcon.defaultProps = {
|
||||||
|
width: 2,
|
||||||
|
height: 6,
|
||||||
|
fill: 'none',
|
||||||
|
stroke: '#FFD778',
|
||||||
|
strokeWidth: '0.978299',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeverityWarningIcon;
|
@ -0,0 +1,14 @@
|
|||||||
|
.reset-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.reset-button {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
11
frontend/src/components/AlertDetailsFilters/Filters.tsx
Normal file
11
frontend/src/components/AlertDetailsFilters/Filters.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import './Filters.styles.scss';
|
||||||
|
|
||||||
|
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
|
||||||
|
export function Filters(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="filters">
|
||||||
|
<DateTimeSelector showAutoRefresh={false} hideShareModal showResetButton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
.checkbox-filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
.filter-header-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.left-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.clear-all {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.values {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.checkbox-value-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.filter-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.value-string {
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-string {
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.toggle-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-value-section:hover {
|
||||||
|
.toggle-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.only-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value:hover {
|
||||||
|
.toggle-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.show-more-text {
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.checkbox-filter {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
.filter-header-checkbox {
|
||||||
|
.left-action {
|
||||||
|
.title {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,510 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import './Checkbox.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
|
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
|
||||||
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||||
|
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'nin'];
|
||||||
|
|
||||||
|
function setDefaultValues(
|
||||||
|
values: string[],
|
||||||
|
trueOrFalse: boolean,
|
||||||
|
): Record<string, boolean> {
|
||||||
|
const defaultState: Record<string, boolean> = {};
|
||||||
|
values.forEach((val) => {
|
||||||
|
defaultState[val] = trueOrFalse;
|
||||||
|
});
|
||||||
|
return defaultState;
|
||||||
|
}
|
||||||
|
interface ICheckboxProps {
|
||||||
|
filter: IQuickFiltersConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||||
|
const { filter } = props;
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
|
||||||
|
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
|
||||||
|
|
||||||
|
const {
|
||||||
|
lastUsedQuery,
|
||||||
|
currentQuery,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetAggregateValues(
|
||||||
|
{
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
|
aggregateAttribute: '',
|
||||||
|
attributeKey: filter.attributeKey.key,
|
||||||
|
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||||
|
tagType: filter.attributeKey.type || '',
|
||||||
|
searchText: searchText ?? '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isOpen,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributeValues: string[] = useMemo(
|
||||||
|
() =>
|
||||||
|
((Object.values(data?.payload || {}).find((el) => !!el) ||
|
||||||
|
[]) as string[]).filter((val) => !isEmpty(val)),
|
||||||
|
[data?.payload],
|
||||||
|
);
|
||||||
|
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
|
||||||
|
|
||||||
|
// derive the state of each filter key here in the renderer itself and keep it in sync with staged query
|
||||||
|
// also we need to keep a note of last focussed query.
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const currentFilterState = useMemo(() => {
|
||||||
|
let filterState: Record<string, boolean> = setDefaultValues(
|
||||||
|
attributeValues,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const filterSync = currentQuery?.builder.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items.find((item) =>
|
||||||
|
isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filterSync) {
|
||||||
|
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||||
|
if (isArray(filterSync.value)) {
|
||||||
|
filterSync.value.forEach((val) => {
|
||||||
|
filterState[val] = true;
|
||||||
|
});
|
||||||
|
} else if (typeof filterSync.value === 'string') {
|
||||||
|
filterState[filterSync.value] = true;
|
||||||
|
} else if (typeof filterSync.value === 'boolean') {
|
||||||
|
filterState[String(filterSync.value)] = true;
|
||||||
|
} else if (typeof filterSync.value === 'number') {
|
||||||
|
filterState[String(filterSync.value)] = true;
|
||||||
|
}
|
||||||
|
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||||
|
filterState = setDefaultValues(attributeValues, true);
|
||||||
|
if (isArray(filterSync.value)) {
|
||||||
|
filterSync.value.forEach((val) => {
|
||||||
|
filterState[val] = false;
|
||||||
|
});
|
||||||
|
} else if (typeof filterSync.value === 'string') {
|
||||||
|
filterState[filterSync.value] = false;
|
||||||
|
} else if (typeof filterSync.value === 'boolean') {
|
||||||
|
filterState[String(filterSync.value)] = false;
|
||||||
|
} else if (typeof filterSync.value === 'number') {
|
||||||
|
filterState[String(filterSync.value)] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filterState = setDefaultValues(attributeValues, true);
|
||||||
|
}
|
||||||
|
return filterState;
|
||||||
|
}, [
|
||||||
|
attributeValues,
|
||||||
|
currentQuery?.builder.queryData,
|
||||||
|
filter.attributeKey,
|
||||||
|
lastUsedQuery,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||||
|
const isFilterDisabled = useMemo(
|
||||||
|
() =>
|
||||||
|
(currentQuery?.builder?.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items?.filter((item) =>
|
||||||
|
isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
)?.length || 0) > 1,
|
||||||
|
|
||||||
|
[currentQuery?.builder?.queryData, lastUsedQuery, filter.attributeKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
// variable to check if the current filter has multiple values to its name in the key op value section
|
||||||
|
const isMultipleValuesTrueForTheKey =
|
||||||
|
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||||
|
|
||||||
|
const handleClearFilterAttribute = (): void => {
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items:
|
||||||
|
idx === lastUsedQuery
|
||||||
|
? item.filters.items.filter(
|
||||||
|
(fil) => !isEqual(fil.key?.key, filter.attributeKey.key),
|
||||||
|
)
|
||||||
|
: [...item.filters.items],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items?.some((item) =>
|
||||||
|
isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange = (
|
||||||
|
value: string,
|
||||||
|
checked: boolean,
|
||||||
|
isOnlyOrAllClicked: boolean,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
): void => {
|
||||||
|
const query = cloneDeep(currentQuery.builder.queryData?.[lastUsedQuery || 0]);
|
||||||
|
|
||||||
|
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||||
|
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||||
|
if (isOnlyOrAllClicked && query?.filters?.items) {
|
||||||
|
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
|
||||||
|
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||||
|
? 'All'
|
||||||
|
: 'Only'
|
||||||
|
: 'Only';
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
if (isOnlyOrAll === 'Only') {
|
||||||
|
const newFilterItem: TagFilterItem = {
|
||||||
|
id: uuid(),
|
||||||
|
op: getOperatorValue(OPERATORS.IN),
|
||||||
|
key: filter.attributeKey,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
query.filters.items = [...query.filters.items, newFilterItem];
|
||||||
|
}
|
||||||
|
} else if (query?.filters?.items) {
|
||||||
|
if (
|
||||||
|
query.filters?.items?.some((item) =>
|
||||||
|
isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// if there is already a running filter for the current attribute key then
|
||||||
|
// we split the cases by which particular operator is present right now!
|
||||||
|
const currentFilter = query.filters?.items?.find((q) =>
|
||||||
|
isEqual(q.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
if (currentFilter) {
|
||||||
|
const runningOperator = currentFilter?.op;
|
||||||
|
switch (runningOperator) {
|
||||||
|
case 'in':
|
||||||
|
if (checked) {
|
||||||
|
// if it's an IN operator then if we are checking another value it get's added to the
|
||||||
|
// filter clause. example - key IN [value1, currentSelectedValue]
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [...currentFilter.value, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// if the current state wasn't an array we make it one and add our value
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (!checked) {
|
||||||
|
// if we are removing some value when the running operator is IN we filter.
|
||||||
|
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: currentFilter.value.filter((val) => val !== value),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.value.length === 0) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if not an array remove the whole thing altogether!
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'nin':
|
||||||
|
// if the current running operator is NIN then when unchecking the value it gets
|
||||||
|
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
||||||
|
if (!checked) {
|
||||||
|
// in case of array add the currentUnselectedValue to the list.
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [...currentFilter.value, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// in case of not an array make it one!
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (checked) {
|
||||||
|
// opposite of above!
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: currentFilter.value.filter((val) => val !== value),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.value.length === 0) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
if (checked) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
op: getOperatorValue(OPERATORS.IN),
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else if (!checked) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '!=':
|
||||||
|
if (!checked) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
op: getOperatorValue(OPERATORS.NIN),
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key?.key, filter.attributeKey.key)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else if (checked) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// case - when there is no filter for the current key that means all are selected right now.
|
||||||
|
const newFilterItem: TagFilterItem = {
|
||||||
|
id: uuid(),
|
||||||
|
op: getOperatorValue(OPERATORS.NIN),
|
||||||
|
key: filter.attributeKey,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
query.filters.items = [...query.filters.items, newFilterItem];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const finalQuery = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
...currentQuery.builder.queryData.map((q, idx) => {
|
||||||
|
if (idx === lastUsedQuery) {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
redirectWithQueryBuilderData(finalQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="checkbox-filter">
|
||||||
|
<section className="filter-header-checkbox">
|
||||||
|
<section className="left-action">
|
||||||
|
{isOpen ? (
|
||||||
|
<ChevronDown
|
||||||
|
size={13}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={(): void => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setVisibleItemsCount(10);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronRight
|
||||||
|
size={13}
|
||||||
|
onClick={(): void => setIsOpen(true)}
|
||||||
|
cursor="pointer"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography.Text className="title">{filter.title}</Typography.Text>
|
||||||
|
</section>
|
||||||
|
<section className="right-action">
|
||||||
|
{isOpen && (
|
||||||
|
<Typography.Text
|
||||||
|
className="clear-all"
|
||||||
|
onClick={handleClearFilterAttribute}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
{isOpen && isLoading && !attributeValues.length && (
|
||||||
|
<section className="loading">
|
||||||
|
<Skeleton paragraph={{ rows: 4 }} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{isOpen && !isLoading && (
|
||||||
|
<>
|
||||||
|
<section className="search">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter values"
|
||||||
|
onChange={(e): void => setSearchText(e.target.value)}
|
||||||
|
disabled={isFilterDisabled}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{attributeValues.length > 0 ? (
|
||||||
|
<section className="values">
|
||||||
|
{currentAttributeKeys.map((value: string) => (
|
||||||
|
<div key={value} className="value">
|
||||||
|
<Checkbox
|
||||||
|
onChange={(e): void => onChange(value, e.target.checked, false)}
|
||||||
|
checked={currentFilterState[value]}
|
||||||
|
disabled={isFilterDisabled}
|
||||||
|
rootClassName="check-box"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
'checkbox-value-section',
|
||||||
|
isFilterDisabled ? 'filter-disabled' : '',
|
||||||
|
)}
|
||||||
|
onClick={(): void => {
|
||||||
|
if (isFilterDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange(value, currentFilterState[value], true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filter.customRendererForValue ? (
|
||||||
|
filter.customRendererForValue(value)
|
||||||
|
) : (
|
||||||
|
<Typography.Text
|
||||||
|
className="value-string"
|
||||||
|
ellipsis={{ tooltip: { placement: 'right' } }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Button type="text" className="only-btn">
|
||||||
|
{isSomeFilterPresentForCurrentAttribute
|
||||||
|
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||||
|
? 'All'
|
||||||
|
: 'Only'
|
||||||
|
: 'Only'}
|
||||||
|
</Button>
|
||||||
|
<Button type="text" className="toggle-btn">
|
||||||
|
Toggle
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<section className="no-data">
|
||||||
|
<Typography.Text>No values found</Typography.Text>{' '}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{visibleItemsCount < attributeValues?.length && (
|
||||||
|
<section className="show-more">
|
||||||
|
<Typography.Text
|
||||||
|
className="show-more-text"
|
||||||
|
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
|
||||||
|
>
|
||||||
|
Show More...
|
||||||
|
</Typography.Text>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import './Slider.styles.scss';
|
||||||
|
|
||||||
|
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
|
||||||
|
|
||||||
|
interface ISliderProps {
|
||||||
|
filter: IQuickFiltersConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not needed for now build when required
|
||||||
|
export default function Slider(props: ISliderProps): JSX.Element {
|
||||||
|
const { filter } = props;
|
||||||
|
console.log(filter);
|
||||||
|
return <div>Slider</div>;
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
.quick-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10.5px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.left-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-tag {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 9px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(78, 116, 248, 0.2);
|
||||||
|
background: rgba(78, 116, 248, 0.1);
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.divider-filter {
|
||||||
|
width: 1px;
|
||||||
|
height: 14px;
|
||||||
|
background: #161922;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.quick-filters {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-right: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.left-actions {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.right-actions {
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
frontend/src/components/QuickFilters/QuickFilters.tsx
Normal file
124
frontend/src/components/QuickFilters/QuickFilters.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import './QuickFilters.styles.scss';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FilterOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
VerticalAlignTopOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
||||||
|
import Slider from './FilterRenderers/Slider/Slider';
|
||||||
|
|
||||||
|
export enum FiltersType {
|
||||||
|
SLIDER = 'SLIDER',
|
||||||
|
CHECKBOX = 'CHECKBOX',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MinMax {
|
||||||
|
MIN = 'MIN',
|
||||||
|
MAX = 'MAX',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SpecficFilterOperations {
|
||||||
|
ALL = 'ALL',
|
||||||
|
ONLY = 'ONLY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQuickFiltersConfig {
|
||||||
|
type: FiltersType;
|
||||||
|
title: string;
|
||||||
|
attributeKey: BaseAutocompleteData;
|
||||||
|
customRendererForValue?: (value: string) => JSX.Element;
|
||||||
|
defaultOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IQuickFiltersProps {
|
||||||
|
config: IQuickFiltersConfig[];
|
||||||
|
handleFilterVisibilityChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||||
|
const { config, handleFilterVisibilityChange } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentQuery,
|
||||||
|
lastUsedQuery,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
// clear all the filters for the query which is in sync with filters
|
||||||
|
const handleReset = (): void => {
|
||||||
|
const updatedQuery = cloneDeep(
|
||||||
|
currentQuery?.builder.queryData?.[lastUsedQuery || 0],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updatedQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedQuery?.filters?.items) {
|
||||||
|
updatedQuery.filters.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items: idx === lastUsedQuery ? [] : [...item.filters.items],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastQueryName =
|
||||||
|
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||||
|
return (
|
||||||
|
<div className="quick-filters">
|
||||||
|
<section className="header">
|
||||||
|
<section className="left-actions">
|
||||||
|
<FilterOutlined />
|
||||||
|
<Typography.Text className="text">Filters for</Typography.Text>
|
||||||
|
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||||
|
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
<section className="right-actions">
|
||||||
|
<Tooltip title="Reset All">
|
||||||
|
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||||
|
</Tooltip>
|
||||||
|
<div className="divider-filter" />
|
||||||
|
<Tooltip title="Collapse Filters">
|
||||||
|
<VerticalAlignTopOutlined
|
||||||
|
rotate={270}
|
||||||
|
onClick={handleFilterVisibilityChange}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="filters">
|
||||||
|
{config.map((filter) => {
|
||||||
|
switch (filter.type) {
|
||||||
|
case FiltersType.CHECKBOX:
|
||||||
|
return <Checkbox filter={filter} />;
|
||||||
|
case FiltersType.SLIDER:
|
||||||
|
return <Slider filter={filter} />;
|
||||||
|
default:
|
||||||
|
return <Checkbox filter={filter} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
.tab-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
41
frontend/src/components/TabsAndFilters/Tabs/Tabs.tsx
Normal file
41
frontend/src/components/TabsAndFilters/Tabs/Tabs.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import './Tabs.styles.scss';
|
||||||
|
|
||||||
|
import { Radio } from 'antd';
|
||||||
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
|
import { History, Table } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { ALERT_TABS } from '../constants';
|
||||||
|
|
||||||
|
export function Tabs(): JSX.Element {
|
||||||
|
const [selectedTab, setSelectedTab] = useState('overview');
|
||||||
|
|
||||||
|
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||||
|
setSelectedTab(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Radio.Group className="tabs" onChange={handleTabChange} value={selectedTab}>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedTab === ALERT_TABS.OVERVIEW ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={ALERT_TABS.OVERVIEW}
|
||||||
|
>
|
||||||
|
<div className="tab-title">
|
||||||
|
<Table size={14} />
|
||||||
|
Overview
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={selectedTab === ALERT_TABS.HISTORY ? 'selected_view tab' : 'tab'}
|
||||||
|
value={ALERT_TABS.HISTORY}
|
||||||
|
>
|
||||||
|
<div className="tab-title">
|
||||||
|
<History size={14} />
|
||||||
|
History
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
@mixin flex-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-and-filters {
|
||||||
|
@include flex-center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
.filters {
|
||||||
|
@include flex-center;
|
||||||
|
gap: 16px;
|
||||||
|
.reset-button {
|
||||||
|
@include flex-center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
frontend/src/components/TabsAndFilters/TabsAndFilters.tsx
Normal file
16
frontend/src/components/TabsAndFilters/TabsAndFilters.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import './TabsAndFilters.styles.scss';
|
||||||
|
|
||||||
|
import { Filters } from 'components/AlertDetailsFilters/Filters';
|
||||||
|
|
||||||
|
import { Tabs } from './Tabs/Tabs';
|
||||||
|
|
||||||
|
function TabsAndFilters(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="tabs-and-filters">
|
||||||
|
<Tabs />
|
||||||
|
<Filters />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabsAndFilters;
|
5
frontend/src/components/TabsAndFilters/constants.ts
Normal file
5
frontend/src/components/TabsAndFilters/constants.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const ALERT_TABS = {
|
||||||
|
OVERVIEW: 'OVERVIEW',
|
||||||
|
HISTORY: 'HISTORY',
|
||||||
|
ACTIVITY: 'ACTIVITY',
|
||||||
|
} as const;
|
@ -21,4 +21,5 @@ export enum FeatureKeys {
|
|||||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||||
GATEWAY = 'GATEWAY',
|
GATEWAY = 'GATEWAY',
|
||||||
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
||||||
|
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,17 @@
|
|||||||
|
import { ManipulateType } from 'dayjs';
|
||||||
|
|
||||||
const MAX_RPS_LIMIT = 100;
|
const MAX_RPS_LIMIT = 100;
|
||||||
export { MAX_RPS_LIMIT };
|
export { MAX_RPS_LIMIT };
|
||||||
|
|
||||||
export const LEGEND = 'legend';
|
export const LEGEND = 'legend';
|
||||||
|
|
||||||
|
export const DAYJS_MANIPULATE_TYPES: { [key: string]: ManipulateType } = {
|
||||||
|
DAY: 'day',
|
||||||
|
WEEK: 'week',
|
||||||
|
MONTH: 'month',
|
||||||
|
YEAR: 'year',
|
||||||
|
HOUR: 'hour',
|
||||||
|
MINUTE: 'minute',
|
||||||
|
SECOND: 'second',
|
||||||
|
MILLISECOND: 'millisecond',
|
||||||
|
};
|
||||||
|
@ -19,4 +19,6 @@ export enum LOCALSTORAGE {
|
|||||||
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
||||||
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
|
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
|
||||||
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
||||||
|
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
|
||||||
|
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
|
||||||
}
|
}
|
||||||
|
@ -8,5 +8,14 @@ export const REACT_QUERY_KEY = {
|
|||||||
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
|
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
|
||||||
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
|
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
|
||||||
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
|
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
|
||||||
|
ALERT_RULE_DETAILS: 'ALERT_RULE_DETAILS',
|
||||||
|
ALERT_RULE_STATS: 'ALERT_RULE_STATS',
|
||||||
|
ALERT_RULE_TOP_CONTRIBUTORS: 'ALERT_RULE_TOP_CONTRIBUTORS',
|
||||||
|
ALERT_RULE_TIMELINE_TABLE: 'ALERT_RULE_TIMELINE_TABLE',
|
||||||
|
ALERT_RULE_TIMELINE_GRAPH: 'ALERT_RULE_TIMELINE_GRAPH',
|
||||||
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
|
GET_CONSUMER_LAG_DETAILS: 'GET_CONSUMER_LAG_DETAILS',
|
||||||
|
TOGGLE_ALERT_STATE: 'TOGGLE_ALERT_STATE',
|
||||||
|
GET_ALL_ALLERTS: 'GET_ALL_ALLERTS',
|
||||||
|
REMOVE_ALERT_RULE: 'REMOVE_ALERT_RULE',
|
||||||
|
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
|
||||||
};
|
};
|
||||||
|
@ -22,6 +22,8 @@ const ROUTES = {
|
|||||||
EDIT_ALERTS: '/alerts/edit',
|
EDIT_ALERTS: '/alerts/edit',
|
||||||
LIST_ALL_ALERT: '/alerts',
|
LIST_ALL_ALERT: '/alerts',
|
||||||
ALERTS_NEW: '/alerts/new',
|
ALERTS_NEW: '/alerts/new',
|
||||||
|
ALERT_HISTORY: '/alerts/history',
|
||||||
|
ALERT_OVERVIEW: '/alerts/overview',
|
||||||
ALL_CHANNELS: '/settings/channels',
|
ALL_CHANNELS: '/settings/channels',
|
||||||
CHANNELS_NEW: '/settings/channels/new',
|
CHANNELS_NEW: '/settings/channels/new',
|
||||||
CHANNELS_EDIT: '/settings/channels/:id',
|
CHANNELS_EDIT: '/settings/channels/:id',
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
.alert-history {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
22
frontend/src/container/AlertHistory/AlertHistory.tsx
Normal file
22
frontend/src/container/AlertHistory/AlertHistory.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import './AlertHistory.styles.scss';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import Statistics from './Statistics/Statistics';
|
||||||
|
import Timeline from './Timeline/Timeline';
|
||||||
|
|
||||||
|
function AlertHistory(): JSX.Element {
|
||||||
|
const [totalCurrentTriggers, setTotalCurrentTriggers] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="alert-history">
|
||||||
|
<Statistics
|
||||||
|
totalCurrentTriggers={totalCurrentTriggers}
|
||||||
|
setTotalCurrentTriggers={setTotalCurrentTriggers}
|
||||||
|
/>
|
||||||
|
<Timeline totalCurrentTriggers={totalCurrentTriggers} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AlertHistory;
|
@ -0,0 +1,21 @@
|
|||||||
|
.alert-popover-trigger-action {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-history-popover {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.lightMode & {
|
||||||
|
background: var(--bg-vanilla-100) !important;
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-popover-arrow {
|
||||||
|
&::before {
|
||||||
|
.lightMode & {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
import './AlertPopover.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Popover } from 'antd';
|
||||||
|
import LogsIcon from 'assets/AlertHistory/LogsIcon';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { DraftingCompass } from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
relatedTracesLink?: string;
|
||||||
|
relatedLogsLink?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
relatedTracesLink,
|
||||||
|
relatedLogsLink,
|
||||||
|
}: {
|
||||||
|
relatedTracesLink?: Props['relatedTracesLink'];
|
||||||
|
relatedLogsLink?: Props['relatedLogsLink'];
|
||||||
|
}): JSX.Element {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
return (
|
||||||
|
<div className="contributor-row-popover-buttons">
|
||||||
|
{!!relatedLogsLink && (
|
||||||
|
<Link
|
||||||
|
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
|
||||||
|
className="contributor-row-popover-buttons__button"
|
||||||
|
>
|
||||||
|
<div className="icon">
|
||||||
|
<LogsIcon />
|
||||||
|
</div>
|
||||||
|
<div className="text">View Logs</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{!!relatedTracesLink && (
|
||||||
|
<Link
|
||||||
|
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
|
||||||
|
className="contributor-row-popover-buttons__button"
|
||||||
|
>
|
||||||
|
<div className="icon">
|
||||||
|
<DraftingCompass
|
||||||
|
size={14}
|
||||||
|
color={isDarkMode ? Color.BG_VANILLA_400 : Color.TEXT_INK_400}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text">View Traces</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PopoverContent.defaultProps = {
|
||||||
|
relatedTracesLink: '',
|
||||||
|
relatedLogsLink: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function AlertPopover({
|
||||||
|
children,
|
||||||
|
relatedTracesLink,
|
||||||
|
relatedLogsLink,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="alert-popover-trigger-action">
|
||||||
|
<Popover
|
||||||
|
showArrow={false}
|
||||||
|
placement="bottom"
|
||||||
|
color="linear-gradient(139deg, rgba(18, 19, 23, 1) 0%, rgba(18, 19, 23, 1) 98.68%)"
|
||||||
|
destroyTooltipOnHide
|
||||||
|
rootClassName="alert-history-popover"
|
||||||
|
content={
|
||||||
|
<PopoverContent
|
||||||
|
relatedTracesLink={relatedTracesLink}
|
||||||
|
relatedLogsLink={relatedLogsLink}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertPopover.defaultProps = {
|
||||||
|
relatedTracesLink: '',
|
||||||
|
relatedLogsLink: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConditionalAlertPopoverProps = {
|
||||||
|
relatedTracesLink: string;
|
||||||
|
relatedLogsLink: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
export function ConditionalAlertPopover({
|
||||||
|
children,
|
||||||
|
relatedTracesLink,
|
||||||
|
relatedLogsLink,
|
||||||
|
}: ConditionalAlertPopoverProps): JSX.Element {
|
||||||
|
if (relatedTracesLink || relatedLogsLink) {
|
||||||
|
return (
|
||||||
|
<AlertPopover
|
||||||
|
relatedTracesLink={relatedTracesLink}
|
||||||
|
relatedLogsLink={relatedLogsLink}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AlertPopover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div>{children}</div>;
|
||||||
|
}
|
||||||
|
export default AlertPopover;
|
@ -0,0 +1,28 @@
|
|||||||
|
import { AlertRuleStats } from 'types/api/alerts/def';
|
||||||
|
import { formatTime } from 'utils/timeUtils';
|
||||||
|
|
||||||
|
import StatsCard from '../StatsCard/StatsCard';
|
||||||
|
|
||||||
|
type TotalTriggeredCardProps = {
|
||||||
|
currentAvgResolutionTime: AlertRuleStats['currentAvgResolutionTime'];
|
||||||
|
pastAvgResolutionTime: AlertRuleStats['pastAvgResolutionTime'];
|
||||||
|
timeSeries: AlertRuleStats['currentAvgResolutionTimeSeries']['values'];
|
||||||
|
};
|
||||||
|
|
||||||
|
function AverageResolutionCard({
|
||||||
|
currentAvgResolutionTime,
|
||||||
|
pastAvgResolutionTime,
|
||||||
|
timeSeries,
|
||||||
|
}: TotalTriggeredCardProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<StatsCard
|
||||||
|
displayValue={formatTime(currentAvgResolutionTime)}
|
||||||
|
totalCurrentCount={currentAvgResolutionTime}
|
||||||
|
totalPastCount={pastAvgResolutionTime}
|
||||||
|
title="Avg. Resolution Time"
|
||||||
|
timeSeries={timeSeries}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AverageResolutionCard;
|
@ -0,0 +1,14 @@
|
|||||||
|
.statistics {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 280px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.statistics {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import './Statistics.styles.scss';
|
||||||
|
|
||||||
|
import { AlertRuleStats } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
import StatsCardsRenderer from './StatsCardsRenderer/StatsCardsRenderer';
|
||||||
|
import TopContributorsRenderer from './TopContributorsRenderer/TopContributorsRenderer';
|
||||||
|
|
||||||
|
function Statistics({
|
||||||
|
setTotalCurrentTriggers,
|
||||||
|
totalCurrentTriggers,
|
||||||
|
}: {
|
||||||
|
setTotalCurrentTriggers: (value: number) => void;
|
||||||
|
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="statistics">
|
||||||
|
<StatsCardsRenderer setTotalCurrentTriggers={setTotalCurrentTriggers} />
|
||||||
|
<TopContributorsRenderer totalCurrentTriggers={totalCurrentTriggers} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Statistics;
|
@ -0,0 +1,112 @@
|
|||||||
|
.stats-card {
|
||||||
|
width: 21.7%;
|
||||||
|
border-right: 1px solid var(--bg-slate-500);
|
||||||
|
padding: 9px 12px 13px;
|
||||||
|
|
||||||
|
&--empty {
|
||||||
|
justify-content: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.duration-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-slate-200);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__stats {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
.count-label {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__graph {
|
||||||
|
margin-top: 80px;
|
||||||
|
|
||||||
|
.graph {
|
||||||
|
width: 100%;
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-percentage {
|
||||||
|
width: max-content;
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&--success {
|
||||||
|
background: rgba(37, 225, 146, 0.1);
|
||||||
|
color: var(--bg-forest-500);
|
||||||
|
}
|
||||||
|
&--error {
|
||||||
|
background: rgba(229, 72, 77, 0.1);
|
||||||
|
color: var(--bg-cherry-500);
|
||||||
|
}
|
||||||
|
&--no-previous-data {
|
||||||
|
color: var(--text-robin-500);
|
||||||
|
background: rgba(78, 116, 248, 0.1);
|
||||||
|
padding: 4px 16px;
|
||||||
|
}
|
||||||
|
&__icon {
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
&__label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.stats-card {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
&__title-wrapper {
|
||||||
|
.title {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
.duration-indicator {
|
||||||
|
.text {
|
||||||
|
color: var(--text-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__stats {
|
||||||
|
.count-label {
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
import './StatsCard.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import { ArrowDownLeft, ArrowUpRight, Calendar } from 'lucide-react';
|
||||||
|
import { AlertRuleStats } from 'types/api/alerts/def';
|
||||||
|
import { calculateChange } from 'utils/calculateChange';
|
||||||
|
|
||||||
|
import StatsGraph from './StatsGraph/StatsGraph';
|
||||||
|
import {
|
||||||
|
convertTimestampToLocaleDateString,
|
||||||
|
extractDayFromTimestamp,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
type ChangePercentageProps = {
|
||||||
|
percentage: number;
|
||||||
|
direction: number;
|
||||||
|
duration: string | null;
|
||||||
|
};
|
||||||
|
function ChangePercentage({
|
||||||
|
percentage,
|
||||||
|
direction,
|
||||||
|
duration,
|
||||||
|
}: ChangePercentageProps): JSX.Element {
|
||||||
|
if (direction > 0) {
|
||||||
|
return (
|
||||||
|
<div className="change-percentage change-percentage--success">
|
||||||
|
<div className="change-percentage__icon">
|
||||||
|
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
|
||||||
|
</div>
|
||||||
|
<div className="change-percentage__label">
|
||||||
|
{percentage}% vs Last {duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (direction < 0) {
|
||||||
|
return (
|
||||||
|
<div className="change-percentage change-percentage--error">
|
||||||
|
<div className="change-percentage__icon">
|
||||||
|
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
|
||||||
|
</div>
|
||||||
|
<div className="change-percentage__label">
|
||||||
|
{percentage}% vs Last {duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="change-percentage change-percentage--no-previous-data">
|
||||||
|
<div className="change-percentage__label">no previous data</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatsCardProps = {
|
||||||
|
totalCurrentCount?: number;
|
||||||
|
totalPastCount?: number;
|
||||||
|
title: string;
|
||||||
|
isEmpty?: boolean;
|
||||||
|
emptyMessage?: string;
|
||||||
|
displayValue?: string | number;
|
||||||
|
timeSeries?: AlertRuleStats['currentTriggersSeries']['values'];
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatsCard({
|
||||||
|
displayValue,
|
||||||
|
totalCurrentCount,
|
||||||
|
totalPastCount,
|
||||||
|
title,
|
||||||
|
isEmpty,
|
||||||
|
emptyMessage,
|
||||||
|
timeSeries = [],
|
||||||
|
}: StatsCardProps): JSX.Element {
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
|
const relativeTime = urlQuery.get('relativeTime');
|
||||||
|
|
||||||
|
const { changePercentage, changeDirection } = calculateChange(
|
||||||
|
totalCurrentCount,
|
||||||
|
totalPastCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
const startTime = urlQuery.get(QueryParams.startTime);
|
||||||
|
const endTime = urlQuery.get(QueryParams.endTime);
|
||||||
|
|
||||||
|
let displayTime = relativeTime;
|
||||||
|
|
||||||
|
if (!displayTime && startTime && endTime) {
|
||||||
|
const formattedStartDate = extractDayFromTimestamp(startTime);
|
||||||
|
const formattedEndDate = extractDayFromTimestamp(endTime);
|
||||||
|
displayTime = `${formattedStartDate} to ${formattedEndDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!displayTime) {
|
||||||
|
displayTime = '';
|
||||||
|
}
|
||||||
|
const formattedStartTimeForTooltip = convertTimestampToLocaleDateString(
|
||||||
|
startTime,
|
||||||
|
);
|
||||||
|
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
|
||||||
|
<div className="stats-card__title-wrapper">
|
||||||
|
<div className="title">{title}</div>
|
||||||
|
<div className="duration-indicator">
|
||||||
|
<div className="icon">
|
||||||
|
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||||
|
</div>
|
||||||
|
{relativeTime ? (
|
||||||
|
<div className="text">{displayTime}</div>
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
title={`From ${formattedStartTimeForTooltip} to ${formattedEndTimeForTooltip}`}
|
||||||
|
>
|
||||||
|
<div className="text">{displayTime}</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stats-card__stats">
|
||||||
|
<div className="count-label">
|
||||||
|
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChangePercentage
|
||||||
|
direction={changeDirection}
|
||||||
|
percentage={changePercentage}
|
||||||
|
duration={relativeTime}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stats-card__graph">
|
||||||
|
<div className="graph">
|
||||||
|
{!isEmpty && timeSeries.length > 1 && (
|
||||||
|
<StatsGraph timeSeries={timeSeries} changeDirection={changeDirection} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatsCard.defaultProps = {
|
||||||
|
totalCurrentCount: 0,
|
||||||
|
totalPastCount: 0,
|
||||||
|
isEmpty: false,
|
||||||
|
emptyMessage: 'No Data',
|
||||||
|
displayValue: '',
|
||||||
|
timeSeries: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatsCard;
|
@ -0,0 +1,90 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { AlertRuleStats } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
timeSeries: AlertRuleStats['currentTriggersSeries']['values'];
|
||||||
|
changeDirection: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyle = (
|
||||||
|
changeDirection: number,
|
||||||
|
): { stroke: string; fill: string } => {
|
||||||
|
if (changeDirection === 0) {
|
||||||
|
return {
|
||||||
|
stroke: Color.BG_ROBIN_500,
|
||||||
|
fill: 'rgba(78, 116, 248, 0.20)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (changeDirection > 0) {
|
||||||
|
return {
|
||||||
|
stroke: Color.BG_FOREST_500,
|
||||||
|
fill: 'rgba(37, 225, 146, 0.20)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stroke: Color.BG_CHERRY_500,
|
||||||
|
fill: ' rgba(229, 72, 77, 0.20)',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatsGraph({ timeSeries, changeDirection }: Props): JSX.Element {
|
||||||
|
const { xData, yData } = useMemo(
|
||||||
|
() => ({
|
||||||
|
xData: timeSeries.map((item) => item.timestamp),
|
||||||
|
yData: timeSeries.map((item) => Number(item.value)),
|
||||||
|
}),
|
||||||
|
[timeSeries],
|
||||||
|
);
|
||||||
|
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
const options: uPlot.Options = useMemo(
|
||||||
|
() => ({
|
||||||
|
width: containerDimensions.width,
|
||||||
|
height: containerDimensions.height,
|
||||||
|
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
x: false,
|
||||||
|
y: false,
|
||||||
|
drag: {
|
||||||
|
x: false,
|
||||||
|
y: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
padding: [0, 0, 2, 0],
|
||||||
|
series: [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
...getStyle(changeDirection),
|
||||||
|
points: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
width: 1.4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
axes: [
|
||||||
|
{ show: false },
|
||||||
|
{
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[changeDirection, containerDimensions.height, containerDimensions.width],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||||
|
<Uplot data={[xData, yData]} options={options} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatsGraph;
|
@ -0,0 +1,12 @@
|
|||||||
|
export const extractDayFromTimestamp = (timestamp: string | null): string => {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
const date = new Date(parseInt(timestamp, 10));
|
||||||
|
return date.getDate().toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertTimestampToLocaleDateString = (
|
||||||
|
timestamp: string | null,
|
||||||
|
): string => {
|
||||||
|
if (!timestamp) return '';
|
||||||
|
return new Date(parseInt(timestamp, 10)).toLocaleString();
|
||||||
|
};
|
@ -0,0 +1,102 @@
|
|||||||
|
import { useGetAlertRuleDetailsStats } from 'pages/AlertDetails/hooks';
|
||||||
|
import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateRenderer';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import AverageResolutionCard from '../AverageResolutionCard/AverageResolutionCard';
|
||||||
|
import StatsCard from '../StatsCard/StatsCard';
|
||||||
|
import TotalTriggeredCard from '../TotalTriggeredCard/TotalTriggeredCard';
|
||||||
|
|
||||||
|
const hasTotalTriggeredStats = (
|
||||||
|
totalCurrentTriggers: number | string,
|
||||||
|
totalPastTriggers: number | string,
|
||||||
|
): boolean =>
|
||||||
|
(Number(totalCurrentTriggers) > 0 && Number(totalPastTriggers) > 0) ||
|
||||||
|
Number(totalCurrentTriggers) > 0;
|
||||||
|
|
||||||
|
const hasAvgResolutionTimeStats = (
|
||||||
|
currentAvgResolutionTime: number | string,
|
||||||
|
pastAvgResolutionTime: number | string,
|
||||||
|
): boolean =>
|
||||||
|
(Number(currentAvgResolutionTime) > 0 && Number(pastAvgResolutionTime) > 0) ||
|
||||||
|
Number(currentAvgResolutionTime) > 0;
|
||||||
|
|
||||||
|
type StatsCardsRendererProps = {
|
||||||
|
setTotalCurrentTriggers: (value: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(shaheer): render the DataStateRenderer inside the TotalTriggeredCard/AverageResolutionCard, it should display the title
|
||||||
|
function StatsCardsRenderer({
|
||||||
|
setTotalCurrentTriggers,
|
||||||
|
}: StatsCardsRendererProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isError,
|
||||||
|
data,
|
||||||
|
isValidRuleId,
|
||||||
|
ruleId,
|
||||||
|
} = useGetAlertRuleDetailsStats();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.payload?.data?.totalCurrentTriggers !== undefined) {
|
||||||
|
setTotalCurrentTriggers(data.payload.data.totalCurrentTriggers);
|
||||||
|
}
|
||||||
|
}, [data, setTotalCurrentTriggers]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataStateRenderer
|
||||||
|
isLoading={isLoading}
|
||||||
|
isRefetching={isRefetching}
|
||||||
|
isError={isError || !isValidRuleId || !ruleId}
|
||||||
|
data={data?.payload?.data || null}
|
||||||
|
>
|
||||||
|
{(data): JSX.Element => {
|
||||||
|
const {
|
||||||
|
currentAvgResolutionTime,
|
||||||
|
pastAvgResolutionTime,
|
||||||
|
totalCurrentTriggers,
|
||||||
|
totalPastTriggers,
|
||||||
|
currentAvgResolutionTimeSeries,
|
||||||
|
currentTriggersSeries,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hasTotalTriggeredStats(totalCurrentTriggers, totalPastTriggers) ? (
|
||||||
|
<TotalTriggeredCard
|
||||||
|
totalCurrentTriggers={totalCurrentTriggers}
|
||||||
|
totalPastTriggers={totalPastTriggers}
|
||||||
|
timeSeries={currentTriggersSeries?.values}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StatsCard
|
||||||
|
title="Total Triggered"
|
||||||
|
isEmpty
|
||||||
|
emptyMessage="None Triggered."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasAvgResolutionTimeStats(
|
||||||
|
currentAvgResolutionTime,
|
||||||
|
pastAvgResolutionTime,
|
||||||
|
) ? (
|
||||||
|
<AverageResolutionCard
|
||||||
|
currentAvgResolutionTime={currentAvgResolutionTime}
|
||||||
|
pastAvgResolutionTime={pastAvgResolutionTime}
|
||||||
|
timeSeries={currentAvgResolutionTimeSeries?.values}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StatsCard
|
||||||
|
title="Avg. Resolution Time"
|
||||||
|
isEmpty
|
||||||
|
emptyMessage="No Resolutions."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</DataStateRenderer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatsCardsRenderer;
|
@ -0,0 +1,213 @@
|
|||||||
|
.top-contributors-card {
|
||||||
|
width: 56.6%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&--view-all {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
.title {
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 22px;
|
||||||
|
letter-spacing: 0.52px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.view-all {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
height: 20px;
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contributors-row {
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
&__content {
|
||||||
|
.ant-table {
|
||||||
|
&-cell {
|
||||||
|
padding: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contributors-row {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
|
||||||
|
td {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
&:not(:last-of-type) td {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.total-contribution {
|
||||||
|
color: var(--text-robin-500);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.06px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(78, 116, 248, 0.1);
|
||||||
|
border-radius: 50px;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-content {
|
||||||
|
margin: 16px 12px;
|
||||||
|
padding: 40px 45px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
border: 1px dashed var(--bg-slate-500);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 26px;
|
||||||
|
letter-spacing: -0.103px;
|
||||||
|
}
|
||||||
|
&__text {
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
line-height: 18px;
|
||||||
|
.bold-text {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__button-wrapper {
|
||||||
|
margin-top: 12px;
|
||||||
|
.configure-alert-rule-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
border-width: 0;
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
line-height: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-popover-inner:has(.contributor-row-popover-buttons) {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.contributor-row-popover-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
width: 160px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.text,
|
||||||
|
.icon {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
|
||||||
|
.lightMode & {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
|
||||||
|
.text,
|
||||||
|
.icon {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
|
||||||
|
.lightMode & {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode & {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-all-drawer {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.ant-table {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-contributors-card {
|
||||||
|
&__header {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
.title {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
.view-all {
|
||||||
|
.label {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__content {
|
||||||
|
.contributors-row {
|
||||||
|
background: inherit;
|
||||||
|
&:not(:last-of-type) td {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.empty-content {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
&__text {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
.bold-text {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__button-wrapper {
|
||||||
|
.configure-alert-rule-button {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
import './TopContributorsCard.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import TopContributorsContent from './TopContributorsContent';
|
||||||
|
import { TopContributorsCardProps } from './types';
|
||||||
|
import ViewAllDrawer from './ViewAllDrawer';
|
||||||
|
|
||||||
|
function TopContributorsCard({
|
||||||
|
topContributorsData,
|
||||||
|
totalCurrentTriggers,
|
||||||
|
}: TopContributorsCardProps): JSX.Element {
|
||||||
|
const { search } = useLocation();
|
||||||
|
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
|
|
||||||
|
const viewAllTopContributorsParam = searchParams.get('viewAllTopContributors');
|
||||||
|
|
||||||
|
const [isViewAllVisible, setIsViewAllVisible] = useState(
|
||||||
|
!!viewAllTopContributorsParam ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const toggleViewAllParam = (isOpen: boolean): void => {
|
||||||
|
if (isOpen) {
|
||||||
|
searchParams.set('viewAllTopContributors', 'true');
|
||||||
|
} else {
|
||||||
|
searchParams.delete('viewAllTopContributors');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleViewAllDrawer = (): void => {
|
||||||
|
setIsViewAllVisible((prev) => {
|
||||||
|
const newState = !prev;
|
||||||
|
|
||||||
|
toggleViewAllParam(newState);
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
history.push({ search: searchParams.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="top-contributors-card">
|
||||||
|
<div className="top-contributors-card__header">
|
||||||
|
<div className="title">top contributors</div>
|
||||||
|
{topContributorsData.length > 3 && (
|
||||||
|
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
|
||||||
|
<div className="label">View all</div>
|
||||||
|
<div className="icon">
|
||||||
|
<ArrowRight
|
||||||
|
size={14}
|
||||||
|
color={isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TopContributorsContent
|
||||||
|
topContributorsData={topContributorsData}
|
||||||
|
totalCurrentTriggers={totalCurrentTriggers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isViewAllVisible && (
|
||||||
|
<ViewAllDrawer
|
||||||
|
isViewAllVisible={isViewAllVisible}
|
||||||
|
toggleViewAllDrawer={toggleViewAllDrawer}
|
||||||
|
totalCurrentTriggers={totalCurrentTriggers}
|
||||||
|
topContributorsData={topContributorsData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopContributorsCard;
|
@ -0,0 +1,32 @@
|
|||||||
|
import TopContributorsRows from './TopContributorsRows';
|
||||||
|
import { TopContributorsCardProps } from './types';
|
||||||
|
|
||||||
|
function TopContributorsContent({
|
||||||
|
topContributorsData,
|
||||||
|
totalCurrentTriggers,
|
||||||
|
}: TopContributorsCardProps): JSX.Element {
|
||||||
|
const isEmpty = !topContributorsData.length;
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
return (
|
||||||
|
<div className="empty-content">
|
||||||
|
<div className="empty-content__icon">ℹ️</div>
|
||||||
|
<div className="empty-content__text">
|
||||||
|
Top contributors highlight the most frequently triggering group-by
|
||||||
|
attributes in multi-dimensional alerts
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="top-contributors-card__content">
|
||||||
|
<TopContributorsRows
|
||||||
|
topContributors={topContributorsData.slice(0, 3)}
|
||||||
|
totalCurrentTriggers={totalCurrentTriggers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopContributorsContent;
|
@ -0,0 +1,87 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Progress, Table } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
||||||
|
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
|
||||||
|
import PaginationInfoText from 'periscope/components/PaginationInfoText/PaginationInfoText';
|
||||||
|
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
function TopContributorsRows({
|
||||||
|
topContributors,
|
||||||
|
totalCurrentTriggers,
|
||||||
|
}: {
|
||||||
|
topContributors: AlertRuleTopContributors[];
|
||||||
|
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
|
||||||
|
}): JSX.Element {
|
||||||
|
const columns: ColumnsType<AlertRuleTopContributors> = [
|
||||||
|
{
|
||||||
|
title: 'labels',
|
||||||
|
dataIndex: 'labels',
|
||||||
|
key: 'labels',
|
||||||
|
width: '51%',
|
||||||
|
render: (
|
||||||
|
labels: AlertRuleTopContributors['labels'],
|
||||||
|
record,
|
||||||
|
): JSX.Element => (
|
||||||
|
<ConditionalAlertPopover
|
||||||
|
relatedTracesLink={record.relatedTracesLink}
|
||||||
|
relatedLogsLink={record.relatedLogsLink}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<AlertLabels labels={labels} />
|
||||||
|
</div>
|
||||||
|
</ConditionalAlertPopover>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'progressBar',
|
||||||
|
dataIndex: 'count',
|
||||||
|
key: 'progressBar',
|
||||||
|
width: '39%',
|
||||||
|
render: (count: AlertRuleTopContributors['count'], record): JSX.Element => (
|
||||||
|
<ConditionalAlertPopover
|
||||||
|
relatedTracesLink={record.relatedTracesLink}
|
||||||
|
relatedLogsLink={record.relatedLogsLink}
|
||||||
|
>
|
||||||
|
<Progress
|
||||||
|
percent={(count / totalCurrentTriggers) * 100}
|
||||||
|
showInfo={false}
|
||||||
|
trailColor="rgba(255, 255, 255, 0)"
|
||||||
|
strokeColor={Color.BG_ROBIN_500}
|
||||||
|
/>
|
||||||
|
</ConditionalAlertPopover>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'count',
|
||||||
|
dataIndex: 'count',
|
||||||
|
key: 'count',
|
||||||
|
width: '10%',
|
||||||
|
render: (count: AlertRuleTopContributors['count'], record): JSX.Element => (
|
||||||
|
<ConditionalAlertPopover
|
||||||
|
relatedTracesLink={record.relatedTracesLink}
|
||||||
|
relatedLogsLink={record.relatedLogsLink}
|
||||||
|
>
|
||||||
|
<div className="total-contribution">
|
||||||
|
{count}/{totalCurrentTriggers}
|
||||||
|
</div>
|
||||||
|
</ConditionalAlertPopover>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
rowClassName="contributors-row"
|
||||||
|
rowKey={(row): string => `top-contributor-${row.fingerprint}`}
|
||||||
|
columns={columns}
|
||||||
|
showHeader={false}
|
||||||
|
dataSource={topContributors}
|
||||||
|
pagination={
|
||||||
|
topContributors.length > 10 ? { showTotal: PaginationInfoText } : false
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopContributorsRows;
|
@ -0,0 +1,46 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Drawer } from 'antd';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
import TopContributorsRows from './TopContributorsRows';
|
||||||
|
|
||||||
|
function ViewAllDrawer({
|
||||||
|
isViewAllVisible,
|
||||||
|
toggleViewAllDrawer,
|
||||||
|
totalCurrentTriggers,
|
||||||
|
topContributorsData,
|
||||||
|
}: {
|
||||||
|
isViewAllVisible: boolean;
|
||||||
|
toggleViewAllDrawer: () => void;
|
||||||
|
topContributorsData: AlertRuleTopContributors[];
|
||||||
|
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
|
||||||
|
}): JSX.Element {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
open={isViewAllVisible}
|
||||||
|
destroyOnClose
|
||||||
|
onClose={toggleViewAllDrawer}
|
||||||
|
placement="right"
|
||||||
|
width="50%"
|
||||||
|
className="view-all-drawer"
|
||||||
|
style={{
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||||
|
}}
|
||||||
|
title="Viewing All Contributors"
|
||||||
|
>
|
||||||
|
<div className="top-contributors-card--view-all">
|
||||||
|
<div className="top-contributors-card__content">
|
||||||
|
<TopContributorsRows
|
||||||
|
topContributors={topContributorsData}
|
||||||
|
totalCurrentTriggers={totalCurrentTriggers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ViewAllDrawer;
|
@ -0,0 +1,6 @@
|
|||||||
|
import { AlertRuleStats, AlertRuleTopContributors } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
export type TopContributorsCardProps = {
|
||||||
|
topContributorsData: AlertRuleTopContributors[];
|
||||||
|
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
|
||||||
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
import { useGetAlertRuleDetailsTopContributors } from 'pages/AlertDetails/hooks';
|
||||||
|
import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateRenderer';
|
||||||
|
import { AlertRuleStats } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
import TopContributorsCard from '../TopContributorsCard/TopContributorsCard';
|
||||||
|
|
||||||
|
type TopContributorsRendererProps = {
|
||||||
|
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
|
||||||
|
};
|
||||||
|
|
||||||
|
function TopContributorsRenderer({
|
||||||
|
totalCurrentTriggers,
|
||||||
|
}: TopContributorsRendererProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isError,
|
||||||
|
data,
|
||||||
|
isValidRuleId,
|
||||||
|
ruleId,
|
||||||
|
} = useGetAlertRuleDetailsTopContributors();
|
||||||
|
const response = data?.payload?.data;
|
||||||
|
|
||||||
|
// TODO(shaheer): render the DataStateRenderer inside the TopContributorsCard, it should display the title and view all
|
||||||
|
return (
|
||||||
|
<DataStateRenderer
|
||||||
|
isLoading={isLoading}
|
||||||
|
isRefetching={isRefetching}
|
||||||
|
isError={isError || !isValidRuleId || !ruleId}
|
||||||
|
data={response || null}
|
||||||
|
>
|
||||||
|
{(topContributorsData): JSX.Element => (
|
||||||
|
<TopContributorsCard
|
||||||
|
topContributorsData={topContributorsData}
|
||||||
|
totalCurrentTriggers={totalCurrentTriggers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DataStateRenderer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopContributorsRenderer;
|
@ -0,0 +1,26 @@
|
|||||||
|
import { AlertRuleStats } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
import StatsCard from '../StatsCard/StatsCard';
|
||||||
|
|
||||||
|
type TotalTriggeredCardProps = {
|
||||||
|
totalCurrentTriggers: AlertRuleStats['totalCurrentTriggers'];
|
||||||
|
totalPastTriggers: AlertRuleStats['totalPastTriggers'];
|
||||||
|
timeSeries: AlertRuleStats['currentTriggersSeries']['values'];
|
||||||
|
};
|
||||||
|
|
||||||
|
function TotalTriggeredCard({
|
||||||
|
totalCurrentTriggers,
|
||||||
|
totalPastTriggers,
|
||||||
|
timeSeries,
|
||||||
|
}: TotalTriggeredCardProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<StatsCard
|
||||||
|
totalCurrentCount={totalCurrentTriggers}
|
||||||
|
totalPastCount={totalPastTriggers}
|
||||||
|
title="Total Triggered"
|
||||||
|
timeSeries={timeSeries}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TotalTriggeredCard;
|
@ -0,0 +1,52 @@
|
|||||||
|
.timeline-graph {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
height: 150px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
width: max-content;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #1d212d;
|
||||||
|
background: rgba(29, 33, 45, 0.5);
|
||||||
|
color: #ebebeb;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.06px;
|
||||||
|
}
|
||||||
|
&__chart {
|
||||||
|
.chart-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 52px;
|
||||||
|
background: rgba(255, 255, 255, 0.1215686275);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
.chart-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.timeline-graph {
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
&__title {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
&__chart {
|
||||||
|
.chart-placeholder {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
184
frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx
Normal file
184
frontend/src/container/AlertHistory/Timeline/Graph/Graph.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
|
||||||
|
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { AlertRuleTimelineGraphResponse } from 'types/api/alerts/def';
|
||||||
|
import uPlot, { AlignedData } from 'uplot';
|
||||||
|
|
||||||
|
import { ALERT_STATUS, TIMELINE_OPTIONS } from './constants';
|
||||||
|
|
||||||
|
type Props = { type: string; data: AlertRuleTimelineGraphResponse[] };
|
||||||
|
|
||||||
|
function HorizontalTimelineGraph({
|
||||||
|
width,
|
||||||
|
isDarkMode,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
width: number;
|
||||||
|
isDarkMode: boolean;
|
||||||
|
data: AlertRuleTimelineGraphResponse[];
|
||||||
|
}): JSX.Element {
|
||||||
|
const transformedData: AlignedData = useMemo(() => {
|
||||||
|
if (!data?.length) {
|
||||||
|
return [[], []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a first and last entry to make sure the graph displays all the data
|
||||||
|
const FIVE_MINUTES_IN_SECONDS = 300;
|
||||||
|
|
||||||
|
const timestamps = [
|
||||||
|
data[0].start / 1000 - FIVE_MINUTES_IN_SECONDS, // 5 minutes before the first entry
|
||||||
|
...data.map((item) => item.start / 1000),
|
||||||
|
data[data.length - 1].end / 1000, // end value of last entry
|
||||||
|
];
|
||||||
|
|
||||||
|
const states = [
|
||||||
|
ALERT_STATUS[data[0].state], // Same state as the first entry
|
||||||
|
...data.map((item) => ALERT_STATUS[item.state]),
|
||||||
|
ALERT_STATUS[data[data.length - 1].state], // Same state as the last entry
|
||||||
|
];
|
||||||
|
|
||||||
|
return [timestamps, states];
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const options: uPlot.Options = useMemo(
|
||||||
|
() => ({
|
||||||
|
width,
|
||||||
|
height: 85,
|
||||||
|
cursor: { show: false },
|
||||||
|
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
gap: 10,
|
||||||
|
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
|
||||||
|
},
|
||||||
|
{ show: false },
|
||||||
|
],
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
padding: [null, 0, null, 0],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
label: 'Time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'States',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins:
|
||||||
|
transformedData?.length > 1
|
||||||
|
? [
|
||||||
|
timelinePlugin({
|
||||||
|
count: transformedData.length - 1,
|
||||||
|
...TIMELINE_OPTIONS,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
|
[width, isDarkMode, transformedData],
|
||||||
|
);
|
||||||
|
return <Uplot data={transformedData} options={options} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformVerticalTimelineGraph = (data: any[]): any => [
|
||||||
|
data.map((item: { timestamp: any }) => item.timestamp),
|
||||||
|
Array(data.length).fill(0),
|
||||||
|
Array(data.length).fill(10),
|
||||||
|
Array(data.length).fill([0, 1, 2, 3, 4, 5]),
|
||||||
|
data.map((item: { value: number }) => {
|
||||||
|
const count = Math.floor(item.value / 10);
|
||||||
|
return [...Array(count).fill(1), 2];
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const datatest: any[] = [];
|
||||||
|
const now = Math.floor(Date.now() / 1000); // current timestamp in seconds
|
||||||
|
const oneDay = 24 * 60 * 60; // one day in seconds
|
||||||
|
|
||||||
|
for (let i = 0; i < 90; i++) {
|
||||||
|
const timestamp = now - i * oneDay;
|
||||||
|
const startOfDay = timestamp - (timestamp % oneDay);
|
||||||
|
datatest.push({
|
||||||
|
timestamp: startOfDay,
|
||||||
|
value: Math.floor(Math.random() * 30) + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function VerticalTimelineGraph({
|
||||||
|
isDarkMode,
|
||||||
|
width,
|
||||||
|
}: {
|
||||||
|
width: number;
|
||||||
|
isDarkMode: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
|
const transformedData = useMemo(
|
||||||
|
() => transformVerticalTimelineGraph(datatest),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const options: uPlot.Options = useMemo(
|
||||||
|
() => ({
|
||||||
|
width,
|
||||||
|
height: 90,
|
||||||
|
plugins: [heatmapPlugin()],
|
||||||
|
cursor: { show: false },
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
gap: 10,
|
||||||
|
stroke: isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400,
|
||||||
|
},
|
||||||
|
{ show: false },
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
paths: (): null => null,
|
||||||
|
points: { show: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
paths: (): null => null,
|
||||||
|
points: { show: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[isDarkMode, width],
|
||||||
|
);
|
||||||
|
return <Uplot data={transformedData} options={options} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Graph({ type, data }: Props): JSX.Element | null {
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
if (type === 'horizontal') {
|
||||||
|
return (
|
||||||
|
<div ref={graphRef}>
|
||||||
|
<HorizontalTimelineGraph
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
width={containerDimensions.width}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div ref={graphRef}>
|
||||||
|
<VerticalTimelineGraph
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
width={containerDimensions.width}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Graph;
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
|
||||||
|
export const ALERT_STATUS: { [key: string]: number } = {
|
||||||
|
firing: 0,
|
||||||
|
inactive: 1,
|
||||||
|
normal: 1,
|
||||||
|
'no-data': 2,
|
||||||
|
disabled: 3,
|
||||||
|
muted: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STATE_VS_COLOR: {
|
||||||
|
[key: string]: { stroke: string; fill: string };
|
||||||
|
}[] = [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
0: { stroke: Color.BG_CHERRY_500, fill: Color.BG_CHERRY_500 },
|
||||||
|
1: { stroke: Color.BG_FOREST_500, fill: Color.BG_FOREST_500 },
|
||||||
|
2: { stroke: Color.BG_SIENNA_400, fill: Color.BG_SIENNA_400 },
|
||||||
|
3: { stroke: Color.BG_VANILLA_400, fill: Color.BG_VANILLA_400 },
|
||||||
|
4: { stroke: Color.BG_INK_100, fill: Color.BG_INK_100 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TIMELINE_OPTIONS = {
|
||||||
|
mode: 1,
|
||||||
|
fill: (seriesIdx: any, _: any, value: any): any =>
|
||||||
|
STATE_VS_COLOR[seriesIdx][value].fill,
|
||||||
|
stroke: (seriesIdx: any, _: any, value: any): any =>
|
||||||
|
STATE_VS_COLOR[seriesIdx][value].stroke,
|
||||||
|
laneWidthOption: 0.3,
|
||||||
|
showGrid: false,
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
import '../Graph/Graph.styles.scss';
|
||||||
|
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import { useGetAlertRuleDetailsTimelineGraphData } from 'pages/AlertDetails/hooks';
|
||||||
|
import DataStateRenderer from 'periscope/components/DataStateRenderer/DataStateRenderer';
|
||||||
|
|
||||||
|
import Graph from '../Graph/Graph';
|
||||||
|
|
||||||
|
function GraphWrapper({
|
||||||
|
totalCurrentTriggers,
|
||||||
|
}: {
|
||||||
|
totalCurrentTriggers: number;
|
||||||
|
}): JSX.Element {
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
|
const relativeTime = urlQuery.get('relativeTime');
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isError,
|
||||||
|
data,
|
||||||
|
isValidRuleId,
|
||||||
|
ruleId,
|
||||||
|
} = useGetAlertRuleDetailsTimelineGraphData();
|
||||||
|
|
||||||
|
// TODO(shaheer): uncomment when the API is ready for
|
||||||
|
// const { startTime } = useAlertHistoryQueryParams();
|
||||||
|
|
||||||
|
// const [isVerticalGraph, setIsVerticalGraph] = useState(false);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const checkVerticalGraph = (): void => {
|
||||||
|
// if (startTime) {
|
||||||
|
// const startTimeDate = dayjs(Number(startTime));
|
||||||
|
// const twentyFourHoursAgo = dayjs().subtract(
|
||||||
|
// HORIZONTAL_GRAPH_HOURS_THRESHOLD,
|
||||||
|
// DAYJS_MANIPULATE_TYPES.HOUR,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// setIsVerticalGraph(startTimeDate.isBefore(twentyFourHoursAgo));
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// checkVerticalGraph();
|
||||||
|
// }, [startTime]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="timeline-graph">
|
||||||
|
<div className="timeline-graph__title">
|
||||||
|
{totalCurrentTriggers} triggers in {relativeTime}
|
||||||
|
</div>
|
||||||
|
<div className="timeline-graph__chart">
|
||||||
|
<DataStateRenderer
|
||||||
|
isLoading={isLoading}
|
||||||
|
isError={isError || !isValidRuleId || !ruleId}
|
||||||
|
isRefetching={isRefetching}
|
||||||
|
data={data?.payload?.data || null}
|
||||||
|
>
|
||||||
|
{(data): JSX.Element => <Graph type="horizontal" data={data} />}
|
||||||
|
</DataStateRenderer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GraphWrapper;
|
@ -0,0 +1,123 @@
|
|||||||
|
.timeline-table {
|
||||||
|
border-top: 1px solid var(--text-slate-500);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 4px;
|
||||||
|
min-height: 600px;
|
||||||
|
.ant-table {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
&-cell {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
vertical-align: baseline;
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-thead > tr > th {
|
||||||
|
border-color: var(--bg-slate-500);
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12px 16px 8px !important;
|
||||||
|
}
|
||||||
|
&-tbody > tr > td {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-filter {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--text-ink-400);
|
||||||
|
border-width: 0;
|
||||||
|
line-height: 18px;
|
||||||
|
& ::placeholder {
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert-rule {
|
||||||
|
&-value,
|
||||||
|
&__created-at {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
}
|
||||||
|
&-value {
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
&__created-at {
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-table.ant-table-middle {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
border-left: 1px solid var(--bg-slate-500);
|
||||||
|
border-right: 1px solid var(--bg-slate-500);
|
||||||
|
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.ant-pagination-item {
|
||||||
|
&-active {
|
||||||
|
display: flex;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
& > a {
|
||||||
|
color: var(--text-ink-500);
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert-history-label-search {
|
||||||
|
.ant-select-selector {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.timeline-table {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.ant-table {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
&-thead {
|
||||||
|
& > tr > th {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ant-table-middle {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert-history-label-search {
|
||||||
|
.ant-select-selector {
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-rule {
|
||||||
|
&-value,
|
||||||
|
&-created-at {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-pagination-item {
|
||||||
|
&-active > a {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
frontend/src/container/AlertHistory/Timeline/Table/Table.tsx
Normal file
56
frontend/src/container/AlertHistory/Timeline/Table/Table.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import './Table.styles.scss';
|
||||||
|
|
||||||
|
import { Table } from 'antd';
|
||||||
|
import {
|
||||||
|
useGetAlertRuleDetailsTimelineTable,
|
||||||
|
useTimelineTable,
|
||||||
|
} from 'pages/AlertDetails/hooks';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { timelineTableColumns } from './useTimelineTable';
|
||||||
|
|
||||||
|
function TimelineTable(): JSX.Element {
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isError,
|
||||||
|
data,
|
||||||
|
isValidRuleId,
|
||||||
|
ruleId,
|
||||||
|
} = useGetAlertRuleDetailsTimelineTable();
|
||||||
|
|
||||||
|
const { timelineData, totalItems } = useMemo(() => {
|
||||||
|
const response = data?.payload?.data;
|
||||||
|
return {
|
||||||
|
timelineData: response?.items,
|
||||||
|
totalItems: response?.total,
|
||||||
|
};
|
||||||
|
}, [data?.payload?.data]);
|
||||||
|
|
||||||
|
const { paginationConfig, onChangeHandler } = useTimelineTable({
|
||||||
|
totalItems: totalItems ?? 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
if (isError || !isValidRuleId || !ruleId) {
|
||||||
|
return <div>{t('something_went_wrong')}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="timeline-table">
|
||||||
|
<Table
|
||||||
|
rowKey={(row): string => `${row.fingerprint}-${row.value}-${row.unixMilli}`}
|
||||||
|
columns={timelineTableColumns()}
|
||||||
|
dataSource={timelineData}
|
||||||
|
pagination={paginationConfig}
|
||||||
|
size="middle"
|
||||||
|
onChange={onChangeHandler}
|
||||||
|
loading={isLoading || isRefetching}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimelineTable;
|
@ -0,0 +1,9 @@
|
|||||||
|
import {
|
||||||
|
AlertRuleTimelineTableResponse,
|
||||||
|
AlertRuleTimelineTableResponsePayload,
|
||||||
|
} from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
export type TimelineTableProps = {
|
||||||
|
timelineData: AlertRuleTimelineTableResponse[];
|
||||||
|
totalItems: AlertRuleTimelineTableResponsePayload['data']['total'];
|
||||||
|
};
|
@ -0,0 +1,54 @@
|
|||||||
|
import { EllipsisOutlined } from '@ant-design/icons';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
||||||
|
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
|
||||||
|
import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
|
||||||
|
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
||||||
|
import { formatEpochTimestamp } from 'utils/timeUtils';
|
||||||
|
|
||||||
|
export const timelineTableColumns = (): ColumnsType<AlertRuleTimelineTableResponse> => [
|
||||||
|
{
|
||||||
|
title: 'STATE',
|
||||||
|
dataIndex: 'state',
|
||||||
|
sorter: true,
|
||||||
|
width: 140,
|
||||||
|
render: (value): JSX.Element => (
|
||||||
|
<div className="alert-rule-state">
|
||||||
|
<AlertState state={value} showLabel />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LABELS',
|
||||||
|
dataIndex: 'labels',
|
||||||
|
render: (labels): JSX.Element => (
|
||||||
|
<div className="alert-rule-labels">
|
||||||
|
<AlertLabels labels={labels} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CREATED AT',
|
||||||
|
dataIndex: 'unixMilli',
|
||||||
|
width: 200,
|
||||||
|
render: (value): JSX.Element => (
|
||||||
|
<div className="alert-rule__created-at">{formatEpochTimestamp(value)}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ACTIONS',
|
||||||
|
width: 140,
|
||||||
|
align: 'right',
|
||||||
|
render: (record): JSX.Element => (
|
||||||
|
<ConditionalAlertPopover
|
||||||
|
relatedTracesLink={record.relatedTracesLink}
|
||||||
|
relatedLogsLink={record.relatedLogsLink}
|
||||||
|
>
|
||||||
|
<Button type="text" ghost>
|
||||||
|
<EllipsisOutlined className="dropdown-icon" />
|
||||||
|
</Button>
|
||||||
|
</ConditionalAlertPopover>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
@ -0,0 +1,32 @@
|
|||||||
|
.timeline-tabs-and-filters {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.reset-button,
|
||||||
|
.top-5-contributors {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.coming-soon {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(173, 127, 88, 0.2);
|
||||||
|
background: rgba(173, 127, 88, 0.1);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
color: var(--text-sienna-400);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.05px;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
&__icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
import './TabsAndFilters.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { TimelineFilter, TimelineTab } from 'container/AlertHistory/types';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
import Tabs2 from 'periscope/components/Tabs2';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
function ComingSoon(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="coming-soon">
|
||||||
|
<div className="coming-soon__text">Coming Soon</div>
|
||||||
|
<div className="coming-soon__icon">
|
||||||
|
<Info size={10} color={Color.BG_SIENNA_400} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function TimelineTabs(): JSX.Element {
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
value: TimelineTab.OVERALL_STATUS,
|
||||||
|
label: 'Overall Status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TimelineTab.TOP_5_CONTRIBUTORS,
|
||||||
|
label: (
|
||||||
|
<div className="top-5-contributors">
|
||||||
|
Top 5 Contributors
|
||||||
|
<ComingSoon />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Tabs2 tabs={tabs} initialSelectedTab={TimelineTab.OVERALL_STATUS} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimelineFilters(): JSX.Element {
|
||||||
|
const { search } = useLocation();
|
||||||
|
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
|
|
||||||
|
const initialSelectedTab = useMemo(
|
||||||
|
() => searchParams.get('timelineFilter') ?? TimelineFilter.ALL,
|
||||||
|
[searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilter = (value: TimelineFilter): void => {
|
||||||
|
searchParams.set('timelineFilter', value);
|
||||||
|
history.push({ search: searchParams.toString() });
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
value: TimelineFilter.ALL,
|
||||||
|
label: 'All',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TimelineFilter.FIRED,
|
||||||
|
label: 'Fired',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TimelineFilter.RESOLVED,
|
||||||
|
label: 'Resolved',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs2
|
||||||
|
tabs={tabs}
|
||||||
|
initialSelectedTab={initialSelectedTab}
|
||||||
|
onSelectTab={handleFilter}
|
||||||
|
hasResetButton
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsAndFilters(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="timeline-tabs-and-filters">
|
||||||
|
<TimelineTabs />
|
||||||
|
<TimelineFilters />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabsAndFilters;
|
@ -0,0 +1,22 @@
|
|||||||
|
.timeline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 16px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.timeline {
|
||||||
|
&__title {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
frontend/src/container/AlertHistory/Timeline/Timeline.tsx
Normal file
32
frontend/src/container/AlertHistory/Timeline/Timeline.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import './Timeline.styles.scss';
|
||||||
|
|
||||||
|
import GraphWrapper from './GraphWrapper/GraphWrapper';
|
||||||
|
import TimelineTable from './Table/Table';
|
||||||
|
import TabsAndFilters from './TabsAndFilters/TabsAndFilters';
|
||||||
|
|
||||||
|
function TimelineTableRenderer(): JSX.Element {
|
||||||
|
return <TimelineTable />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Timeline({
|
||||||
|
totalCurrentTriggers,
|
||||||
|
}: {
|
||||||
|
totalCurrentTriggers: number;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="timeline">
|
||||||
|
<div className="timeline__title">Timeline</div>
|
||||||
|
<div className="timeline__tabs-and-filters">
|
||||||
|
<TabsAndFilters />
|
||||||
|
</div>
|
||||||
|
<div className="timeline__graph">
|
||||||
|
<GraphWrapper totalCurrentTriggers={totalCurrentTriggers} />
|
||||||
|
</div>
|
||||||
|
<div className="timeline__table">
|
||||||
|
<TimelineTableRenderer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Timeline;
|
@ -0,0 +1,2 @@
|
|||||||
|
// setting to 25 hours because we want to display the horizontal graph when the user selects 'Last 1 day' from date and time selector
|
||||||
|
export const HORIZONTAL_GRAPH_HOURS_THRESHOLD = 25;
|
1
frontend/src/container/AlertHistory/constants.ts
Normal file
1
frontend/src/container/AlertHistory/constants.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const TIMELINE_TABLE_PAGE_SIZE = 20;
|
3
frontend/src/container/AlertHistory/index.tsx
Normal file
3
frontend/src/container/AlertHistory/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import AlertHistory from './AlertHistory';
|
||||||
|
|
||||||
|
export default AlertHistory;
|
15
frontend/src/container/AlertHistory/types.ts
Normal file
15
frontend/src/container/AlertHistory/types.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export enum AlertDetailsTab {
|
||||||
|
OVERVIEW = 'OVERVIEW',
|
||||||
|
HISTORY = 'HISTORY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TimelineTab {
|
||||||
|
OVERALL_STATUS = 'OVERALL_STATUS',
|
||||||
|
TOP_5_CONTRIBUTORS = 'TOP_5_CONTRIBUTORS',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TimelineFilter {
|
||||||
|
ALL = 'ALL',
|
||||||
|
FIRED = 'FIRED',
|
||||||
|
RESOLVED = 'RESOLVED',
|
||||||
|
}
|
@ -78,6 +78,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const isCloudUserVal = isCloudUser();
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
const showAddCreditCardModal =
|
const showAddCreditCardModal =
|
||||||
|
isLoggedIn &&
|
||||||
isChatSupportEnabled &&
|
isChatSupportEnabled &&
|
||||||
isCloudUserVal &&
|
isCloudUserVal &&
|
||||||
!isPremiumChatSupportEnabled &&
|
!isPremiumChatSupportEnabled &&
|
||||||
@ -213,7 +214,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const pageTitle = t(routeKey);
|
const pageTitle = t(routeKey);
|
||||||
const renderFullScreen =
|
const renderFullScreen =
|
||||||
pathname === ROUTES.GET_STARTED ||
|
pathname === ROUTES.GET_STARTED ||
|
||||||
pathname === ROUTES.WORKSPACE_LOCKED ||
|
|
||||||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
||||||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
||||||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
||||||
@ -253,6 +253,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL';
|
routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL';
|
||||||
|
|
||||||
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
|
||||||
|
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
|
||||||
|
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
|
||||||
const isDashboardView = (): boolean => {
|
const isDashboardView = (): boolean => {
|
||||||
/**
|
/**
|
||||||
* need to match using regex here as the getRoute function will not work for
|
* need to match using regex here as the getRoute function will not work for
|
||||||
@ -279,6 +281,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
|
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: Right now we don't have a page-level method to pass the sidebar collapse state.
|
||||||
|
* Since the use case for overriding is not widely needed, we are setting it here
|
||||||
|
* so that the workspace locked page will have an expanded sidebar regardless of how users
|
||||||
|
* have set it or what is stored in localStorage. This will not affect the localStorage config.
|
||||||
|
*/
|
||||||
|
const isWorkspaceLocked = pathname === ROUTES.WORKSPACE_LOCKED;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
className={cx(
|
className={cx(
|
||||||
@ -323,7 +333,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
licenseData={licenseData}
|
licenseData={licenseData}
|
||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
onCollapse={onCollapse}
|
onCollapse={onCollapse}
|
||||||
collapsed={collapsed}
|
collapsed={isWorkspaceLocked ? false : collapsed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@ -341,6 +351,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
isDashboardView() ||
|
isDashboardView() ||
|
||||||
isDashboardWidgetView() ||
|
isDashboardWidgetView() ||
|
||||||
isDashboardListView() ||
|
isDashboardListView() ||
|
||||||
|
isAlertHistory() ||
|
||||||
|
isAlertOverview() ||
|
||||||
isMessagingQueues()
|
isMessagingQueues()
|
||||||
? 0
|
? 0
|
||||||
: '0 1rem',
|
: '0 1rem',
|
||||||
|
@ -19,6 +19,7 @@ import axios from 'axios';
|
|||||||
import cx from 'classnames';
|
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 { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@ -48,6 +49,7 @@ import {
|
|||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@ -61,7 +63,9 @@ import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
|||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
|
import { PreservedViewsTypes } from './constants';
|
||||||
import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
|
import ExplorerOptionsHideArea from './ExplorerOptionsHideArea';
|
||||||
|
import { PreservedViewsInLocalStorage } from './types';
|
||||||
import {
|
import {
|
||||||
DATASOURCE_VS_ROUTES,
|
DATASOURCE_VS_ROUTES,
|
||||||
generateRGBAFromHex,
|
generateRGBAFromHex,
|
||||||
@ -90,6 +94,12 @@ 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 isLogsExplorer = sourcepage === DataSource.LOGS;
|
||||||
|
|
||||||
|
const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS;
|
||||||
|
const PRESERVED_VIEW_TYPE = isLogsExplorer
|
||||||
|
? PreservedViewsTypes.LOGS
|
||||||
|
: PreservedViewsTypes.TRACES;
|
||||||
|
|
||||||
const onModalToggle = useCallback((value: boolean) => {
|
const onModalToggle = useCallback((value: boolean) => {
|
||||||
setIsExport(value);
|
setIsExport(value);
|
||||||
@ -107,7 +117,7 @@ function ExplorerOptions({
|
|||||||
logEvent('Traces Explorer: Save view clicked', {
|
logEvent('Traces Explorer: Save view clicked', {
|
||||||
panelType,
|
panelType,
|
||||||
});
|
});
|
||||||
} else if (sourcepage === DataSource.LOGS) {
|
} else if (isLogsExplorer) {
|
||||||
logEvent('Logs Explorer: Save view clicked', {
|
logEvent('Logs Explorer: Save view clicked', {
|
||||||
panelType,
|
panelType,
|
||||||
});
|
});
|
||||||
@ -141,7 +151,7 @@ function ExplorerOptions({
|
|||||||
logEvent('Traces Explorer: Create alert', {
|
logEvent('Traces Explorer: Create alert', {
|
||||||
panelType,
|
panelType,
|
||||||
});
|
});
|
||||||
} else if (sourcepage === DataSource.LOGS) {
|
} else if (isLogsExplorer) {
|
||||||
logEvent('Logs Explorer: Create alert', {
|
logEvent('Logs Explorer: Create alert', {
|
||||||
panelType,
|
panelType,
|
||||||
});
|
});
|
||||||
@ -166,7 +176,7 @@ function ExplorerOptions({
|
|||||||
logEvent('Traces Explorer: Add to dashboard clicked', {
|
logEvent('Traces Explorer: Add to dashboard clicked', {
|
||||||
panelType,
|
panelType,
|
||||||
});
|
});
|
||||||
} else if (sourcepage === DataSource.LOGS) {
|
} else if (isLogsExplorer) {
|
||||||
logEvent('Logs Explorer: Add to dashboard clicked', {
|
logEvent('Logs Explorer: Add to dashboard clicked', {
|
||||||
panelType,
|
panelType,
|
||||||
});
|
});
|
||||||
@ -265,6 +275,31 @@ function ExplorerOptions({
|
|||||||
[viewsData, handleExplorerTabChange],
|
[viewsData, handleExplorerTabChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updatePreservedViewInLocalStorage = (option: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}): void => {
|
||||||
|
// Retrieve stored views from local storage
|
||||||
|
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
|
||||||
|
|
||||||
|
// Initialize or parse the stored views
|
||||||
|
const updatedViews: PreservedViewsInLocalStorage = storedViews
|
||||||
|
? JSON.parse(storedViews)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// Update the views with the new selection
|
||||||
|
updatedViews[PRESERVED_VIEW_TYPE] = {
|
||||||
|
key: option.key,
|
||||||
|
value: option.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the updated views back to local storage
|
||||||
|
localStorage.setItem(
|
||||||
|
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
|
||||||
|
JSON.stringify(updatedViews),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelect = (
|
const handleSelect = (
|
||||||
value: string,
|
value: string,
|
||||||
option: { key: string; value: string },
|
option: { key: string; value: string },
|
||||||
@ -277,18 +312,42 @@ function ExplorerOptions({
|
|||||||
panelType,
|
panelType,
|
||||||
viewName: option?.value,
|
viewName: option?.value,
|
||||||
});
|
});
|
||||||
} else if (sourcepage === DataSource.LOGS) {
|
} else if (isLogsExplorer) {
|
||||||
logEvent('Logs Explorer: Select view', {
|
logEvent('Logs Explorer: Select view', {
|
||||||
panelType,
|
panelType,
|
||||||
viewName: option?.value,
|
viewName: option?.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePreservedViewInLocalStorage(option);
|
||||||
|
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.blur();
|
ref.current.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeCurrentViewFromLocalStorage = (): void => {
|
||||||
|
// Retrieve stored views from local storage
|
||||||
|
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (storedViews) {
|
||||||
|
// Parse the stored views
|
||||||
|
const parsedViews = JSON.parse(storedViews);
|
||||||
|
|
||||||
|
// Remove the current view type from the parsed views
|
||||||
|
delete parsedViews[PRESERVED_VIEW_TYPE];
|
||||||
|
|
||||||
|
// Update local storage with the modified views
|
||||||
|
localStorage.setItem(
|
||||||
|
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
|
||||||
|
JSON.stringify(parsedViews),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClearSelect = (): void => {
|
const handleClearSelect = (): void => {
|
||||||
|
removeCurrentViewFromLocalStorage();
|
||||||
|
|
||||||
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
|
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -323,7 +382,7 @@ function ExplorerOptions({
|
|||||||
panelType,
|
panelType,
|
||||||
viewName: newViewName,
|
viewName: newViewName,
|
||||||
});
|
});
|
||||||
} else if (sourcepage === DataSource.LOGS) {
|
} else if (isLogsExplorer) {
|
||||||
logEvent('Logs Explorer: Save view successful', {
|
logEvent('Logs Explorer: Save view successful', {
|
||||||
panelType,
|
panelType,
|
||||||
viewName: newViewName,
|
viewName: newViewName,
|
||||||
@ -358,6 +417,44 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
const isEditDeleteSupported = allowedRoles.includes(role as string);
|
const isEditDeleteSupported = allowedRoles.includes(role as string);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isRecentlyUsedSavedViewSelected,
|
||||||
|
setIsRecentlyUsedSavedViewSelected,
|
||||||
|
] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parsedPreservedView = JSON.parse(
|
||||||
|
localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
|
||||||
|
);
|
||||||
|
|
||||||
|
const preservedView = parsedPreservedView[PRESERVED_VIEW_TYPE] || {};
|
||||||
|
|
||||||
|
let timeoutId: string | number | NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!!preservedView?.key &&
|
||||||
|
viewsData?.data?.data &&
|
||||||
|
!(!!viewName || !!viewKey) &&
|
||||||
|
!isRecentlyUsedSavedViewSelected
|
||||||
|
) {
|
||||||
|
// prevent the race condition with useShareBuilderUrl
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
onMenuItemSelectHandler({ key: preservedView.key });
|
||||||
|
}, 0);
|
||||||
|
setIsRecentlyUsedSavedViewSelected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (): void => clearTimeout(timeoutId);
|
||||||
|
}, [
|
||||||
|
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
|
||||||
|
PRESERVED_VIEW_TYPE,
|
||||||
|
isRecentlyUsedSavedViewSelected,
|
||||||
|
onMenuItemSelectHandler,
|
||||||
|
viewKey,
|
||||||
|
viewName,
|
||||||
|
viewsData?.data?.data,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="explorer-options-container">
|
<div className="explorer-options-container">
|
||||||
{isQueryUpdated && !isExplorerOptionHidden && (
|
{isQueryUpdated && !isExplorerOptionHidden && (
|
||||||
@ -476,12 +573,12 @@ function ExplorerOptions({
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<div>
|
<div>
|
||||||
{sourcepage === DataSource.LOGS
|
{isLogsExplorer
|
||||||
? 'Learn more about Logs explorer '
|
? 'Learn more about Logs explorer '
|
||||||
: 'Learn more about Traces explorer '}
|
: 'Learn more about Traces explorer '}
|
||||||
<Typography.Link
|
<Typography.Link
|
||||||
href={
|
href={
|
||||||
sourcepage === DataSource.LOGS
|
isLogsExplorer
|
||||||
? 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar'
|
? 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar'
|
||||||
: 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar'
|
: 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar'
|
||||||
}
|
}
|
||||||
|
4
frontend/src/container/ExplorerOptions/constants.ts
Normal file
4
frontend/src/container/ExplorerOptions/constants.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum PreservedViewsTypes {
|
||||||
|
LOGS = 'logs',
|
||||||
|
TRACES = 'traces',
|
||||||
|
}
|
@ -8,6 +8,8 @@ import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
|||||||
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
|
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
|
||||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { PreservedViewsTypes } from './constants';
|
||||||
|
|
||||||
export interface SaveNewViewHandlerProps {
|
export interface SaveNewViewHandlerProps {
|
||||||
viewName: string;
|
viewName: string;
|
||||||
compositeQuery: ICompositeMetricQuery;
|
compositeQuery: ICompositeMetricQuery;
|
||||||
@ -26,3 +28,11 @@ export interface SaveNewViewHandlerProps {
|
|||||||
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
||||||
setNewViewName: Dispatch<SetStateAction<string>>;
|
setNewViewName: Dispatch<SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PreservedViewType =
|
||||||
|
| PreservedViewsTypes.LOGS
|
||||||
|
| PreservedViewsTypes.TRACES;
|
||||||
|
|
||||||
|
export type PreservedViewsInLocalStorage = Partial<
|
||||||
|
Record<PreservedViewType, { key: string; value: string }>
|
||||||
|
>;
|
||||||
|
@ -42,6 +42,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-btn {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
@ -19,6 +19,7 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
|||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||||
@ -369,7 +370,7 @@ function FormAlertRules({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// invalidate rule in cache
|
// invalidate rule in cache
|
||||||
ruleCache.invalidateQueries(['ruleId', ruleId]);
|
ruleCache.invalidateQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -22,6 +22,7 @@ import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
|||||||
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 { isDataAvailableByPanelType } from './utils';
|
||||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||||
|
|
||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
@ -34,6 +35,7 @@ function GridCardGraph({
|
|||||||
onClickHandler,
|
onClickHandler,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
customTooltipElement,
|
customTooltipElement,
|
||||||
|
dataAvailable,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
@ -180,6 +182,11 @@ function GridCardGraph({
|
|||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setErrorMessage(error.message);
|
setErrorMessage(error.message);
|
||||||
},
|
},
|
||||||
|
onSettled: (data) => {
|
||||||
|
dataAvailable?.(
|
||||||
|
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ export interface GridCardGraphProps {
|
|||||||
version?: string;
|
version?: string;
|
||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
customTooltipElement?: HTMLDivElement;
|
customTooltipElement?: HTMLDivElement;
|
||||||
|
dataAvailable?: (isDataAvailable: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import getLabelName from 'lib/getLabelName';
|
import getLabelName from 'lib/getLabelName';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { QueryData } from 'types/api/widgets/getQuery';
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
import { LegendEntryProps } from './FullView/types';
|
import { LegendEntryProps } from './FullView/types';
|
||||||
@ -131,3 +133,21 @@ export const toggleGraphsVisibilityInChart = ({
|
|||||||
lineChartRef?.current?.toggleGraph(index, showLegendData);
|
lineChartRef?.current?.toggleGraph(index, showLegendData);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isDataAvailableByPanelType = (
|
||||||
|
data?: MetricRangePayloadProps['data'],
|
||||||
|
panelType?: string,
|
||||||
|
): boolean => {
|
||||||
|
const getPanelData = (): any[] | undefined => {
|
||||||
|
switch (panelType) {
|
||||||
|
case PANEL_TYPES.TABLE:
|
||||||
|
return (data?.result?.[0] as any)?.table?.rows;
|
||||||
|
case PANEL_TYPES.LIST:
|
||||||
|
return data?.newResult?.data?.result?.[0]?.list as any[];
|
||||||
|
default:
|
||||||
|
return data?.result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Boolean(getPanelData()?.length);
|
||||||
|
};
|
||||||
|
@ -438,6 +438,10 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
: true,
|
: true,
|
||||||
[selectedDashboard],
|
[selectedDashboard],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let isDataAvailableInAnyWidget = false;
|
||||||
|
const isLogEventCalled = useRef<boolean>(false);
|
||||||
|
|
||||||
return isDashboardEmpty ? (
|
return isDashboardEmpty ? (
|
||||||
<DashboardEmptyState />
|
<DashboardEmptyState />
|
||||||
) : (
|
) : (
|
||||||
@ -468,6 +472,15 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
|
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
|
||||||
const rowWidgetProperties = currentPanelMap[id] || {};
|
const rowWidgetProperties = currentPanelMap[id] || {};
|
||||||
|
let { title } = currentWidget;
|
||||||
|
if (rowWidgetProperties.collapsed) {
|
||||||
|
const widgetCount = rowWidgetProperties.widgets?.length || 0;
|
||||||
|
const collapsedText = `(${widgetCount} widget${
|
||||||
|
widgetCount > 1 ? 's' : ''
|
||||||
|
})`;
|
||||||
|
title += ` ${collapsedText}`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer
|
<CardContainer
|
||||||
className="row-card"
|
className="row-card"
|
||||||
@ -485,9 +498,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
cursor="move"
|
cursor="move"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Typography.Text className="section-title">
|
<Typography.Text className="section-title">{title}</Typography.Text>
|
||||||
{currentWidget.title}
|
|
||||||
</Typography.Text>
|
|
||||||
{rowWidgetProperties.collapsed ? (
|
{rowWidgetProperties.collapsed ? (
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={14}
|
size={14}
|
||||||
@ -516,6 +527,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkIfDataExists = (isDataAvailable: boolean): void => {
|
||||||
|
if (!isDataAvailableInAnyWidget && isDataAvailable) {
|
||||||
|
isDataAvailableInAnyWidget = true;
|
||||||
|
}
|
||||||
|
if (!isLogEventCalled.current && isDataAvailableInAnyWidget) {
|
||||||
|
isLogEventCalled.current = true;
|
||||||
|
logEvent('Dashboard Detail: Panel data fetched', {
|
||||||
|
isDataAvailableInAnyWidget,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer
|
<CardContainer
|
||||||
className={isDashboardLocked ? '' : 'enable-resize'}
|
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||||
@ -534,6 +557,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
variables={variables}
|
variables={variables}
|
||||||
version={selectedDashboard?.data?.version}
|
version={selectedDashboard?.data?.version}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
|
dataAvailable={checkIfDataExists}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
|
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