diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c1aa885b6a..fd42658745 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,6 +2,6 @@ # Owners are automatically requested for review for PRs that changes code # that they own. * @ankitnayan -/frontend/ @palashgdev @pranshuchittora +/frontend/ @palashgdev /deploy/ @prashant-shahi **/query-service/ @srikanthccv diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 0f4a4b94db..b624a90b9f 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -7,12 +7,7 @@ jobs: lint-commits: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.1 + - uses: actions/checkout@v3 with: - # we actually need "github.event.pull_request.commits + 1" commit fetch-depth: 0 - - uses: actions/setup-node@v2.1.0 - # or just "yarn" if you depend on "@commitlint/cli" already - - run: yarn add @commitlint/cli - - run: yarn add @commitlint/config-conventional - - run: yarn run commitlint --config ./node_modules/@commitlint/config-conventional/index.js --from HEAD~${{ github.event.pull_request.commits }} --to HEAD + - uses: wagoid/commitlint-github-action@v5 diff --git a/.github/workflows/pr_verify_linked_issue.yml b/.github/workflows/pr_verify_linked_issue.yml index 3fd5abd2ec..a2442cc3a4 100644 --- a/.github/workflows/pr_verify_linked_issue.yml +++ b/.github/workflows/pr_verify_linked_issue.yml @@ -5,7 +5,7 @@ name: VerifyIssue on: pull_request: - types: [edited, synchronize, opened, reopened] + types: [edited, opened] check_run: jobs: @@ -14,6 +14,6 @@ jobs: name: Ensure Pull Request has a linked issue. steps: - name: Verify Linked Issue - uses: hattan/verify-linked-issue-action@v1.1.0 + uses: srikanthccv/verify-linked-issue-action@v0.70 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index d52cb94568..3627f21481 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ node_modules -yarn.lock -package.json deploy/docker/environment_tiny/common_test frontend/node_modules diff --git a/README.de-de.md b/README.de-de.md index 55dd7f4c22..6587756b9b 100644 --- a/README.de-de.md +++ b/README.de-de.md @@ -85,9 +85,9 @@ Hier findest du die vollständige Liste von unterstützten Programmiersprachen - ### Bereitstellung mit Docker -Bitte folge den [hier](https://signoz.io/docs/deployment/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen. +Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen. -Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/deployment/troubleshooting) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt. +Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshooting/) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.

 

diff --git a/README.md b/README.md index 70779f3de5..feae818ce6 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ SigNoz helps developers monitor applications and troubleshoot problems in their

- ## Join our Slack community @@ -78,7 +77,6 @@ Come say Hi to us on [Slack](https://signoz.io/slack) 👋

- ## Features: @@ -89,13 +87,12 @@ Come say Hi to us on [Slack](https://signoz.io/slack) 👋 - Filter traces by service name, operation, latency, error, tags/annotations. - Run aggregates on trace data (events/spans) to get business relevant metrics. e.g. You can get error rate and 99th percentile latency of `customer_type: gold` or `deployment_version: v2` or `external_call: paypal` - Native support for OpenTelemetry Logs, advanced log query builder, and automatic log collection from k8s cluster -- Lightening quick log analytics ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/)) +- Lightning quick log analytics ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/)) - End-to-End visibility into infrastructure performance, ingest metrics from all kinds of host environments - Easy to set alerts with DIY query builder

- ## Why SigNoz? @@ -124,15 +121,14 @@ You can find the complete list of languages here - https://opentelemetry.io/docs

- ## Getting Started ### Deploy using Docker -Please follow the steps listed [here](https://signoz.io/docs/deployment/docker/) to install using docker +Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker -The [troubleshooting instructions](https://signoz.io/docs/deployment/troubleshooting) may be helpful if you face any issues. +The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.

 

@@ -143,7 +139,6 @@ Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_cha

- ## Comparisons to Familiar Tools @@ -185,7 +180,6 @@ We have published benchmarks comparing Loki with SigNoz. Check it out [here](htt

- ## Contributing @@ -212,7 +206,6 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu

- ## Documentation @@ -220,7 +213,6 @@ You can find docs at https://signoz.io/docs/. If you need any clarification or f

- ## Community diff --git a/README.pt-br.md b/README.pt-br.md index ce168b4101..c817e8afb9 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -84,9 +84,9 @@ Você pode encontrar a lista completa de linguagens aqui - https://opentelemetry ### Implantar usando Docker -Siga as etapas listadas [aqui](https://signoz.io/docs/deployment/docker/) para instalar usando o Docker. +Siga as etapas listadas [aqui](https://signoz.io/docs/install/docker/) para instalar usando o Docker. -Esse [guia para solução de problemas](https://signoz.io/docs/deployment/troubleshooting) pode ser útil se você enfrentar quaisquer problemas. +Esse [guia para solução de problemas](https://signoz.io/docs/install/troubleshooting/) pode ser útil se você enfrentar quaisquer problemas.

 

diff --git a/README.zh-cn.md b/README.zh-cn.md index 3658eeb520..aaa89551bf 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -80,9 +80,9 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo ### 使用Docker部署 -请按照[这里](https://signoz.io/docs/deployment/docker/)列出的步骤使用Docker来安装 +请按照[这里](https://signoz.io/docs/install/docker/)列出的步骤使用Docker来安装 -如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/deployment/troubleshooting)会对你有帮助。 +如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/install/troubleshooting/)会对你有帮助。

 

diff --git a/deploy/docker-swarm/clickhouse-setup/clickhouse-storage.xml b/deploy/docker-swarm/clickhouse-setup/clickhouse-storage.xml index 2b2f4010ac..54ec4976f5 100644 --- a/deploy/docker-swarm/clickhouse-setup/clickhouse-storage.xml +++ b/deploy/docker-swarm/clickhouse-setup/clickhouse-storage.xml @@ -7,9 +7,21 @@ s3 - https://BUCKET-NAME.s3.amazonaws.com/data/ + + https://BUCKET-NAME.s3-REGION-NAME.amazonaws.com/data/ ACCESS-KEY-ID SECRET-ACCESS-KEY + + + + diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 26b87d18a6..5c34f979ad 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -34,7 +34,7 @@ x-clickhouse-depend: &clickhouse-depend services: zookeeper-1: - image: bitnami/zookeeper:3.7.0 + image: bitnami/zookeeper:3.7.1 hostname: zookeeper-1 user: root ports: @@ -124,7 +124,7 @@ services: # - ./data/clickhouse-3/:/var/lib/clickhouse/ alertmanager: - image: signoz/alertmanager:0.23.0-0.2 + image: signoz/alertmanager:0.23.1 volumes: - ./data/alertmanager:/data command: @@ -137,7 +137,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.18.3 + image: signoz/query-service:0.19.0 command: ["-config=/root/config/prometheus.yml"] # ports: # - "6060:6060" # pprof port @@ -166,7 +166,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:0.18.3 + image: signoz/frontend:0.19.0 deploy: restart_policy: condition: on-failure @@ -179,8 +179,8 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.66.7 - command: ["--config=/etc/otel-collector-config.yaml"] + image: signoz/signoz-otel-collector:0.76.1 + command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] user: root # required for reading docker container logs volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml @@ -208,8 +208,8 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:0.66.7 - command: ["--config=/etc/otel-collector-metrics-config.yaml"] + image: signoz/signoz-otel-collector:0.76.1 + command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml # ports: diff --git a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml index 61b937ea4b..e755936e8b 100644 --- a/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker-swarm/clickhouse-setup/otel-collector-config.yaml @@ -75,7 +75,7 @@ processors: timeout: 10s resourcedetection: # Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels. - detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure. + detectors: [env, system] # include ec2 for AWS, gcp for GCP and azure for Azure. timeout: 2s signozspanmetrics/prometheus: metrics_exporter: prometheus diff --git a/deploy/docker/clickhouse-setup/clickhouse-storage.xml b/deploy/docker/clickhouse-setup/clickhouse-storage.xml index 2b2f4010ac..54ec4976f5 100644 --- a/deploy/docker/clickhouse-setup/clickhouse-storage.xml +++ b/deploy/docker/clickhouse-setup/clickhouse-storage.xml @@ -7,9 +7,21 @@ s3 - https://BUCKET-NAME.s3.amazonaws.com/data/ + + https://BUCKET-NAME.s3-REGION-NAME.amazonaws.com/data/ ACCESS-KEY-ID SECRET-ACCESS-KEY + + + + diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index 80bd4a9890..e7b598ae38 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -27,7 +27,7 @@ services: alertmanager: container_name: alertmanager - image: signoz/alertmanager:0.23.0-0.2 + image: signoz/alertmanager:0.23.1 volumes: - ./data/alertmanager:/data depends_on: @@ -41,8 +41,8 @@ services: # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` otel-collector: container_name: otel-collector - image: signoz/signoz-otel-collector:0.66.7 - command: ["--config=/etc/otel-collector-config.yaml"] + image: signoz/signoz-otel-collector:0.76.1 + command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] # user: root # required for reading docker container logs volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml @@ -67,8 +67,8 @@ services: otel-collector-metrics: container_name: otel-collector-metrics - image: signoz/signoz-otel-collector:0.66.7 - command: ["--config=/etc/otel-collector-metrics-config.yaml"] + image: signoz/signoz-otel-collector:0.76.1 + command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml # ports: diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 88923daac6..6b9006f876 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -34,9 +34,9 @@ x-clickhouse-depend: &clickhouse-depend # condition: service_healthy services: - + zookeeper-1: - image: bitnami/zookeeper:3.7.0 + image: bitnami/zookeeper:3.7.1 container_name: zookeeper-1 hostname: zookeeper-1 user: root @@ -120,7 +120,7 @@ services: # - ./data/clickhouse-2/:/var/lib/clickhouse/ # - ./user_scripts:/var/lib/clickhouse/user_scripts/ - + # clickhouse-3: # <<: *clickhouse-defaults # container_name: clickhouse-3 @@ -139,7 +139,7 @@ services: # - ./user_scripts:/var/lib/clickhouse/user_scripts/ alertmanager: - image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.0-0.2} + image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.1} volumes: - ./data/alertmanager:/data depends_on: @@ -150,10 +150,10 @@ services: - --queryService.url=http://query-service:8085 - --storage.path=/data -# 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: - image: signoz/query-service:${DOCKER_TAG:-0.18.3} + image: signoz/query-service:${DOCKER_TAG:-0.19.0} container_name: query-service command: ["-config=/root/config/prometheus.yml"] # ports: @@ -181,7 +181,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.18.3} + image: signoz/frontend:${DOCKER_TAG:-0.19.0} container_name: frontend restart: on-failure depends_on: @@ -193,8 +193,8 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.7} - command: ["--config=/etc/otel-collector-config.yaml"] + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1} + command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] user: root # required for reading docker container logs volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml @@ -219,8 +219,8 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.7} - command: ["--config=/etc/otel-collector-metrics-config.yaml"] + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1} + command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml # ports: diff --git a/deploy/docker/clickhouse-setup/otel-collector-config.yaml b/deploy/docker/clickhouse-setup/otel-collector-config.yaml index 409580696a..c331f3a032 100644 --- a/deploy/docker/clickhouse-setup/otel-collector-config.yaml +++ b/deploy/docker/clickhouse-setup/otel-collector-config.yaml @@ -70,6 +70,40 @@ receivers: processors: + logstransform/internal: + operators: + - type: trace_parser + if: '"trace_id" in attributes or "span_id" in attributes' + trace_id: + parse_from: attributes.trace_id + span_id: + parse_from: attributes.span_id + output: remove_trace_id + - type: trace_parser + if: '"traceId" in attributes or "spanId" in attributes' + trace_id: + parse_from: attributes.traceId + span_id: + parse_from: attributes.spanId + output: remove_traceId + - id: remove_traceId + type: remove + if: '"traceId" in attributes' + field: attributes.traceId + output: remove_spanId + - id: remove_spanId + type: remove + if: '"spanId" in attributes' + field: attributes.spanId + - id: remove_trace_id + type: remove + if: '"trace_id" in attributes' + field: attributes.trace_id + output: remove_span_id + - id: remove_span_id + type: remove + if: '"span_id" in attributes' + field: attributes.span_id batch: send_batch_size: 10000 send_batch_max_size: 11000 @@ -104,7 +138,7 @@ processors: # retry_on_failure: true resourcedetection: # Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels. - detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure. + detectors: [env, system] # include ec2 for AWS, gcp for GCP and azure for Azure. timeout: 2s extensions: @@ -172,5 +206,5 @@ service: exporters: [prometheus] logs: receivers: [otlp, filelog/dockercontainers] - processors: [batch] + processors: [logstransform/internal, batch] exporters: [clickhouselogsexporter] \ No newline at end of file diff --git a/deploy/install.sh b/deploy/install.sh index e8a14a5821..e908dd8952 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -125,7 +125,7 @@ check_ports_occupied() { echo "+++++++++++ ERROR ++++++++++++++++++++++" echo "SigNoz requires ports 3301 & 4317 to be open. Please shut down any other service(s) that may be running on these ports." - echo "You can run SigNoz on another port following this guide https://signoz.io/docs/deployment/docker#troubleshooting" + echo "You can run SigNoz on another port following this guide https://signoz.io/docs/install/troubleshooting/" echo "++++++++++++++++++++++++++++++++++++++++" echo "" exit 1 @@ -249,7 +249,7 @@ bye() { # Prints a friendly good bye message and exits the script. echo "" echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a" - # echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker#troubleshooting" + echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/" echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack" echo "++++++++++++++++++++++++++++++++++++++++" @@ -500,7 +500,7 @@ if [[ $status_code -ne 200 ]]; then echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a" - echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker/#troubleshooting-of-common-issues" + echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/" echo "or reach us on SigNoz for support https://signoz.io/slack" echo "++++++++++++++++++++++++++++++++++++++++" diff --git a/ee/query-service/app/api/featureFlags.go b/ee/query-service/app/api/featureFlags.go index 9c979d17ba..63b36d45c4 100644 --- a/ee/query-service/app/api/featureFlags.go +++ b/ee/query-service/app/api/featureFlags.go @@ -5,6 +5,10 @@ import ( ) func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { - featureSet := ah.FF().GetFeatureFlags() + featureSet, err := ah.FF().GetFeatureFlags() + if err != nil { + ah.HandleError(w, err, http.StatusInternalServerError) + return + } ah.Respond(w, featureSet) } diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 72091cc1e3..315a211f9f 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -22,6 +22,8 @@ import ( "go.signoz.io/signoz/ee/query-service/app/db" "go.signoz.io/signoz/ee/query-service/dao" "go.signoz.io/signoz/ee/query-service/interfaces" + baseInterface "go.signoz.io/signoz/pkg/query-service/interfaces" + licensepkg "go.signoz.io/signoz/ee/query-service/license" "go.signoz.io/signoz/ee/query-service/usage" @@ -126,7 +128,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { serverOptions.RuleRepoURL, localDB, reader, - serverOptions.DisableRules) + serverOptions.DisableRules, + lm) if err != nil { return nil, err @@ -402,7 +405,7 @@ func setTimeoutMiddleware(next http.Handler) http.Handler { // check if route is not excluded url := r.URL.Path if _, ok := baseconst.TimeoutExcludedRoutes[url]; !ok { - ctx, cancel = context.WithTimeout(r.Context(), baseconst.ContextTimeout*time.Second) + ctx, cancel = context.WithTimeout(r.Context(), baseconst.ContextTimeout) defer cancel() } @@ -544,7 +547,8 @@ func makeRulesManager( ruleRepoURL string, db *sqlx.DB, ch baseint.Reader, - disableRules bool) (*rules.Manager, error) { + disableRules bool, + fm baseInterface.FeatureLookup) (*rules.Manager, error) { // create engine pqle, err := pqle.FromConfigPath(promConfigPath) @@ -571,6 +575,7 @@ func makeRulesManager( Context: context.Background(), Logger: nil, DisableRules: disableRules, + FeatureFlags: fm, } // create Manager diff --git a/ee/query-service/license/db.go b/ee/query-service/license/db.go index a82f0377e2..8d2f7065ff 100644 --- a/ee/query-service/license/db.go +++ b/ee/query-service/license/db.go @@ -2,6 +2,7 @@ package license import ( "context" + "database/sql" "fmt" "time" @@ -9,6 +10,7 @@ import ( "go.signoz.io/signoz/ee/query-service/license/sqlite" "go.signoz.io/signoz/ee/query-service/model" + basemodel "go.signoz.io/signoz/pkg/query-service/model" "go.uber.org/zap" ) @@ -125,3 +127,79 @@ func (r *Repo) UpdatePlanDetails(ctx context.Context, return nil } + +func (r *Repo) CreateFeature(req *basemodel.Feature) *basemodel.ApiError { + + _, err := r.db.Exec( + `INSERT INTO feature_status (name, active, usage, usage_limit, route) + VALUES (?, ?, ?, ?, ?);`, + req.Name, req.Active, req.Usage, req.UsageLimit, req.Route) + if err != nil { + return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err} + } + return nil +} + +func (r *Repo) GetFeature(featureName string) (basemodel.Feature, error) { + + var feature basemodel.Feature + + err := r.db.Get(&feature, + `SELECT * FROM feature_status WHERE name = ?;`, featureName) + if err != nil { + return feature, err + } + if feature.Name == "" { + return feature, basemodel.ErrFeatureUnavailable{Key: featureName} + } + return feature, nil +} + +func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) { + + var feature []basemodel.Feature + + err := r.db.Select(&feature, + `SELECT * FROM feature_status;`) + if err != nil { + return feature, err + } + + return feature, nil +} + +func (r *Repo) UpdateFeature(req basemodel.Feature) error { + + _, err := r.db.Exec( + `UPDATE feature_status SET active = ?, usage = ?, usage_limit = ?, route = ? WHERE name = ?;`, + req.Active, req.Usage, req.UsageLimit, req.Route, req.Name) + if err != nil { + return err + } + return nil +} + +func (r *Repo) InitFeatures(req basemodel.FeatureSet) error { + // get a feature by name, if it doesn't exist, create it. If it does exist, update it. + for _, feature := range req { + currentFeature, err := r.GetFeature(feature.Name) + if err != nil && err == sql.ErrNoRows { + err := r.CreateFeature(&feature) + if err != nil { + return err + } + continue + } else if err != nil { + return err + } + feature.Usage = currentFeature.Usage + if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 { + feature.Active = false + } + err = r.UpdateFeature(feature) + if err != nil { + return err + } + } + return nil +} diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go index a3e9ba0771..7a1be5118f 100644 --- a/ee/query-service/license/manager.go +++ b/ee/query-service/license/manager.go @@ -96,6 +96,11 @@ func (lm *Manager) SetActive(l *model.License) { lm.activeFeatures = l.FeatureSet // set default features setDefaultFeatures(lm) + + err := lm.InitFeatures(lm.activeFeatures) + if err != nil { + zap.S().Panicf("Couldn't activate features: %v", err) + } if !lm.validatorRunning { // we want to make sure only one validator runs, // we already have lock() so good to go @@ -106,9 +111,7 @@ func (lm *Manager) SetActive(l *model.License) { } func setDefaultFeatures(lm *Manager) { - for k, v := range baseconstants.DEFAULT_FEATURE_SET { - lm.activeFeatures[k] = v - } + lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...) } // LoadActiveLicense loads the most recent active license @@ -123,8 +126,13 @@ func (lm *Manager) LoadActiveLicense() error { } else { zap.S().Info("No active license found, defaulting to basic plan") // if no active license is found, we default to basic(free) plan with all default features - lm.activeFeatures = basemodel.BasicPlan + lm.activeFeatures = model.BasicPlan setDefaultFeatures(lm) + err := lm.InitFeatures(lm.activeFeatures) + if err != nil { + zap.S().Error("Couldn't initialize features: ", err) + return err + } } return nil @@ -291,18 +299,31 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m // CheckFeature will be internally used by backend routines // for feature gating func (lm *Manager) CheckFeature(featureKey string) error { - if value, ok := lm.activeFeatures[featureKey]; ok { - if value { - return nil - } - return basemodel.ErrFeatureUnavailable{Key: featureKey} + feature, err := lm.repo.GetFeature(featureKey) + if err != nil { + return err + } + if feature.Active { + return nil } return basemodel.ErrFeatureUnavailable{Key: featureKey} } // GetFeatureFlags returns current active features -func (lm *Manager) GetFeatureFlags() basemodel.FeatureSet { - return lm.activeFeatures +func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) { + return lm.repo.GetAllFeatures() +} + +func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error { + return lm.repo.InitFeatures(features) +} + +func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error { + return lm.repo.UpdateFeature(feature) +} + +func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) { + return lm.repo.GetFeature(key) } // GetRepo return the license repo diff --git a/ee/query-service/license/sqlite/init.go b/ee/query-service/license/sqlite/init.go index a03153659c..e500ddb4aa 100644 --- a/ee/query-service/license/sqlite/init.go +++ b/ee/query-service/license/sqlite/init.go @@ -2,6 +2,7 @@ package sqlite import ( "fmt" + "github.com/jmoiron/sqlx" ) @@ -33,5 +34,19 @@ func InitDB(db *sqlx.DB) error { if err != nil { return fmt.Errorf("Error in creating licenses table: %s", err.Error()) } + + table_schema = `CREATE TABLE IF NOT EXISTS feature_status ( + name TEXT PRIMARY KEY, + active bool, + usage INTEGER DEFAULT 0, + usage_limit INTEGER DEFAULT 0, + route TEXT + );` + + _, err = db.Exec(table_schema) + if err != nil { + return fmt.Errorf("Error in creating feature_status table: %s", err.Error()) + } + return nil } diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go index c42712f693..52ebd5c5b5 100644 --- a/ee/query-service/model/plans.go +++ b/ee/query-service/model/plans.go @@ -11,21 +11,143 @@ const Enterprise = "ENTERPRISE_PLAN" const DisableUpsell = "DISABLE_UPSELL" var BasicPlan = basemodel.FeatureSet{ - Basic: true, - SSO: false, - DisableUpsell: false, + basemodel.Feature{ + Name: SSO, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.OSS, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: DisableUpsell, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.SmartTraceDetail, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.CustomMetricsFunction, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.QueryBuilderPanels, + Active: true, + Usage: 0, + UsageLimit: 5, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.QueryBuilderAlerts, + Active: true, + Usage: 0, + UsageLimit: 5, + Route: "", + }, } var ProPlan = basemodel.FeatureSet{ - Pro: true, - SSO: true, - basemodel.SmartTraceDetail: true, - basemodel.CustomMetricsFunction: true, + basemodel.Feature{ + Name: SSO, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.OSS, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.SmartTraceDetail, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.CustomMetricsFunction, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.QueryBuilderPanels, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.QueryBuilderAlerts, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, } var EnterprisePlan = basemodel.FeatureSet{ - Enterprise: true, - SSO: true, - basemodel.SmartTraceDetail: true, - basemodel.CustomMetricsFunction: true, + basemodel.Feature{ + Name: SSO, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.OSS, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.SmartTraceDetail, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.CustomMetricsFunction, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.QueryBuilderPanels, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, + basemodel.Feature{ + Name: basemodel.QueryBuilderAlerts, + Active: true, + Usage: 0, + UsageLimit: -1, + Route: "", + }, } diff --git a/frontend/.babelrc b/frontend/.babelrc index aa9ef302a0..9efe6ca907 100644 --- a/frontend/.babelrc +++ b/frontend/.babelrc @@ -1,7 +1,7 @@ { "presets": [ "@babel/preset-env", - "@babel/preset-react", + ["@babel/preset-react", { "runtime": "automatic" }], "@babel/preset-typescript" ], "plugins": [ diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 37c7b17c45..540b1ded70 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -16,6 +16,7 @@ module.exports = { 'plugin:sonarjs/recommended', 'plugin:import/errors', 'plugin:import/warnings', + 'plugin:react/jsx-runtime', ], parser: '@typescript-eslint/parser', parserOptions: { diff --git a/frontend/.yarnrc b/frontend/.yarnrc index 788570fcd5..843c88f028 100644 --- a/frontend/.yarnrc +++ b/frontend/.yarnrc @@ -1 +1,2 @@ network-timeout 600000 +save-prefix "" diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 145f205a9d..cf9b792cf1 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,5 +1,5 @@ # Builder stage -FROM node:16.15.0-slim as builder +FROM node:16.15.0 as builder # Add Maintainer Info LABEL maintainer="signoz" @@ -11,6 +11,8 @@ WORKDIR /frontend # Copy the package.json and .yarnrc files prior to install dependencies COPY package.json ./ +# Copy lock file +COPY yarn.lock ./ COPY .yarnrc ./ # Install the dependencies and make the folder diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index b3b8061422..c9441402d9 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -1,5 +1,20 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable object-shorthand */ +/* eslint-disable func-names */ + /** * Adds custom matchers from the react testing library to all tests */ import '@testing-library/jest-dom'; import 'jest-styled-components'; + +// Mock window.matchMedia +window.matchMedia = + window.matchMedia || + function (): any { + return { + matches: false, + addListener: function () {}, + removeListener: function () {}, + }; + }; diff --git a/frontend/package.json b/frontend/package.json index 24e883bb8b..f26a92c9be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,15 +48,11 @@ "cross-env": "^7.0.3", "css-loader": "4.3.0", "css-minimizer-webpack-plugin": "^3.2.0", - "d3": "^6.2.0", - "d3-flame-graph": "^3.1.1", - "d3-tip": "^0.9.1", "dayjs": "^1.10.7", "dompurify": "3.0.0", "dotenv": "8.2.0", "event-source-polyfill": "1.0.31", "file-loader": "6.1.1", - "flat": "^5.0.2", "fontfaceobserver": "2.3.0", "history": "4.10.1", "html-webpack-plugin": "5.1.0", @@ -69,10 +65,10 @@ "less-loader": "^10.2.0", "lodash-es": "^4.17.21", "mini-css-extract-plugin": "2.4.5", + "papaparse": "5.4.1", "react": "18.2.0", "react-dom": "18.2.0", "react-force-graph": "^1.41.0", - "react-graph-vis": "^1.0.5", "react-grid-layout": "^1.3.4", "react-i18next": "^11.16.1", "react-intersection-observer": "9.4.1", @@ -126,16 +122,14 @@ "@types/color": "^3.0.3", "@types/compression-webpack-plugin": "^9.0.0", "@types/copy-webpack-plugin": "^8.0.1", - "@types/d3": "^6.2.0", - "@types/d3-tip": "^3.5.5", "@types/dompurify": "^2.4.0", "@types/event-source-polyfill": "^1.0.0", - "@types/flat": "^5.0.2", "@types/fontfaceobserver": "2.1.0", "@types/jest": "^27.5.1", "@types/lodash-es": "^4.17.4", "@types/mini-css-extract-plugin": "^2.5.1", "@types/node": "^16.10.3", + "@types/papaparse": "5.3.7", "@types/react": "18.0.26", "@types/react-dom": "18.0.10", "@types/react-grid-layout": "^1.1.2", @@ -144,7 +138,6 @@ "@types/react-router-dom": "^5.1.6", "@types/styled-components": "^5.1.4", "@types/uuid": "^8.3.1", - "@types/vis": "^4.21.21", "@types/webpack": "^5.28.0", "@types/webpack-dev-server": "^4.3.0", "@typescript-eslint/eslint-plugin": "^4.28.2", @@ -174,7 +167,6 @@ "is-ci": "^3.0.1", "jest-playwright-preset": "^1.7.0", "jest-styled-components": "^7.0.8", - "less-plugin-npm-import": "^2.1.0", "lint-staged": "^12.3.7", "portfinder-sync": "^0.0.2", "prettier": "2.2.1", diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index 82d6be2620..ddfb072d02 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -7,7 +7,7 @@ import { LOCALSTORAGE } from 'constants/localStorage'; import ROUTES from 'constants/routes'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useEffect, useMemo } from 'react'; +import { ReactChild, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { matchPath, Redirect, useLocation } from 'react-router-dom'; @@ -161,7 +161,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { } interface PrivateRouteProps { - children: React.ReactChild; + children: ReactChild; } export default PrivateRoute; diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index edfe843882..a4ccb0d0f0 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -7,7 +7,7 @@ import { NotificationProvider } from 'hooks/useNotifications'; import { ResourceProvider } from 'hooks/useResourceAttribute'; import history from 'lib/history'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; -import React, { Suspense } from 'react'; +import { Suspense } from 'react'; import { Route, Router, Switch } from 'react-router-dom'; import PrivateRoute from './Private'; diff --git a/frontend/src/api/features/getFeatures.ts b/frontend/src/api/features/getFeatures.ts deleted file mode 100644 index ca6bf30ca7..0000000000 --- a/frontend/src/api/features/getFeatures.ts +++ /dev/null @@ -1,23 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps } from 'types/api/features/getFeatures'; - -const getFeaturesFlags = async (): Promise< - SuccessResponse | ErrorResponse -> => { - try { - const response = await axios.get(`/featureFlags`); - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default getFeaturesFlags; diff --git a/frontend/src/api/metrics/getQueryRange.ts b/frontend/src/api/metrics/getQueryRange.ts index b6715f85e6..9a0a22bda7 100644 --- a/frontend/src/api/metrics/getQueryRange.ts +++ b/frontend/src/api/metrics/getQueryRange.ts @@ -1,17 +1,17 @@ -import { ApiV2Instance as axios } from 'api'; +import { ApiV3Instance as axios } from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { - MetricRangePayloadProps, + MetricRangePayloadV3, MetricsRangeProps, } from 'types/api/metrics/getQueryRange'; export const getMetricsQueryRange = async ( props: MetricsRangeProps, -): Promise | ErrorResponse> => { +): Promise | ErrorResponse> => { try { - const response = await axios.post(`/metrics/query_range`, props); + const response = await axios.post('/query_range', props); return { statusCode: 200, diff --git a/frontend/src/api/queryBuilder/getAggregateAttribute.ts b/frontend/src/api/queryBuilder/getAggregateAttribute.ts index 15e221d975..cbd6740f31 100644 --- a/frontend/src/api/queryBuilder/getAggregateAttribute.ts +++ b/frontend/src/api/queryBuilder/getAggregateAttribute.ts @@ -1,11 +1,16 @@ import { ApiV3Instance } from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError, AxiosResponse } from 'axios'; +import createQueryParams from 'lib/createQueryParams'; // ** Helpers import { ErrorResponse, SuccessResponse } from 'types/api'; // ** Types import { IGetAggregateAttributePayload } from 'types/api/queryBuilder/getAggregatorAttribute'; -import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { + BaseAutocompleteData, + IQueryAutocompleteResponse, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { v4 as uuid } from 'uuid'; export const getAggregateAttribute = async ({ aggregateOperator, @@ -18,14 +23,22 @@ export const getAggregateAttribute = async ({ const response: AxiosResponse<{ data: IQueryAutocompleteResponse; }> = await ApiV3Instance.get( - `autocomplete/aggregate_attributes?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&searchText=${searchText}`, + `autocomplete/aggregate_attributes?${createQueryParams({ + aggregateOperator, + searchText, + dataSource, + })}`, ); + const payload: BaseAutocompleteData[] = + response.data.data.attributeKeys?.map((item) => ({ ...item, id: uuid() })) || + []; + return { statusCode: 200, error: null, message: response.statusText, - payload: response.data.data, + payload: { attributeKeys: payload }, }; } catch (e) { return ErrorResponseHandler(e as AxiosError); diff --git a/frontend/src/api/queryBuilder/getAttributeKeys.ts b/frontend/src/api/queryBuilder/getAttributeKeys.ts index b42566a75e..b47509de0a 100644 --- a/frontend/src/api/queryBuilder/getAttributeKeys.ts +++ b/frontend/src/api/queryBuilder/getAttributeKeys.ts @@ -1,16 +1,22 @@ import { ApiV3Instance } from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError, AxiosResponse } from 'axios'; +import createQueryParams from 'lib/createQueryParams'; import { ErrorResponse, SuccessResponse } from 'types/api'; // ** Types import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys'; -import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { + BaseAutocompleteData, + IQueryAutocompleteResponse, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { v4 as uuid } from 'uuid'; export const getAggregateKeys = async ({ aggregateOperator, searchText, dataSource, aggregateAttribute, + tagType, }: IGetAttributeKeysPayload): Promise< SuccessResponse | ErrorResponse > => { @@ -18,14 +24,23 @@ export const getAggregateKeys = async ({ const response: AxiosResponse<{ data: IQueryAutocompleteResponse; }> = await ApiV3Instance.get( - `autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&searchText=${searchText}`, + `autocomplete/attribute_keys?${createQueryParams({ + aggregateOperator, + searchText, + dataSource, + aggregateAttribute, + })}&tagType=${tagType}`, ); + const payload: BaseAutocompleteData[] = + response.data.data.attributeKeys?.map((item) => ({ ...item, id: uuid() })) || + []; + return { statusCode: 200, error: null, message: response.statusText, - payload: response.data.data, + payload: { attributeKeys: payload }, }; } catch (e) { return ErrorResponseHandler(e as AxiosError); diff --git a/frontend/src/api/queryBuilder/getAttributesValues.ts b/frontend/src/api/queryBuilder/getAttributesValues.ts new file mode 100644 index 0000000000..216da1e451 --- /dev/null +++ b/frontend/src/api/queryBuilder/getAttributesValues.ts @@ -0,0 +1,42 @@ +import { ApiV3Instance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import createQueryParams from 'lib/createQueryParams'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + IAttributeValuesResponse, + IGetAttributeValuesPayload, +} from 'types/api/queryBuilder/getAttributesValues'; + +export const getAttributesValues = async ({ + aggregateOperator, + dataSource, + aggregateAttribute, + attributeKey, + filterAttributeKeyDataType, + tagType, + searchText, +}: IGetAttributeValuesPayload): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await ApiV3Instance.get( + `/autocomplete/attribute_values?${createQueryParams({ + aggregateOperator, + dataSource, + aggregateAttribute, + attributeKey, + searchText, + })}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; diff --git a/frontend/src/assets/Dashboard/TimeSeries.tsx b/frontend/src/assets/Dashboard/TimeSeries.tsx index 46e30e29fc..54d8100a63 100644 --- a/frontend/src/assets/Dashboard/TimeSeries.tsx +++ b/frontend/src/assets/Dashboard/TimeSeries.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - function TimeSeries(): JSX.Element { return ( ({ + useIsDarkMode: jest.fn(), +})); + +describe('Editor', () => { + it('renders correctly with default props', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('renders correctly with custom props', () => { + const customProps = { + value: 'test', + language: 'javascript', + readOnly: true, + height: '50vh', + options: { minimap: { enabled: false } }, + }; + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('renders with dark mode theme', () => { + (useIsDarkMode as jest.Mock).mockImplementation(() => true); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('renders with light mode theme', () => { + (useIsDarkMode as jest.Mock).mockImplementation(() => false); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + + it('displays "Loading..." message initially', () => { + const { rerender } = render(); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + + rerender(); + }); +}); diff --git a/frontend/src/components/Editor/__snapshots__/Editor.test.tsx.snap b/frontend/src/components/Editor/__snapshots__/Editor.test.tsx.snap new file mode 100644 index 0000000000..1670cedb9c --- /dev/null +++ b/frontend/src/components/Editor/__snapshots__/Editor.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Editor renders correctly with custom props 1`] = ` +
+
+
+ Loading... +
+
+
+
+`; + +exports[`Editor renders correctly with default props 1`] = ` +
+
+
+ Loading... +
+
+
+
+`; + +exports[`Editor renders with dark mode theme 1`] = ` +
+
+
+ Loading... +
+
+
+
+`; + +exports[`Editor renders with light mode theme 1`] = ` +
+
+
+ Loading... +
+
+
+
+`; diff --git a/frontend/src/components/Editor/index.tsx b/frontend/src/components/Editor/index.tsx index e24fab3c80..5f70d92c41 100644 --- a/frontend/src/components/Editor/index.tsx +++ b/frontend/src/components/Editor/index.tsx @@ -1,6 +1,6 @@ import MEditor, { EditorProps } from '@monaco-editor/react'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; function Editor({ value, @@ -13,6 +13,8 @@ function Editor({ const isDarkMode = useIsDarkMode(); const onChangeHandler = (newValue?: string): void => { + if (readOnly) return; + if (typeof newValue === 'string' && onChange) onChange(newValue); }; @@ -29,6 +31,7 @@ function Editor({ options={editorOptions} height={height} onChange={onChangeHandler} + data-testid="monaco-editor" /> ); } diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index 527510f764..f4ea8b14b2 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -26,7 +26,7 @@ import annotationPlugin from 'chartjs-plugin-annotation'; import dayjs from 'dayjs'; import { useIsDarkMode } from 'hooks/useDarkMode'; import isEqual from 'lodash-es/isEqual'; -import React, { memo, useCallback, useEffect, useRef } from 'react'; +import { memo, useCallback, useEffect, useRef } from 'react'; import { hasData } from './hasData'; import { getAxisLabelColor } from './helpers'; diff --git a/frontend/src/components/Input/index.tsx b/frontend/src/components/Input/index.tsx index 6516b6b209..18a84f611a 100644 --- a/frontend/src/components/Input/index.tsx +++ b/frontend/src/components/Input/index.tsx @@ -1,5 +1,12 @@ import { Form, Input, InputProps, InputRef } from 'antd'; -import React from 'react'; +import { + ChangeEventHandler, + FocusEventHandler, + KeyboardEventHandler, + LegacyRef, + ReactNode, + Ref, +} from 'react'; function InputComponent({ value, @@ -22,7 +29,7 @@ function InputComponent({ type={type} onChange={onChangeHandler} value={value} - ref={ref as React.Ref} + ref={ref as Ref} size={size} addonBefore={addonBefore} onBlur={onBlurHandler} @@ -37,15 +44,15 @@ function InputComponent({ interface InputComponentProps extends InputProps { value: InputProps['value']; type?: InputProps['type']; - onChangeHandler?: React.ChangeEventHandler; + onChangeHandler?: ChangeEventHandler; placeholder?: InputProps['placeholder']; - ref?: React.LegacyRef; + ref?: LegacyRef; size?: InputProps['size']; - onBlurHandler?: React.FocusEventHandler; - onPressEnterHandler?: React.KeyboardEventHandler; + onBlurHandler?: FocusEventHandler; + onPressEnterHandler?: KeyboardEventHandler; label?: string; labelOnTop?: boolean; - addonBefore?: React.ReactNode; + addonBefore?: ReactNode; } InputComponent.defaultProps = { diff --git a/frontend/src/components/Loadable/Loadable.test.tsx b/frontend/src/components/Loadable/Loadable.test.tsx new file mode 100644 index 0000000000..2d2a2173e5 --- /dev/null +++ b/frontend/src/components/Loadable/Loadable.test.tsx @@ -0,0 +1,49 @@ +import { + render, + screen, + waitForElementToBeRemoved, +} from '@testing-library/react'; +import React, { ComponentType, Suspense } from 'react'; + +import Loadable from './index'; + +// Sample component to be loaded lazily +function SampleComponent(): JSX.Element { + return
Sample Component
; +} + +const loadSampleComponent = (): Promise<{ + default: ComponentType; +}> => + new Promise<{ default: ComponentType }>((resolve) => { + setTimeout(() => { + resolve({ default: SampleComponent }); + }, 500); + }); + +describe('Loadable', () => { + it('should render the lazily loaded component', async () => { + const LoadableSampleComponent = Loadable(loadSampleComponent); + + const { container } = render( + Loading...}> + + , + ); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + await waitForElementToBeRemoved(() => screen.queryByText('Loading...')); + + expect(container.querySelector('div')).toHaveTextContent('Sample Component'); + }); + + it('should call lazy with the provided import path', () => { + const reactLazySpy = jest.spyOn(React, 'lazy'); + Loadable(loadSampleComponent); + + expect(reactLazySpy).toHaveBeenCalledTimes(1); + expect(reactLazySpy).toHaveBeenCalledWith(expect.any(Function)); + + reactLazySpy.mockRestore(); + }); +}); diff --git a/frontend/src/components/Loadable/index.tsx b/frontend/src/components/Loadable/index.tsx index 60c794f542..5cffc5793e 100644 --- a/frontend/src/components/Loadable/index.tsx +++ b/frontend/src/components/Loadable/index.tsx @@ -1,8 +1,8 @@ -import { ComponentType, lazy } from 'react'; +import { ComponentType, lazy, LazyExoticComponent } from 'react'; function Loadable(importPath: { (): LoadableProps; -}): React.LazyExoticComponent { +}): LazyExoticComponent { return lazy(() => importPath()); } diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index d8aee19708..874a9a0ec7 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -1,13 +1,14 @@ -import { Button, Popover } from 'antd'; +import { Popover } from 'antd'; +import ROUTES from 'constants/routes'; +import history from 'lib/history'; import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; -import React, { memo, useCallback, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Dispatch } from 'redux'; +import { memo, ReactNode, useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs'; import { ILogsReducer } from 'types/reducer/logs'; +import { ButtonContainer } from './styles'; + function AddToQueryHOC({ fieldKey, fieldValue, @@ -16,7 +17,6 @@ function AddToQueryHOC({ const { searchFilter: { queryString }, } = useSelector((store) => store.logs); - const dispatch = useDispatch>(); const generatedQuery = useMemo( () => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }), @@ -31,31 +31,27 @@ function AddToQueryHOC({ } else { updatedQueryString += ` AND ${generatedQuery}`; } - dispatch({ - type: SET_SEARCH_QUERY_STRING, - payload: { - searchQueryString: updatedQueryString, - }, - }); - }, [dispatch, generatedQuery, queryString]); + + history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); + }, [generatedQuery, queryString]); const popOverContent = useMemo(() => Add to query: {fieldKey}, [ fieldKey, ]); return ( - + ); } interface AddToQueryHOCProps { fieldKey: string; fieldValue: string; - children: React.ReactNode; + children: ReactNode; } export default memo(AddToQueryHOC); diff --git a/frontend/src/components/Logs/CategoryHeading/index.tsx b/frontend/src/components/Logs/CategoryHeading/index.tsx index f48160593a..b7d8ae7b50 100644 --- a/frontend/src/components/Logs/CategoryHeading/index.tsx +++ b/frontend/src/components/Logs/CategoryHeading/index.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import { ReactNode } from 'react'; import { CategoryHeadingText } from './styles'; interface ICategoryHeadingProps { - children: React.ReactNode; + children: ReactNode; } function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element { return {children}; diff --git a/frontend/src/components/Logs/CopyClipboardHOC.tsx b/frontend/src/components/Logs/CopyClipboardHOC.tsx index 8296b8a05c..a12208bf77 100644 --- a/frontend/src/components/Logs/CopyClipboardHOC.tsx +++ b/frontend/src/components/Logs/CopyClipboardHOC.tsx @@ -1,6 +1,6 @@ import { Popover } from 'antd'; import { useNotifications } from 'hooks/useNotifications'; -import React, { useCallback, useEffect } from 'react'; +import { ReactNode, useCallback, useEffect } from 'react'; import { useCopyToClipboard } from 'react-use'; function CopyClipboardHOC({ @@ -22,7 +22,7 @@ function CopyClipboardHOC({ }, [setCopy, textToCopy]); return ( - + Copy to clipboard} @@ -35,7 +35,7 @@ function CopyClipboardHOC({ interface CopyClipboardHOCProps { textToCopy: string; - children: React.ReactNode; + children: ReactNode; } export default CopyClipboardHOC; diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index 6095d343fc..ebee0899c7 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -2,13 +2,12 @@ import { blue, grey, orange } from '@ant-design/colors'; import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons'; import Convert from 'ansi-to-html'; import { Button, Divider, Row, Typography } from 'antd'; -import { map } from 'd3'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useNotifications } from 'hooks/useNotifications'; // utils import { FlatLogData } from 'lib/logs/flatLogData'; -import React, { useCallback, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useCopyToClipboard } from 'react-use'; // interfaces @@ -127,7 +126,7 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
- {map(updatedSelecedFields, (field) => + {updatedSelecedFields.map((field) => isValidLogField(flattenLogData[field.name] as never) ? ( ` font-size: 0.875rem; line-height: 2rem; - - cursor: pointer; `; diff --git a/frontend/src/components/Logs/styles.ts b/frontend/src/components/Logs/styles.ts new file mode 100644 index 0000000000..53f31192f9 --- /dev/null +++ b/frontend/src/components/Logs/styles.ts @@ -0,0 +1,8 @@ +import { Button } from 'antd'; +import styled from 'styled-components'; + +export const ButtonContainer = styled(Button)` + &&& { + padding-left: 0; + } +`; diff --git a/frontend/src/components/MessageTip/MessageTip.test.tsx b/frontend/src/components/MessageTip/MessageTip.test.tsx new file mode 100644 index 0000000000..1c050c01f6 --- /dev/null +++ b/frontend/src/components/MessageTip/MessageTip.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from '@testing-library/react'; + +import MessageTip from './index'; + +describe('MessageTip', () => { + it('should not render when show prop is false', () => { + render( + Close} + />, + ); + + const messageTip = screen.queryByRole('alert'); + + expect(messageTip).toBeNull(); + }); + + it('should render with the provided message and action', () => { + const message = 'Test Message'; + const action = ; + + render(); + + const messageTip = screen.getByRole('alert'); + const messageText = screen.getByText(message); + const actionButton = screen.getByRole('button', { name: 'Close' }); + + expect(messageTip).toBeInTheDocument(); + expect(messageText).toBeInTheDocument(); + expect(actionButton).toBeInTheDocument(); + }); + + // taken from antd docs + // https://github.com/ant-design/ant-design/blob/master/components/alert/__tests__/index.test.tsx + it('custom action', () => { + const { container } = render( + Close} + />, + ); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/components/MessageTip/__snapshots__/MessageTip.test.tsx.snap b/frontend/src/components/MessageTip/__snapshots__/MessageTip.test.tsx.snap new file mode 100644 index 0000000000..044b288468 --- /dev/null +++ b/frontend/src/components/MessageTip/__snapshots__/MessageTip.test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageTip custom action 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + + +`; diff --git a/frontend/src/components/MessageTip/index.tsx b/frontend/src/components/MessageTip/index.tsx index 67cdb41703..23c740395e 100644 --- a/frontend/src/components/MessageTip/index.tsx +++ b/frontend/src/components/MessageTip/index.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import { ReactNode } from 'react'; import { StyledAlert } from './styles'; interface MessageTipProps { show?: boolean; - message: React.ReactNode | string; - action: React.ReactNode | undefined; + message: ReactNode | string; + action: ReactNode | undefined; } function MessageTip({ diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index 83e380a981..a8f8bb6532 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -1,5 +1,5 @@ import { Modal, ModalProps as Props } from 'antd'; -import React, { ReactElement } from 'react'; +import { ReactElement } from 'react'; function CustomModal({ title, diff --git a/frontend/src/components/NotFound/NotFound.test.tsx b/frontend/src/components/NotFound/NotFound.test.tsx index 0eed86262c..f72596e875 100644 --- a/frontend/src/components/NotFound/NotFound.test.tsx +++ b/frontend/src/components/NotFound/NotFound.test.tsx @@ -1,5 +1,4 @@ import { render } from '@testing-library/react'; -import React from 'react'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import store from 'store'; diff --git a/frontend/src/components/NotFound/index.tsx b/frontend/src/components/NotFound/index.tsx index 04d140e34f..5af3c4640c 100644 --- a/frontend/src/components/NotFound/index.tsx +++ b/frontend/src/components/NotFound/index.tsx @@ -2,7 +2,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get'; import NotFoundImage from 'assets/NotFound'; import { LOCALSTORAGE } from 'constants/localStorage'; import ROUTES from 'constants/routes'; -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import AppActions from 'types/actions'; diff --git a/frontend/src/components/ReleaseNote/Releases/ReleaseNote0120.tsx b/frontend/src/components/ReleaseNote/Releases/ReleaseNote0120.tsx index 78b2800b61..249147fcde 100644 --- a/frontend/src/components/ReleaseNote/Releases/ReleaseNote0120.tsx +++ b/frontend/src/components/ReleaseNote/Releases/ReleaseNote0120.tsx @@ -1,7 +1,7 @@ import { Button, Space } from 'antd'; import setFlags from 'api/user/setFlags'; import MessageTip from 'components/MessageTip'; -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; diff --git a/frontend/src/components/ReleaseNote/index.tsx b/frontend/src/components/ReleaseNote/index.tsx index daede3a51f..bfabbd2637 100644 --- a/frontend/src/components/ReleaseNote/index.tsx +++ b/frontend/src/components/ReleaseNote/index.tsx @@ -1,7 +1,6 @@ import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps'; import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120'; import ROUTES from 'constants/routes'; -import React from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { UserFlags } from 'types/api/user/setFlags'; diff --git a/frontend/src/components/ResizeTable/ResizableHeader.tsx b/frontend/src/components/ResizeTable/ResizableHeader.tsx index b3119c0c1a..8611a45886 100644 --- a/frontend/src/components/ResizeTable/ResizableHeader.tsx +++ b/frontend/src/components/ResizeTable/ResizableHeader.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import { SyntheticEvent, useMemo } from 'react'; import { Resizable, ResizeCallbackData } from 'react-resizable'; import { enableUserSelectHack } from './config'; @@ -37,7 +37,7 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element { } interface ResizableHeaderProps { - onResize: (e: React.SyntheticEvent, data: ResizeCallbackData) => void; + onResize: (e: SyntheticEvent, data: ResizeCallbackData) => void; width: number; } diff --git a/frontend/src/components/ResizeTable/ResizeTable.tsx b/frontend/src/components/ResizeTable/ResizeTable.tsx index b7aee770c9..681d8b8670 100644 --- a/frontend/src/components/ResizeTable/ResizeTable.tsx +++ b/frontend/src/components/ResizeTable/ResizeTable.tsx @@ -1,7 +1,7 @@ import { Table } from 'antd'; import type { TableProps } from 'antd/es/table'; import { ColumnsType } from 'antd/lib/table'; -import React, { useCallback, useMemo, useState } from 'react'; +import { SyntheticEvent, useCallback, useMemo, useState } from 'react'; import { ResizeCallbackData } from 'react-resizable'; import ResizableHeader from './ResizableHeader'; @@ -12,7 +12,7 @@ function ResizeTable({ columns, ...restprops }: TableProps): JSX.Element { const handleResize = useCallback( (index: number) => ( - _e: React.SyntheticEvent, + _e: SyntheticEvent, { size }: ResizeCallbackData, ): void => { const newColumns = [...columnsData]; diff --git a/frontend/src/components/RouteTab/index.tsx b/frontend/src/components/RouteTab/index.tsx index 0059f5b84c..07b9f70938 100644 --- a/frontend/src/components/RouteTab/index.tsx +++ b/frontend/src/components/RouteTab/index.tsx @@ -1,6 +1,5 @@ import { Tabs, TabsProps } from 'antd'; import history from 'lib/history'; -import React from 'react'; function RouteTab({ routes, diff --git a/frontend/src/components/Spinner/index.tsx b/frontend/src/components/Spinner/index.tsx index 819e552442..669b3f0cda 100644 --- a/frontend/src/components/Spinner/index.tsx +++ b/frontend/src/components/Spinner/index.tsx @@ -1,6 +1,6 @@ import { LoadingOutlined } from '@ant-design/icons'; import { Spin, SpinProps } from 'antd'; -import React from 'react'; +import { CSSProperties } from 'react'; import { SpinerStyle } from './styles'; @@ -15,8 +15,8 @@ function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element { interface SpinnerProps { size?: SpinProps['size']; tip?: SpinProps['tip']; - height?: React.CSSProperties['height']; - style?: React.CSSProperties; + height?: CSSProperties['height']; + style?: CSSProperties; } Spinner.defaultProps = { size: undefined, diff --git a/frontend/src/components/Spinner/styles.ts b/frontend/src/components/Spinner/styles.ts index 53a77a8978..6763239c40 100644 --- a/frontend/src/components/Spinner/styles.ts +++ b/frontend/src/components/Spinner/styles.ts @@ -1,8 +1,8 @@ -import React from 'react'; +import { CSSProperties } from 'react'; import styled from 'styled-components'; interface Props { - height: React.CSSProperties['height']; + height: CSSProperties['height']; } export const SpinerStyle = styled.div` diff --git a/frontend/src/components/Styled/index.ts b/frontend/src/components/Styled/index.ts index ffa3fbccc0..579e15a499 100644 --- a/frontend/src/components/Styled/index.ts +++ b/frontend/src/components/Styled/index.ts @@ -1,7 +1,7 @@ import * as AntD from 'antd'; import { TextProps } from 'antd/lib/typography/Text'; import { TitleProps } from 'antd/lib/typography/Title'; -import React from 'react'; +import { HTMLAttributes } from 'react'; import styled, { FlattenSimpleInterpolation } from 'styled-components'; import { IStyledClass } from './types'; @@ -51,7 +51,7 @@ const StyledTypographyTitle = styled(Title)` ${styledClass} `; -type TStyledDiv = React.HTMLAttributes & IStyledClass; +type TStyledDiv = HTMLAttributes & IStyledClass; const StyledDiv = styled.div` ${styledClass} `; diff --git a/frontend/src/components/TextToolTip/TextToolTip.test.tsx b/frontend/src/components/TextToolTip/TextToolTip.test.tsx new file mode 100644 index 0000000000..65192cf001 --- /dev/null +++ b/frontend/src/components/TextToolTip/TextToolTip.test.tsx @@ -0,0 +1,53 @@ +import { fireEvent, render, waitFor } from '@testing-library/react'; + +import TextToolTip from './index'; + +describe('TextToolTip', () => { + const tooltipText = 'Tooltip Text'; + + it('displays the tooltip when hovering over the icon', async () => { + const { getByRole, getByText } = render(); + const icon = getByRole('img'); + fireEvent.mouseOver(icon); + + await waitFor(() => { + const tooltip = getByText(tooltipText); + expect(tooltip).toBeInTheDocument(); + }); + }); + + it('renders the tooltip content correctly', async () => { + const { getByRole, getByText } = render( + , + ); + const icon = getByRole('img'); + fireEvent.mouseOver(icon); + + await waitFor(() => { + const tooltip = getByText(tooltipText); + const link = getByText('here'); + expect(tooltip).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveAttribute('target', '_blank'); + }); + }); + + it('does not display the tooltip by default', () => { + const { queryByText } = render(); + const tooltip = queryByText(tooltipText); + expect(tooltip).toBeNull(); + }); + + it('opens the URL in a new tab when clicked', async () => { + const { getByRole } = render( + , + ); + const icon = getByRole('img'); + fireEvent.mouseOver(icon); + await waitFor(() => { + const link = getByRole('link'); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveAttribute('target', '_blank'); + }); + }); +}); diff --git a/frontend/src/components/TextToolTip/index.tsx b/frontend/src/components/TextToolTip/index.tsx index 64a9cd053a..f01a623988 100644 --- a/frontend/src/components/TextToolTip/index.tsx +++ b/frontend/src/components/TextToolTip/index.tsx @@ -3,7 +3,7 @@ import { QuestionCircleFilled } from '@ant-design/icons'; import { Tooltip } from 'antd'; import { themeColors } from 'constants/theme'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { style } from './styles'; diff --git a/frontend/src/components/TimePreferenceDropDown/config.tsx b/frontend/src/components/TimePreferenceDropDown/config.tsx index bf09498ec7..e7bdd793db 100644 --- a/frontend/src/components/TimePreferenceDropDown/config.tsx +++ b/frontend/src/components/TimePreferenceDropDown/config.tsx @@ -1,6 +1,5 @@ import { Typography } from 'antd'; import { timeItems } from 'container/NewWidget/RightContainer/timeItems'; -import React from 'react'; export const menuItems = timeItems.map((item) => ({ key: item.enum, diff --git a/frontend/src/components/TimePreferenceDropDown/index.tsx b/frontend/src/components/TimePreferenceDropDown/index.tsx index 3ce9795f15..b24c07b3f2 100644 --- a/frontend/src/components/TimePreferenceDropDown/index.tsx +++ b/frontend/src/components/TimePreferenceDropDown/index.tsx @@ -3,7 +3,7 @@ import TimeItems, { timePreferance, timePreferenceType, } from 'container/NewWidget/RightContainer/timeItems'; -import React, { useCallback, useMemo } from 'react'; +import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; import { menuItems } from './config'; import { TextContainer } from './styles'; @@ -44,7 +44,7 @@ interface TimeMenuItemOnChangeHandlerEvent { } interface TimePreferenceDropDownProps { - setSelectedTime: React.Dispatch>; + setSelectedTime: Dispatch>; selectedTime: timePreferance; } diff --git a/frontend/src/components/ValueGraph/index.tsx b/frontend/src/components/ValueGraph/index.tsx index c5de1785c6..e75f338945 100644 --- a/frontend/src/components/ValueGraph/index.tsx +++ b/frontend/src/components/ValueGraph/index.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { Value } from './styles'; function ValueGraph({ value }: ValueGraphProps): JSX.Element { diff --git a/frontend/src/components/WelcomeLeftContainer/index.tsx b/frontend/src/components/WelcomeLeftContainer/index.tsx index ef69a4599d..3e8ec68e42 100644 --- a/frontend/src/components/WelcomeLeftContainer/index.tsx +++ b/frontend/src/components/WelcomeLeftContainer/index.tsx @@ -1,5 +1,5 @@ import { Card, Space, Typography } from 'antd'; -import React from 'react'; +import { ReactChild } from 'react'; import { useTranslation } from 'react-i18next'; import { Container, LeftContainer, Logo } from './styles'; @@ -34,7 +34,7 @@ function WelcomeLeftContainer({ interface WelcomeLeftContainerProps { version: string; - children: React.ReactChild; + children: ReactChild; } export default WelcomeLeftContainer; diff --git a/frontend/src/constants/alerts.ts b/frontend/src/constants/alerts.ts new file mode 100644 index 0000000000..3565ded3d7 --- /dev/null +++ b/frontend/src/constants/alerts.ts @@ -0,0 +1,9 @@ +import { AlertTypes } from 'types/api/alerts/alertTypes'; +import { DataSource } from 'types/common/queryBuilder'; + +export const ALERTS_DATA_SOURCE_MAP: Record = { + [AlertTypes.METRICS_BASED_ALERT]: DataSource.METRICS, + [AlertTypes.LOGS_BASED_ALERT]: DataSource.LOGS, + [AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES, + [AlertTypes.EXCEPTIONS_BASED_ALERT]: DataSource.TRACES, +}; diff --git a/frontend/src/constants/dashboard.ts b/frontend/src/constants/dashboard.ts index 587b781906..2941f00077 100644 --- a/frontend/src/constants/dashboard.ts +++ b/frontend/src/constants/dashboard.ts @@ -1,5 +1,3 @@ -import { EAggregateOperator, EReduceOperator } from 'types/common/dashboard'; - export const PromQLQueryTemplate = { query: '', legend: '', @@ -11,24 +9,3 @@ export const ClickHouseQueryTemplate = { legend: '', disabled: false, }; - -export const QueryBuilderQueryTemplate = { - metricName: null, - aggregateOperator: EAggregateOperator.NOOP, - tagFilters: { - op: 'AND', - items: [], - }, - legend: '', - disabled: false, - // Specific to TIME_SERIES type graph - groupBy: [], - // Specific to VALUE type graph - reduceTo: EReduceOperator['Latest of values in timeframe'], -}; - -export const QueryBuilderFormulaTemplate = { - expression: '', - disabled: false, - legend: '', -}; diff --git a/frontend/src/constants/featureKeys.ts b/frontend/src/constants/featureKeys.ts deleted file mode 100644 index 6684f3ddae..0000000000 --- a/frontend/src/constants/featureKeys.ts +++ /dev/null @@ -1,7 +0,0 @@ -// keep this consistent with backend model>features.go -export enum FeatureKeys { - SSO = 'SSO', - ENTERPRISE_PLAN = 'ENTERPRISE_PLAN', - BASIC_PLAN = 'BASIC_PLAN', - DISABLE_UPSELL = 'DISABLE_UPSELL', -} diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts index ee7d323b30..cceaf2817b 100644 --- a/frontend/src/constants/features.ts +++ b/frontend/src/constants/features.ts @@ -1,6 +1,11 @@ // keep this consistent with backend constants.go export enum FeatureKeys { SSO = 'SSO', - ENTERPRISE_PLAN = 'ENTERPRISE_PLAN', - BASIC_PLAN = 'BASIC_PLAN', + DurationSort = 'DurationSort', + TimestampSort = 'TimestampSort', + SMART_TRACE_DETAIL = 'SMART_TRACE_DETAIL', + CUSTOM_METRICS_FUNCTION = 'CUSTOM_METRICS_FUNCTION', + QUERY_BUILDER_PANELS = 'QUERY_BUILDER_PANELS', + QUERY_BUILDER_ALERTS = 'QUERY_BUILDER_ALERTS', + DISABLE_UPSELL = 'DISABLE_UPSELL', } diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index 29221e9f9c..35c1e2c2ca 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -1,5 +1,4 @@ -// eslint-disable-next-line @typescript-eslint/naming-convention -export enum METRICS_PAGE_QUERY_PARAM { +export enum QueryParams { interval = 'interval', startTime = 'startTime', endTime = 'endTime', @@ -12,4 +11,5 @@ export enum METRICS_PAGE_QUERY_PARAM { selectedTags = 'selectedTags', aggregationOption = 'aggregationOption', entity = 'entity', + resourceAttributes = 'resourceAttribute', } diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts new file mode 100644 index 0000000000..525c679681 --- /dev/null +++ b/frontend/src/constants/queryBuilder.ts @@ -0,0 +1,235 @@ +// ** Helpers +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; +import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { + HavingForm, + IBuilderFormula, + IBuilderQuery, +} from 'types/api/queryBuilder/queryBuilderData'; +import { + BoolOperators, + DataSource, + MetricAggregateOperator, + NumberOperators, + PanelTypeKeys, + QueryAdditionalFilter, + ReduceOperators, + StringOperators, +} from 'types/common/queryBuilder'; +import { SelectOption } from 'types/common/select'; +import { v4 as uuid } from 'uuid'; + +import { + logsAggregateOperatorOptions, + metricAggregateOperatorOptions, + tracesAggregateOperatorOptions, +} from './queryBuilderOperators'; + +export const MAX_FORMULAS = 20; +export const MAX_QUERIES = 26; + +export const selectValueDivider = '--'; + +export const formulasNames: string[] = Array.from( + Array(MAX_FORMULAS), + (_, i) => `F${i + 1}`, +); +const alpha: number[] = Array.from(Array(MAX_QUERIES), (_, i) => i + 65); +export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str)); + +export enum QueryBuilderKeys { + GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE', + GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS', + GET_ATTRIBUTE_KEY = 'GET_ATTRIBUTE_KEY', +} + +export const mapOfOperators = { + metrics: metricAggregateOperatorOptions, + logs: logsAggregateOperatorOptions, + traces: tracesAggregateOperatorOptions, +}; + +export const mapOfFilters: Record = { + metrics: [ + // eslint-disable-next-line sonarjs/no-duplicate-string + { text: 'Aggregation interval', field: 'stepInterval' }, + { text: 'Having', field: 'having' }, + ], + logs: [ + { text: 'Order by', field: 'orderBy' }, + { text: 'Limit', field: 'limit' }, + { text: 'Having', field: 'having' }, + { text: 'Aggregation interval', field: 'stepInterval' }, + ], + traces: [ + { text: 'Order by', field: 'orderBy' }, + { text: 'Limit', field: 'limit' }, + { text: 'Having', field: 'having' }, + { text: 'Aggregation interval', field: 'stepInterval' }, + ], +}; + +export const REDUCE_TO_VALUES: SelectOption[] = [ + { value: 'last', label: 'Latest of values in timeframe' }, + { value: 'sum', label: 'Sum of values in timeframe' }, + { value: 'avg', label: 'Average of values in timeframe' }, + { value: 'max', label: 'Max of values in timeframe' }, + { value: 'min', label: 'Min of values in timeframe' }, +]; + +export const initialHavingValues: HavingForm = { + columnName: '', + op: '', + value: [], +}; + +export const initialAggregateAttribute: IBuilderQuery['aggregateAttribute'] = { + id: uuid(), + dataType: null, + key: '', + isColumn: null, + type: null, +}; + +export const initialQueryBuilderFormValues: IBuilderQuery = { + dataSource: DataSource.METRICS, + queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), + aggregateOperator: MetricAggregateOperator.NOOP, + aggregateAttribute: initialAggregateAttribute, + filters: { items: [], op: 'AND' }, + expression: createNewBuilderItemName({ + existNames: [], + sourceNames: alphabet, + }), + disabled: false, + having: [], + stepInterval: 30, + limit: null, + orderBy: [], + groupBy: [], + legend: '', + reduceTo: 'sum', +}; + +export const initialFormulaBuilderFormValues: IBuilderFormula = { + queryName: createNewBuilderItemName({ + existNames: [], + sourceNames: formulasNames, + }), + expression: '', + disabled: false, + legend: '', +}; + +export const operatorsByTypes: Record = { + string: Object.values(StringOperators), + number: Object.values(NumberOperators), + bool: Object.values(BoolOperators), +}; + +export const PANEL_TYPES: Record = { + TIME_SERIES: 'graph', + VALUE: 'value', + TABLE: 'table', + LIST: 'list', + EMPTY_WIDGET: 'EMPTY_WIDGET', +}; + +export type IQueryBuilderState = 'search'; + +export const QUERY_BUILDER_SEARCH_VALUES = { + MULTIPLY: 'MULTIPLY_VALUE', + SINGLE: 'SINGLE_VALUE', + NON: 'NON_VALUE', + NOT_VALID: 'NOT_VALID', +}; + +export const OPERATORS = { + IN: 'IN', + NIN: 'NOT_IN', + LIKE: 'LIKE', + NLIKE: 'NOT_LIKE', + '=': '=', + '!=': '!=', + EXISTS: 'EXISTS', + NOT_EXISTS: 'NOT_EXISTS', + CONTAINS: 'CONTAINS', + NOT_CONTAINS: 'NOT_CONTAINS', + '>=': '>=', + '>': '>', + '<=': '<=', + '<': '<', +}; + +export const QUERY_BUILDER_OPERATORS_BY_TYPES = { + string: [ + OPERATORS['='], + OPERATORS['!='], + OPERATORS.IN, + OPERATORS.NIN, + OPERATORS.LIKE, + OPERATORS.NLIKE, + OPERATORS.CONTAINS, + OPERATORS.NOT_CONTAINS, + OPERATORS.EXISTS, + OPERATORS.NOT_EXISTS, + ], + int64: [ + OPERATORS['='], + OPERATORS['!='], + OPERATORS.IN, + OPERATORS.NIN, + OPERATORS.EXISTS, + OPERATORS.NOT_EXISTS, + OPERATORS['>='], + OPERATORS['>'], + OPERATORS['<='], + OPERATORS['<'], + ], + float64: [ + OPERATORS['='], + OPERATORS['!='], + OPERATORS.IN, + OPERATORS.NIN, + OPERATORS.EXISTS, + OPERATORS.NOT_EXISTS, + OPERATORS['>='], + OPERATORS['>'], + OPERATORS['<='], + OPERATORS['<'], + ], + bool: [ + OPERATORS['='], + OPERATORS['!='], + OPERATORS.EXISTS, + OPERATORS.NOT_EXISTS, + ], + universal: [ + OPERATORS['='], + OPERATORS['!='], + OPERATORS.IN, + OPERATORS.NIN, + OPERATORS.EXISTS, + OPERATORS.NOT_EXISTS, + OPERATORS.LIKE, + OPERATORS.NLIKE, + OPERATORS['>='], + OPERATORS['>'], + OPERATORS['<='], + OPERATORS['<'], + OPERATORS.CONTAINS, + OPERATORS.NOT_CONTAINS, + ], +}; + +export const HAVING_OPERATORS: string[] = [ + OPERATORS['='], + OPERATORS['!='], + OPERATORS.IN, + OPERATORS.NIN, + OPERATORS['>='], + OPERATORS['>'], + OPERATORS['<='], + OPERATORS['<'], +]; diff --git a/frontend/src/constants/queryBuilderOperators.ts b/frontend/src/constants/queryBuilderOperators.ts new file mode 100644 index 0000000000..7c5cff2b69 --- /dev/null +++ b/frontend/src/constants/queryBuilderOperators.ts @@ -0,0 +1,304 @@ +import { + LogsAggregatorOperator, + MetricAggregateOperator, + TracesAggregatorOperator, +} from 'types/common/queryBuilder'; +import { SelectOption } from 'types/common/select'; + +export const metricAggregateOperatorOptions: SelectOption[] = [ + { + value: MetricAggregateOperator.NOOP, + label: 'NOOP', + }, + { + value: MetricAggregateOperator.COUNT, + label: 'Count', + }, + { + value: MetricAggregateOperator.COUNT_DISTINCT, + // eslint-disable-next-line sonarjs/no-duplicate-string + label: 'Count Distinct', + }, + { + value: MetricAggregateOperator.SUM, + label: 'Sum', + }, + { + value: MetricAggregateOperator.AVG, + label: 'Avg', + }, + { + value: MetricAggregateOperator.MAX, + label: 'Max', + }, + { + value: MetricAggregateOperator.MIN, + label: 'Min', + }, + { + value: MetricAggregateOperator.P05, + label: 'P05', + }, + { + value: MetricAggregateOperator.P10, + label: 'P10', + }, + { + value: MetricAggregateOperator.P20, + label: 'P20', + }, + { + value: MetricAggregateOperator.P25, + label: 'P25', + }, + { + value: MetricAggregateOperator.P50, + label: 'P50', + }, + { + value: MetricAggregateOperator.P75, + label: 'P75', + }, + { + value: MetricAggregateOperator.P90, + label: 'P90', + }, + { + value: MetricAggregateOperator.P95, + label: 'P95', + }, + { + value: MetricAggregateOperator.P99, + label: 'P99', + }, + { + value: MetricAggregateOperator.RATE, + label: 'Rate', + }, + { + value: MetricAggregateOperator.SUM_RATE, + label: 'Sum_rate', + }, + { + value: MetricAggregateOperator.AVG_RATE, + label: 'Avg_rate', + }, + { + value: MetricAggregateOperator.MAX_RATE, + label: 'Max_rate', + }, + { + value: MetricAggregateOperator.MIN_RATE, + label: 'Min_rate', + }, + { + value: MetricAggregateOperator.RATE_SUM, + label: 'Rate_sum', + }, + { + value: MetricAggregateOperator.RATE_AVG, + label: 'Rate_avg', + }, + { + value: MetricAggregateOperator.RATE_MIN, + label: 'Rate_min', + }, + { + value: MetricAggregateOperator.RATE_MAX, + label: 'Rate_max', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_50, + label: 'Hist_quantile_50', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_75, + label: 'Hist_quantile_75', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_90, + label: 'Hist_quantile_90', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_95, + label: 'Hist_quantile_95', + }, + { + value: MetricAggregateOperator.HIST_QUANTILE_99, + label: 'Hist_quantile_99', + }, +]; + +export const tracesAggregateOperatorOptions: SelectOption[] = [ + { + value: TracesAggregatorOperator.NOOP, + label: 'NOOP', + }, + { + value: TracesAggregatorOperator.COUNT, + label: 'Count', + }, + { + value: TracesAggregatorOperator.COUNT_DISTINCT, + label: 'Count Distinct', + }, + { + value: TracesAggregatorOperator.SUM, + label: 'Sum', + }, + { + value: TracesAggregatorOperator.AVG, + label: 'Avg', + }, + { + value: TracesAggregatorOperator.MAX, + label: 'Max', + }, + { + value: TracesAggregatorOperator.MIN, + label: 'Min', + }, + { + value: TracesAggregatorOperator.P05, + label: 'P05', + }, + { + value: TracesAggregatorOperator.P10, + label: 'P10', + }, + { + value: TracesAggregatorOperator.P20, + label: 'P20', + }, + { + value: TracesAggregatorOperator.P25, + label: 'P25', + }, + { + value: TracesAggregatorOperator.P50, + label: 'P50', + }, + { + value: TracesAggregatorOperator.P75, + label: 'P75', + }, + { + value: TracesAggregatorOperator.P90, + label: 'P90', + }, + { + value: TracesAggregatorOperator.P95, + label: 'P95', + }, + { + value: TracesAggregatorOperator.P99, + label: 'P99', + }, + { + value: TracesAggregatorOperator.RATE, + label: 'Rate', + }, + { + value: TracesAggregatorOperator.RATE_SUM, + label: 'Rate_sum', + }, + { + value: TracesAggregatorOperator.RATE_AVG, + label: 'Rate_avg', + }, + { + value: TracesAggregatorOperator.RATE_MIN, + label: 'Rate_min', + }, + { + value: TracesAggregatorOperator.RATE_MAX, + label: 'Rate_max', + }, +]; + +export const logsAggregateOperatorOptions: SelectOption[] = [ + { + value: LogsAggregatorOperator.NOOP, + label: 'NOOP', + }, + { + value: LogsAggregatorOperator.COUNT, + label: 'Count', + }, + { + value: LogsAggregatorOperator.COUNT_DISTINCT, + label: 'Count Distinct', + }, + { + value: LogsAggregatorOperator.SUM, + label: 'Sum', + }, + { + value: LogsAggregatorOperator.AVG, + label: 'Avg', + }, + { + value: LogsAggregatorOperator.MAX, + label: 'Max', + }, + { + value: LogsAggregatorOperator.MIN, + label: 'Min', + }, + { + value: LogsAggregatorOperator.P05, + label: 'P05', + }, + { + value: LogsAggregatorOperator.P10, + label: 'P10', + }, + { + value: LogsAggregatorOperator.P20, + label: 'P20', + }, + { + value: LogsAggregatorOperator.P25, + label: 'P25', + }, + { + value: LogsAggregatorOperator.P50, + label: 'P50', + }, + { + value: LogsAggregatorOperator.P75, + label: 'P75', + }, + { + value: LogsAggregatorOperator.P90, + label: 'P90', + }, + { + value: LogsAggregatorOperator.P95, + label: 'P95', + }, + { + value: LogsAggregatorOperator.P99, + label: 'P99', + }, + { + value: LogsAggregatorOperator.RATE, + label: 'Rate', + }, + { + value: LogsAggregatorOperator.RATE_SUM, + label: 'Rate_sum', + }, + { + value: LogsAggregatorOperator.RATE_AVG, + label: 'Rate_avg', + }, + { + value: LogsAggregatorOperator.RATE_MIN, + label: 'Rate_min', + }, + { + value: LogsAggregatorOperator.RATE_MAX, + label: 'Rate_max', + }, +]; diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts new file mode 100644 index 0000000000..7cc38c153e --- /dev/null +++ b/frontend/src/constants/reactQueryKeys.ts @@ -0,0 +1,3 @@ +export const REACT_QUERY_KEY = { + GET_ALL_LICENCES: 'GET_ALL_LICENCES', +}; diff --git a/frontend/src/constants/regExp.ts b/frontend/src/constants/regExp.ts new file mode 100644 index 0000000000..3ea1e3179c --- /dev/null +++ b/frontend/src/constants/regExp.ts @@ -0,0 +1,5 @@ +export const FORMULA_REGEXP = /F\d+/; + +export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/; + +export const TYPE_ADDON_REGEXP = /_(.+)/; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index c596918129..b5a92b3a0e 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -16,8 +16,8 @@ const ROUTES = { LIST_ALL_ALERT: '/alerts', ALERTS_NEW: '/alerts/new', ALL_CHANNELS: '/settings/channels', - CHANNELS_NEW: '/setting/channels/new', - CHANNELS_EDIT: '/setting/channels/edit/:id', + CHANNELS_NEW: '/settings/channels/new', + CHANNELS_EDIT: '/settings/channels/:id', ALL_ERROR: '/exceptions', ERROR_DETAIL: '/error-detail', VERSION: '/status', diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts index 36dfe4bd87..ce6cdd354a 100644 --- a/frontend/src/constants/theme.ts +++ b/frontend/src/constants/theme.ts @@ -38,6 +38,8 @@ const themeColors = { whiteCream: '#ffffffd5', black: '#000000', lightgrey: '#ddd', + borderLightGrey: '#d9d9d9', + borderDarkGrey: '#424242', }; export { themeColors }; diff --git a/frontend/src/constants/useQueryKeys.ts b/frontend/src/constants/useQueryKeys.ts deleted file mode 100644 index 705d5ef350..0000000000 --- a/frontend/src/constants/useQueryKeys.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryBuilderKeys { - GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE', -} diff --git a/frontend/src/container/AllAlertChannels/AlertChannels.tsx b/frontend/src/container/AllAlertChannels/AlertChannels.tsx index 72d5a54b85..e3dccb6160 100644 --- a/frontend/src/container/AllAlertChannels/AlertChannels.tsx +++ b/frontend/src/container/AllAlertChannels/AlertChannels.tsx @@ -6,7 +6,7 @@ import ROUTES from 'constants/routes'; import useComponentPermission from 'hooks/useComponentPermission'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useCallback, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { generatePath } from 'react-router-dom'; diff --git a/frontend/src/container/AllAlertChannels/Delete.tsx b/frontend/src/container/AllAlertChannels/Delete.tsx index 1a4456cced..c5edbf1e6a 100644 --- a/frontend/src/container/AllAlertChannels/Delete.tsx +++ b/frontend/src/container/AllAlertChannels/Delete.tsx @@ -1,7 +1,7 @@ import { Button } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import deleteChannel from 'api/channels/delete'; -import React, { useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Channels } from 'types/api/channels/getAll'; @@ -55,7 +55,7 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element { interface DeleteProps { notifications: NotificationInstance; - setChannels: React.Dispatch>; + setChannels: Dispatch>; id: string; } diff --git a/frontend/src/container/AllAlertChannels/index.tsx b/frontend/src/container/AllAlertChannels/index.tsx index 99636806ea..8038f17778 100644 --- a/frontend/src/container/AllAlertChannels/index.tsx +++ b/frontend/src/container/AllAlertChannels/index.tsx @@ -7,7 +7,7 @@ import ROUTES from 'constants/routes'; import useComponentPermission from 'hooks/useComponentPermission'; import useFetch from 'hooks/useFetch'; import history from 'lib/history'; -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; diff --git a/frontend/src/container/AllError/index.tsx b/frontend/src/container/AllError/index.tsx index c3b0580f44..4bc7d199e1 100644 --- a/frontend/src/container/AllError/index.tsx +++ b/frontend/src/container/AllError/index.tsx @@ -23,7 +23,7 @@ import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueries } from 'react-query'; import { useSelector } from 'react-redux'; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index c31a2bf6f9..b127d6c429 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -6,7 +6,7 @@ import Header from 'container/Header'; import SideNav from 'container/SideNav'; import TopNav from 'container/TopNav'; import { useNotifications } from 'hooks/useNotifications'; -import React, { ReactNode, useEffect, useRef } from 'react'; +import { ReactNode, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueries } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; @@ -18,7 +18,7 @@ import { UPDATE_CONFIGS, UPDATE_CURRENT_ERROR, UPDATE_CURRENT_VERSION, - UPDATE_FEATURE_FLAGS, + UPDATE_FEATURE_FLAG_RESPONSE, UPDATE_LATEST_VERSION, UPDATE_LATEST_VERSION_ERROR, } from 'types/actions/app'; @@ -27,7 +27,9 @@ import AppReducer from 'types/reducer/app'; import { ChildrenContainer, Layout } from './styles'; function AppLayout(props: AppLayoutProps): JSX.Element { - const { isLoggedIn } = useSelector((state) => state.app); + const { isLoggedIn, user } = useSelector( + (state) => state.app, + ); const { pathname } = useLocation(); const { t } = useTranslation(); @@ -39,21 +41,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element { ] = useQueries([ { queryFn: getUserVersion, - queryKey: 'getUserVersion', + queryKey: ['getUserVersion', user?.accessJwt], enabled: isLoggedIn, }, { queryFn: getUserLatestVersion, - queryKey: 'getUserLatestVersion', + queryKey: ['getUserLatestVersion', user?.accessJwt], enabled: isLoggedIn, }, { queryFn: getFeaturesFlags, - queryKey: 'getFeatureFlags', + queryKey: ['getFeatureFlags', user?.accessJwt], }, { queryFn: getDynamicConfigs, - queryKey: 'getDynamicConfigs', + queryKey: ['getDynamicConfigs', user?.accessJwt], }, ]); @@ -129,19 +131,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element { message: t('oops_something_went_wrong_version'), }); } - if ( - getFeaturesResponse.isFetched && - getFeaturesResponse.isSuccess && - getFeaturesResponse.data && - getFeaturesResponse.data.payload - ) { - dispatch({ - type: UPDATE_FEATURE_FLAGS, - payload: { - ...getFeaturesResponse.data.payload, - }, - }); - } if ( getUserVersionResponse.isFetched && @@ -173,20 +162,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element { }); } - if ( - getFeaturesResponse.isFetched && - getFeaturesResponse.isSuccess && - getFeaturesResponse.data && - getFeaturesResponse.data.payload - ) { - dispatch({ - type: UPDATE_FEATURE_FLAGS, - payload: { - ...getFeaturesResponse.data.payload, - }, - }); - } - if ( getDynamicConfigsResponse.isFetched && getDynamicConfigsResponse.isSuccess && @@ -226,6 +201,29 @@ function AppLayout(props: AppLayoutProps): JSX.Element { notifications, ]); + useEffect(() => { + if ( + getFeaturesResponse.isFetched && + getFeaturesResponse.isSuccess && + getFeaturesResponse.data && + getFeaturesResponse.data.payload + ) { + dispatch({ + type: UPDATE_FEATURE_FLAG_RESPONSE, + payload: { + featureFlag: getFeaturesResponse.data.payload, + refetch: getFeaturesResponse.refetch, + }, + }); + } + }, [ + dispatch, + getFeaturesResponse.data, + getFeaturesResponse.isFetched, + getFeaturesResponse.isSuccess, + getFeaturesResponse.refetch, + ]); + const isToDisplayLayout = isLoggedIn; return ( diff --git a/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx b/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx index 84ac44e60e..fa2f471113 100644 --- a/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx +++ b/frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import { PureComponent } from 'react'; interface State { hasError: boolean; diff --git a/frontend/src/container/ConfigDropdown/Config/Link.tsx b/frontend/src/container/ConfigDropdown/Config/Link.tsx index 2cc39b7779..b31e374538 100644 --- a/frontend/src/container/ConfigDropdown/Config/Link.tsx +++ b/frontend/src/container/ConfigDropdown/Config/Link.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { ReactNode } from 'react'; import { Link } from 'react-router-dom'; function LinkContainer({ children, href }: LinkContainerProps): JSX.Element { @@ -16,7 +16,7 @@ function LinkContainer({ children, href }: LinkContainerProps): JSX.Element { } interface LinkContainerProps { - children: React.ReactNode; + children: ReactNode; href: string; } diff --git a/frontend/src/container/ConfigDropdown/Config/index.tsx b/frontend/src/container/ConfigDropdown/Config/index.tsx index 3d356a0af4..52879ce2d7 100644 --- a/frontend/src/container/ConfigDropdown/Config/index.tsx +++ b/frontend/src/container/ConfigDropdown/Config/index.tsx @@ -1,7 +1,7 @@ import { Menu, Space } from 'antd'; import Spinner from 'components/Spinner'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import React, { Suspense, useMemo } from 'react'; +import { lazy, Suspense, useMemo } from 'react'; import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs'; import ErrorLink from './ErrorLink'; @@ -17,7 +17,7 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element { const items = sortedConfig.map((item) => { const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`; - const Component = React.lazy( + const Component = lazy( () => import(`@ant-design/icons/es/icons/${iconName}.js`), ); return { diff --git a/frontend/src/container/ConfigDropdown/index.tsx b/frontend/src/container/ConfigDropdown/index.tsx index 1ddd676948..cae4d087d6 100644 --- a/frontend/src/container/ConfigDropdown/index.tsx +++ b/frontend/src/container/ConfigDropdown/index.tsx @@ -6,7 +6,7 @@ import { } from '@ant-design/icons'; import { Dropdown, Space } from 'antd'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs'; diff --git a/frontend/src/container/CreateAlertChannels/defaults.ts b/frontend/src/container/CreateAlertChannels/defaults.ts index ac15056703..e37ad6be03 100644 --- a/frontend/src/container/CreateAlertChannels/defaults.ts +++ b/frontend/src/container/CreateAlertChannels/defaults.ts @@ -1,15 +1,17 @@ import { PagerChannel } from './config'; export const PagerInitialConfig: Partial = { - description: `{{ range .Alerts -}} - *Alert:* {{ if .Annotations.title }} {{ .Annotations.title }} {{ else }} {{ .Annotations.summary }} {{end}} {{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }} - - *Description:* {{ .Annotations.description }} - - *Details:* - {{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }} - {{ end }} - {{ end }}`, + description: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }} + {{- if gt (len .CommonLabels) (len .GroupLabels) -}} + {{" "}}( + {{- with .CommonLabels.Remove .GroupLabels.Names }} + {{- range $index, $label := .SortedPairs -}} + {{ if $index }}, {{ end }} + {{- $label.Name }}="{{ $label.Value -}}" + {{- end }} + {{- end -}} + ) + {{- end }}`, severity: '{{ (index .Alerts 0).Labels.severity }}', client: 'SigNoz Alert Manager', client_url: 'https://enter-signoz-host-n-port-here/alerts', diff --git a/frontend/src/container/CreateAlertChannels/index.tsx b/frontend/src/container/CreateAlertChannels/index.tsx index a5db46da07..1261f53567 100644 --- a/frontend/src/container/CreateAlertChannels/index.tsx +++ b/frontend/src/container/CreateAlertChannels/index.tsx @@ -9,7 +9,7 @@ import ROUTES from 'constants/routes'; import FormAlertChannels from 'container/FormAlertChannels'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useCallback, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { diff --git a/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx b/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx index cc2da48727..8385c8462e 100644 --- a/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx +++ b/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx @@ -1,5 +1,4 @@ import { Row } from 'antd'; -import React from 'react'; import { useTranslation } from 'react-i18next'; import { AlertTypes } from 'types/api/alerts/alertTypes'; diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 5ce07a03be..2b85a052b2 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -1,3 +1,7 @@ +import { + initialQueryBuilderFormValues, + PANEL_TYPES, +} from 'constants/queryBuilder'; import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertDef, @@ -5,6 +9,12 @@ import { defaultEvalWindow, defaultMatchType, } from 'types/api/alerts/def'; +import { EQueryType } from 'types/common/dashboard'; +import { + DataSource, + LogsAggregatorOperator, + TracesAggregatorOperator, +} from 'types/common/queryBuilder'; const defaultAlertDescription = 'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})'; @@ -19,28 +29,24 @@ const defaultAnnotations = { export const alertDefaults: AlertDef = { alertType: AlertTypes.METRICS_BASED_ALERT, condition: { - compositeMetricQuery: { + compositeQuery: { builderQueries: { A: { - queryName: 'A', - name: 'A', - formulaOnly: false, - metricName: '', - tagFilters: { - op: 'AND', - items: [], - }, - groupBy: [], - aggregateOperator: 1, - expression: 'A', - disabled: false, - toggleDisable: false, - toggleDelete: false, + ...initialQueryBuilderFormValues, }, }, promQueries: {}, - chQueries: {}, - queryType: 1, + chQueries: { + A: { + name: 'A', + query: ``, + rawQuery: ``, + legend: '', + disabled: false, + }, + }, + queryType: EQueryType.CLICKHOUSE, + panelType: PANEL_TYPES.TIME_SERIES, }, op: defaultCompareOp, matchType: defaultMatchType, @@ -55,23 +61,12 @@ export const alertDefaults: AlertDef = { export const logAlertDefaults: AlertDef = { alertType: AlertTypes.LOGS_BASED_ALERT, condition: { - compositeMetricQuery: { + compositeQuery: { builderQueries: { A: { - queryName: 'A', - name: 'A', - formulaOnly: false, - metricName: '', - tagFilters: { - op: 'AND', - items: [], - }, - groupBy: [], - aggregateOperator: 1, - expression: 'A', - disabled: false, - toggleDisable: false, - toggleDelete: false, + ...initialQueryBuilderFormValues, + aggregateOperator: LogsAggregatorOperator.COUNT, + dataSource: DataSource.LOGS, }, }, promQueries: {}, @@ -84,7 +79,8 @@ export const logAlertDefaults: AlertDef = { disabled: false, }, }, - queryType: 2, + queryType: EQueryType.CLICKHOUSE, + panelType: PANEL_TYPES.TIME_SERIES, }, op: defaultCompareOp, matchType: '4', @@ -100,23 +96,12 @@ export const logAlertDefaults: AlertDef = { export const traceAlertDefaults: AlertDef = { alertType: AlertTypes.TRACES_BASED_ALERT, condition: { - compositeMetricQuery: { + compositeQuery: { builderQueries: { A: { - queryName: 'A', - name: 'A', - formulaOnly: false, - metricName: '', - tagFilters: { - op: 'AND', - items: [], - }, - groupBy: [], - aggregateOperator: 1, - expression: 'A', - disabled: false, - toggleDisable: false, - toggleDelete: false, + ...initialQueryBuilderFormValues, + aggregateOperator: TracesAggregatorOperator.COUNT, + dataSource: DataSource.TRACES, }, }, promQueries: {}, @@ -129,7 +114,8 @@ export const traceAlertDefaults: AlertDef = { disabled: false, }, }, - queryType: 2, + queryType: EQueryType.CLICKHOUSE, + panelType: PANEL_TYPES.TIME_SERIES, }, op: defaultCompareOp, matchType: '4', @@ -145,23 +131,12 @@ export const traceAlertDefaults: AlertDef = { export const exceptionAlertDefaults: AlertDef = { alertType: AlertTypes.EXCEPTIONS_BASED_ALERT, condition: { - compositeMetricQuery: { + compositeQuery: { builderQueries: { A: { - queryName: 'A', - name: 'A', - formulaOnly: false, - metricName: '', - tagFilters: { - op: 'AND', - items: [], - }, - groupBy: [], - aggregateOperator: 1, - expression: 'A', - disabled: false, - toggleDisable: false, - toggleDelete: false, + ...initialQueryBuilderFormValues, + aggregateOperator: TracesAggregatorOperator.COUNT, + dataSource: DataSource.TRACES, }, }, promQueries: {}, @@ -174,7 +149,8 @@ export const exceptionAlertDefaults: AlertDef = { disabled: false, }, }, - queryType: 2, + queryType: EQueryType.CLICKHOUSE, + panelType: PANEL_TYPES.TIME_SERIES, }, op: defaultCompareOp, matchType: '4', diff --git a/frontend/src/container/CreateAlertRule/index.tsx b/frontend/src/container/CreateAlertRule/index.tsx index ae3d21897e..b0cb5546c1 100644 --- a/frontend/src/container/CreateAlertRule/index.tsx +++ b/frontend/src/container/CreateAlertRule/index.tsx @@ -1,6 +1,6 @@ import { Form, Row } from 'antd'; import FormAlertRules from 'container/FormAlertRules'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { AlertTypes } from 'types/api/alerts/alertTypes'; import { diff --git a/frontend/src/container/EditAlertChannels/index.tsx b/frontend/src/container/EditAlertChannels/index.tsx index 5711635167..42ebd543c6 100644 --- a/frontend/src/container/EditAlertChannels/index.tsx +++ b/frontend/src/container/EditAlertChannels/index.tsx @@ -19,7 +19,7 @@ import { import FormAlertChannels from 'container/FormAlertChannels'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useCallback, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; diff --git a/frontend/src/container/EditRules/index.tsx b/frontend/src/container/EditRules/index.tsx index 89c9e66410..b6a32615a6 100644 --- a/frontend/src/container/EditRules/index.tsx +++ b/frontend/src/container/EditRules/index.tsx @@ -1,6 +1,5 @@ import { Form } from 'antd'; import FormAlertRules from 'container/FormAlertRules'; -import React from 'react'; import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertDef } from 'types/api/alerts/def'; diff --git a/frontend/src/container/ErrorDetails/index.tsx b/frontend/src/container/ErrorDetails/index.tsx index 999e8013d9..561114a289 100644 --- a/frontend/src/container/ErrorDetails/index.tsx +++ b/frontend/src/container/ErrorDetails/index.tsx @@ -7,7 +7,7 @@ import dayjs from 'dayjs'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import { urlKey } from 'pages/ErrorDetails/utils'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; import { useLocation } from 'react-router-dom'; diff --git a/frontend/src/container/FormAlertChannels/Settings/LabelFilter.tsx b/frontend/src/container/FormAlertChannels/Settings/LabelFilter.tsx index 21311e416d..cc6c45843d 100644 --- a/frontend/src/container/FormAlertChannels/Settings/LabelFilter.tsx +++ b/frontend/src/container/FormAlertChannels/Settings/LabelFilter.tsx @@ -1,6 +1,6 @@ import { Form, Input, Select } from 'antd'; import { LabelFilterStatement } from 'container/CreateAlertChannels/config'; -import React from 'react'; +import { Dispatch, SetStateAction } from 'react'; const { Option } = Select; @@ -55,9 +55,7 @@ function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element { } export interface LabelFilterProps { - setFilter: React.Dispatch< - React.SetStateAction>> - >; + setFilter: Dispatch>>>; } export default LabelFilterForm; diff --git a/frontend/src/container/FormAlertChannels/Settings/Pager.tsx b/frontend/src/container/FormAlertChannels/Settings/Pager.tsx index 5c0ca2e7c8..ec228f4b8d 100644 --- a/frontend/src/container/FormAlertChannels/Settings/Pager.tsx +++ b/frontend/src/container/FormAlertChannels/Settings/Pager.tsx @@ -1,5 +1,5 @@ import { Form, Input } from 'antd'; -import React from 'react'; +import { Dispatch, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; import { PagerChannel } from '../../CreateAlertChannels/config'; @@ -148,7 +148,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element { } interface PagerFormProps { - setSelectedConfig: React.Dispatch>>; + setSelectedConfig: Dispatch>>; } export default PagerForm; diff --git a/frontend/src/container/FormAlertChannels/Settings/Slack.tsx b/frontend/src/container/FormAlertChannels/Settings/Slack.tsx index 9415a500d7..c344df8ff5 100644 --- a/frontend/src/container/FormAlertChannels/Settings/Slack.tsx +++ b/frontend/src/container/FormAlertChannels/Settings/Slack.tsx @@ -1,5 +1,5 @@ import { Form, Input } from 'antd'; -import React from 'react'; +import { Dispatch, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; import { SlackChannel } from '../../CreateAlertChannels/config'; @@ -66,7 +66,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element { } interface SlackProps { - setSelectedConfig: React.Dispatch>>; + setSelectedConfig: Dispatch>>; } export default Slack; diff --git a/frontend/src/container/FormAlertChannels/Settings/Webhook.tsx b/frontend/src/container/FormAlertChannels/Settings/Webhook.tsx index 537d826313..ead1464734 100644 --- a/frontend/src/container/FormAlertChannels/Settings/Webhook.tsx +++ b/frontend/src/container/FormAlertChannels/Settings/Webhook.tsx @@ -1,5 +1,5 @@ import { Form, Input } from 'antd'; -import React from 'react'; +import { Dispatch, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; import { WebhookChannel } from '../../CreateAlertChannels/config'; @@ -53,9 +53,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element { } interface WebhookProps { - setSelectedConfig: React.Dispatch< - React.SetStateAction> - >; + setSelectedConfig: Dispatch>>; } export default WebhookSettings; diff --git a/frontend/src/container/FormAlertChannels/index.tsx b/frontend/src/container/FormAlertChannels/index.tsx index 91de745b3b..14f6d32413 100644 --- a/frontend/src/container/FormAlertChannels/index.tsx +++ b/frontend/src/container/FormAlertChannels/index.tsx @@ -11,7 +11,7 @@ import { WebhookType, } from 'container/CreateAlertChannels/config'; import history from 'lib/history'; -import React from 'react'; +import { Dispatch, ReactElement, SetStateAction } from 'react'; import { useTranslation } from 'react-i18next'; import PagerSettings from './Settings/Pager'; @@ -37,7 +37,7 @@ function FormAlertChannels({ }: FormAlertChannelsProps): JSX.Element { const { t } = useTranslation('channels'); - const renderSettings = (): React.ReactElement | null => { + const renderSettings = (): ReactElement | null => { switch (type) { case SlackType: return ; @@ -115,8 +115,8 @@ function FormAlertChannels({ interface FormAlertChannelsProps { formInstance: FormInstance; type: ChannelType; - setSelectedConfig: React.Dispatch< - React.SetStateAction> + setSelectedConfig: Dispatch< + SetStateAction> >; onTypeChangeHandler: (value: ChannelType) => void; onSaveHandler: (props: ChannelType) => void; diff --git a/frontend/src/container/FormAlertRules/BasicInfo.tsx b/frontend/src/container/FormAlertRules/BasicInfo.tsx index 130f4fb3dd..f4d99126ec 100644 --- a/frontend/src/container/FormAlertRules/BasicInfo.tsx +++ b/frontend/src/container/FormAlertRules/BasicInfo.tsx @@ -1,5 +1,4 @@ import { Form, Select } from 'antd'; -import React from 'react'; import { useTranslation } from 'react-i18next'; import { AlertDef, Labels } from 'types/api/alerts/def'; diff --git a/frontend/src/container/FormAlertRules/ChQuerySection/ChQuerySection.tsx b/frontend/src/container/FormAlertRules/ChQuerySection/ChQuerySection.tsx index aa316fe558..fc8fb5dc09 100644 --- a/frontend/src/container/FormAlertRules/ChQuerySection/ChQuerySection.tsx +++ b/frontend/src/container/FormAlertRules/ChQuerySection/ChQuerySection.tsx @@ -1,6 +1,5 @@ import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query'; import { IClickHouseQueryHandleChange } from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/types'; -import React from 'react'; import { IChQueries } from 'types/api/alerts/compositeQuery'; import { rawQueryToIChQuery, toIClickHouseQuery } from './transform'; diff --git a/frontend/src/container/FormAlertRules/ChannelSelect/index.tsx b/frontend/src/container/FormAlertRules/ChannelSelect/index.tsx index 662a14c1e6..c2d78e661c 100644 --- a/frontend/src/container/FormAlertRules/ChannelSelect/index.tsx +++ b/frontend/src/container/FormAlertRules/ChannelSelect/index.tsx @@ -2,7 +2,7 @@ import { Select } from 'antd'; import getChannels from 'api/channels/getAll'; import useFetch from 'hooks/useFetch'; import { useNotifications } from 'hooks/useNotifications'; -import React from 'react'; +import { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { StyledSelect } from './styles'; @@ -33,8 +33,8 @@ function ChannelSelect({ description: errorMessage, }); } - const renderOptions = (): React.ReactNode[] => { - const children: React.ReactNode[] = []; + const renderOptions = (): ReactNode[] => { + const children: ReactNode[] = []; if (loading || payload === undefined || payload.length === 0) { return children; diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index e7ee323dce..f358c6cbe4 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -1,12 +1,13 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { StaticLineProps } from 'components/Graph'; import Spinner from 'components/Spinner'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import GridGraphComponent from 'container/GridGraphComponent'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; import getChartData from 'lib/getChartData'; -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults'; @@ -32,7 +33,7 @@ interface QueryResponseError { function ChartPreview({ name, query, - graphType = 'TIME_SERIES', + graphType = PANEL_TYPES.TIME_SERIES, selectedTime = 'GLOBAL_TIME', selectedInterval = '5min', headline, @@ -59,15 +60,16 @@ function ChartPreview({ switch (query?.queryType) { case EQueryType.PROM: - return query.promQL?.length > 0 && query.promQL[0].query !== ''; + return query.promql?.length > 0 && query.promql[0].query !== ''; case EQueryType.CLICKHOUSE: return ( - query.clickHouse?.length > 0 && query.clickHouse[0].rawQuery?.length > 0 + query.clickhouse_sql?.length > 0 && + query.clickhouse_sql[0].rawQuery?.length > 0 ); case EQueryType.QUERY_BUILDER: return ( - query.metricsBuilder?.queryBuilder?.length > 0 && - query.metricsBuilder?.queryBuilder[0].metricName !== '' + query.builder.queryData.length > 0 && + query.builder.queryData[0].queryName !== '' ); default: return false; @@ -83,13 +85,13 @@ function ChartPreview({ queryFn: () => GetMetricQueryRange({ query: query || { - queryType: 1, - promQL: [], - metricsBuilder: { - formulas: [], - queryBuilder: [], + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: { + queryFormulas: [], + queryData: [], }, - clickHouse: [], + clickhouse_sql: [], }, globalSelectedInterval: selectedInterval, graphType, @@ -127,7 +129,7 @@ function ChartPreview({ title={name} data={chartDataSet} isStacked - GRAPH_TYPES={graphType || 'TIME_SERIES'} + GRAPH_TYPES={graphType || PANEL_TYPES.TIME_SERIES} name={name || 'Chart Preview'} staticLine={staticLine} /> @@ -137,7 +139,7 @@ function ChartPreview({ } ChartPreview.defaultProps = { - graphType: 'TIME_SERIES', + graphType: PANEL_TYPES.TIME_SERIES, selectedTime: 'GLOBAL_TIME', selectedInterval: '5min', headline: undefined, diff --git a/frontend/src/container/FormAlertRules/PromqlSection.tsx b/frontend/src/container/FormAlertRules/PromqlSection.tsx index 129e5bb92d..df2fd860d0 100644 --- a/frontend/src/container/FormAlertRules/PromqlSection.tsx +++ b/frontend/src/container/FormAlertRules/PromqlSection.tsx @@ -1,6 +1,5 @@ import PromQLQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query'; import { IPromQLQueryHandleChange } from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/types'; -import React from 'react'; import { IPromQueries } from 'types/api/alerts/compositeQuery'; function PromqlSection({ diff --git a/frontend/src/container/FormAlertRules/QuerySection.tsx b/frontend/src/container/FormAlertRules/QuerySection.tsx index ab39c51083..b24d1ab7a3 100644 --- a/frontend/src/container/FormAlertRules/QuerySection.tsx +++ b/frontend/src/container/FormAlertRules/QuerySection.tsx @@ -1,35 +1,20 @@ -import { PlusOutlined } from '@ant-design/icons'; import { Button, Tabs } from 'antd'; -import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula'; -import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query'; -import { - IQueryBuilderFormulaHandleChange, - IQueryBuilderQueryHandleChange, -} from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types'; -import { useNotifications } from 'hooks/useNotifications'; -import React, { useCallback } from 'react'; +import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { QueryBuilder } from 'container/QueryBuilder'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { AlertTypes } from 'types/api/alerts/alertTypes'; -import { - IChQueries, - IFormulaQueries, - IMetricQueries, - IPromQueries, -} from 'types/api/alerts/compositeQuery'; -import { EAggregateOperator, EQueryType } from 'types/common/dashboard'; +import { IChQueries, IPromQueries } from 'types/api/alerts/compositeQuery'; +import { EQueryType } from 'types/common/dashboard'; import ChQuerySection from './ChQuerySection'; import PromqlSection from './PromqlSection'; -import { FormContainer, QueryButton, StepHeading } from './styles'; -import { toIMetricsBuilderQuery } from './utils'; +import { FormContainer, StepHeading } from './styles'; function QuerySection({ queryCategory, setQueryCategory, - metricQueries, - setMetricQueries, - formulaQueries, - setFormulaQueries, promQueries, setPromQueries, chQueries, @@ -40,9 +25,9 @@ function QuerySection({ // init namespace for translations const { t } = useTranslation('alerts'); - const handleQueryCategoryChange = (s: string): void => { + const handleQueryCategoryChange = (queryType: string): void => { if ( - parseInt(s, 10) === EQueryType.PROM && + queryType === EQueryType.PROM && (!promQueries || Object.keys(promQueries).length === 0) ) { setPromQueries({ @@ -57,7 +42,7 @@ function QuerySection({ } if ( - parseInt(s, 10) === EQueryType.CLICKHOUSE && + queryType === EQueryType.CLICKHOUSE && (!chQueries || Object.keys(chQueries).length === 0) ) { setChQueries({ @@ -70,148 +55,9 @@ function QuerySection({ }, }); } - setQueryCategory(parseInt(s, 10)); + setQueryCategory(queryType as EQueryType); }; - const getNextQueryLabel = useCallback((): string => { - let maxAscii = 0; - - Object.keys(metricQueries).forEach((key) => { - const n = key.charCodeAt(0); - if (n > maxAscii) { - maxAscii = n - 64; - } - }); - - return String.fromCharCode(64 + maxAscii + 1); - }, [metricQueries]); - - const handleFormulaChange = ({ - formulaIndex, - expression, - legend, - toggleDisable, - toggleDelete, - }: IQueryBuilderFormulaHandleChange): void => { - const allFormulas = formulaQueries; - const current = allFormulas[formulaIndex]; - if (expression !== undefined) { - current.expression = expression; - } - - if (legend !== undefined) { - current.legend = legend; - } - - if (toggleDisable) { - current.disabled = !current.disabled; - } - - if (toggleDelete) { - delete allFormulas[formulaIndex]; - } else { - allFormulas[formulaIndex] = current; - } - - setFormulaQueries({ - ...allFormulas, - }); - }; - - const handleMetricQueryChange = ({ - queryIndex, - aggregateFunction, - metricName, - tagFilters, - groupBy, - legend, - toggleDisable, - toggleDelete, - }: IQueryBuilderQueryHandleChange): void => { - const allQueries = metricQueries; - const current = metricQueries[queryIndex]; - if (aggregateFunction) { - current.aggregateOperator = aggregateFunction; - } - if (metricName) { - current.metricName = metricName; - } - - if (tagFilters && current.tagFilters) { - current.tagFilters.items = tagFilters; - } - - if (legend) { - current.legend = legend; - } - - if (groupBy) { - current.groupBy = groupBy; - } - - if (toggleDisable) { - current.disabled = !current.disabled; - } - - if (toggleDelete) { - delete allQueries[queryIndex]; - } else { - allQueries[queryIndex] = current; - } - - setMetricQueries({ - ...allQueries, - }); - }; - const { notifications } = useNotifications(); - - const addMetricQuery = useCallback(() => { - if (Object.keys(metricQueries).length > 5) { - notifications.error({ - message: t('metric_query_max_limit'), - }); - return; - } - - const queryLabel = getNextQueryLabel(); - - const queries = metricQueries; - queries[queryLabel] = { - name: queryLabel, - queryName: queryLabel, - metricName: '', - formulaOnly: false, - aggregateOperator: EAggregateOperator.NOOP, - legend: '', - tagFilters: { - op: 'AND', - items: [], - }, - groupBy: [], - disabled: false, - expression: queryLabel, - }; - setMetricQueries({ ...queries }); - }, [t, getNextQueryLabel, metricQueries, setMetricQueries, notifications]); - - const addFormula = useCallback(() => { - // defaulting to F1 as only one formula is supported - // in alert definition - const queryLabel = 'F1'; - - const formulas = formulaQueries; - formulas[queryLabel] = { - queryName: queryLabel, - name: queryLabel, - formulaOnly: true, - expression: 'A', - disabled: false, - legend: '', - }; - - setFormulaQueries({ ...formulas }); - }, [formulaQueries, setFormulaQueries]); - const renderPromqlUI = (): JSX.Element => ( ); @@ -220,61 +66,14 @@ function QuerySection({ ); - const renderFormulaButton = (): JSX.Element => ( - }> - {t('button_formula')} - - ); - - const renderQueryButton = (): JSX.Element => ( - }> - {t('button_query')} - - ); - const renderMetricUI = (): JSX.Element => ( -
- {metricQueries && - Object.keys(metricQueries).map((key: string) => { - // todo(amol): need to handle this in fetch - const current = metricQueries[key]; - current.name = key; - - return ( - - ); - })} - - {queryCategory !== EQueryType.PROM && renderQueryButton()} -
- {formulaQueries && - Object.keys(formulaQueries).map((key: string) => { - // todo(amol): need to handle this in fetch - const current = formulaQueries[key]; - current.name = key; - - return ( - - ); - })} - {queryCategory === EQueryType.QUERY_BUILDER && - (!formulaQueries || Object.keys(formulaQueries).length === 0) && - metricQueries && - Object.keys(metricQueries).length > 0 && - renderFormulaButton()} -
-
+ ); const handleRunQuery = (): void => { @@ -284,20 +83,22 @@ function QuerySection({ const tabs = [ { label: t('tab_qb'), - key: EQueryType.QUERY_BUILDER.toString(), - disabled: true, + key: EQueryType.QUERY_BUILDER, }, { label: t('tab_chquery'), - key: EQueryType.CLICKHOUSE.toString(), + key: EQueryType.CLICKHOUSE, }, ]; - const items = [ - { label: t('tab_qb'), key: EQueryType.QUERY_BUILDER.toString() }, - { label: t('tab_chquery'), key: EQueryType.CLICKHOUSE.toString() }, - { label: t('tab_promql'), key: EQueryType.PROM.toString() }, - ]; + const items = useMemo( + () => [ + { label: t('tab_qb'), key: EQueryType.QUERY_BUILDER }, + { label: t('tab_chquery'), key: EQueryType.CLICKHOUSE }, + { label: t('tab_promql'), key: EQueryType.PROM }, + ], + [t], + ); const renderTabs = (typ: AlertTypes): JSX.Element | null => { switch (typ) { @@ -308,16 +109,14 @@ function QuerySection({ - {queryCategory === EQueryType.CLICKHOUSE && ( - - )} + } items={tabs} @@ -329,16 +128,14 @@ function QuerySection({ - {queryCategory === EQueryType.CLICKHOUSE && ( - - )} + } items={items} @@ -372,10 +169,6 @@ function QuerySection({ interface QuerySectionProps { queryCategory: EQueryType; setQueryCategory: (n: EQueryType) => void; - metricQueries: IMetricQueries; - setMetricQueries: (b: IMetricQueries) => void; - formulaQueries: IFormulaQueries; - setFormulaQueries: (b: IFormulaQueries) => void; promQueries: IPromQueries; setPromQueries: (p: IPromQueries) => void; chQueries: IChQueries; diff --git a/frontend/src/container/FormAlertRules/RuleOptions.tsx b/frontend/src/container/FormAlertRules/RuleOptions.tsx index 071ff84d7f..f24f38b017 100644 --- a/frontend/src/container/FormAlertRules/RuleOptions.tsx +++ b/frontend/src/container/FormAlertRules/RuleOptions.tsx @@ -1,5 +1,4 @@ import { Form, Select, Typography } from 'antd'; -import React from 'react'; import { useTranslation } from 'react-i18next'; import { AlertDef, diff --git a/frontend/src/container/FormAlertRules/UserGuide/index.tsx b/frontend/src/container/FormAlertRules/UserGuide/index.tsx index 33312a9fd7..86992d7226 100644 --- a/frontend/src/container/FormAlertRules/UserGuide/index.tsx +++ b/frontend/src/container/FormAlertRules/UserGuide/index.tsx @@ -1,6 +1,5 @@ import { Col, Row, Typography } from 'antd'; import TextToolTip from 'components/TextToolTip'; -import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { EQueryType } from 'types/common/dashboard'; diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 9a3ea93466..44dd192729 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -1,22 +1,22 @@ import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons'; -import { Col, FormInstance, Modal, Typography } from 'antd'; +import { Col, FormInstance, Modal, Tooltip, Typography } from 'antd'; import saveAlertApi from 'api/alerts/save'; import testAlertApi from 'api/alerts/testAlert'; +import { FeatureKeys } from 'constants/features'; import ROUTES from 'constants/routes'; import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag'; import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useCallback, useEffect, useState } from 'react'; +import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; +import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi'; +import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from 'react-query'; import { AlertTypes } from 'types/api/alerts/alertTypes'; -import { - IChQueries, - IFormulaQueries, - IMetricQueries, - IPromQueries, -} from 'types/api/alerts/compositeQuery'; +import { IChQueries, IPromQueries } from 'types/api/alerts/compositeQuery'; import { AlertDef, defaultEvalWindow, @@ -36,15 +36,8 @@ import { PanelContainer, StyledLeftContainer, } from './styles'; -import useDebounce from './useDebounce'; import UserGuide from './UserGuide'; -import { - prepareBuilderQueries, - prepareStagedQuery, - toChartInterval, - toFormulaQueries, - toMetricQueries, -} from './utils'; +import { prepareStagedQuery, toChartInterval } from './utils'; function FormAlertRules({ alertType, @@ -55,33 +48,21 @@ function FormAlertRules({ // init namespace for translations const { t } = useTranslation('alerts'); + const { queryBuilderData, initQueryBuilderData } = useQueryBuilder(); + // use query client const ruleCache = useQueryClient(); const [loading, setLoading] = useState(false); - // queryRunId helps to override of query caching for clickhouse query - // tab. A random string will be assigned for each execution - const [runQueryId, setRunQueryId] = useState(); - // alertDef holds the form values to be posted const [alertDef, setAlertDef] = useState(initialValue); // initQuery contains initial query when component was mounted - const initQuery = initialValue?.condition?.compositeMetricQuery; + const initQuery = initialValue.condition.compositeQuery; const [queryCategory, setQueryCategory] = useState( - initQuery?.queryType, - ); - - // local state to handle metric queries - const [metricQueries, setMetricQueries] = useState( - toMetricQueries(initQuery?.builderQueries), - ); - - // local state to handle formula queries - const [formulaQueries, setFormulaQueries] = useState( - toFormulaQueries(initQuery?.builderQueries), + initQuery.queryType, ); // local state to handle promql queries @@ -106,43 +87,31 @@ function FormAlertRules({ // run query button is provided. const [manualStagedQuery, setManualStagedQuery] = useState(); - // delay to reduce load on backend api with auto-run query. only for clickhouse - // queries we have manual run, hence both debounce and debounceStagedQuery are not required - const debounceDelay = queryCategory !== EQueryType.CLICKHOUSE ? 1000 : 0; - - // debounce query to delay backend api call and chart update. - // used in query builder and promql tabs to enable auto-refresh - // of chart on user edit - const debouncedStagedQuery = useDebounce(stagedQuery, debounceDelay); - // this use effect initiates staged query and // other queries based on server data. // useful when fetching of initial values (from api) // is delayed useEffect(() => { - const initQuery = initialValue?.condition?.compositeMetricQuery; - const typ = initQuery?.queryType; + const initQuery = initialValue?.condition?.compositeQuery; + const type = initQuery.queryType; - // extract metric query from builderQueries - const mq = toMetricQueries(initQuery?.builderQueries); - - // extract formula query from builderQueries - const fq = toFormulaQueries(initQuery?.builderQueries); + const builderData = mapQueryDataFromApi( + initialValue?.condition?.compositeQuery?.builderQueries || {}, + ); // prepare staged query const sq = prepareStagedQuery( - typ, - mq, - fq, + type, + builderData.queryData, + builderData.queryFormulas, initQuery?.promQueries, initQuery?.chQueries, ); const pq = initQuery?.promQueries; const chq = initQuery?.chQueries; - setQueryCategory(typ); - setMetricQueries(mq); - setFormulaQueries(fq); + setQueryCategory(type); + initQueryBuilderData(builderData); setPromQueries(pq); setStagedQuery(sq); @@ -151,7 +120,7 @@ function FormAlertRules({ setChQueries(chq); setAlertDef(initialValue); - }, [initialValue]); + }, [initialValue, initQueryBuilderData]); // this useEffect updates staging query when // any of its sub-parameters changes @@ -159,16 +128,15 @@ function FormAlertRules({ // prepare staged query const sq: StagedQuery = prepareStagedQuery( queryCategory, - metricQueries, - formulaQueries, + queryBuilderData.queryData, + queryBuilderData.queryFormulas, promQueries, chQueries, ); setStagedQuery(sq); - }, [queryCategory, chQueries, metricQueries, formulaQueries, promQueries]); + }, [queryCategory, chQueries, queryBuilderData, promQueries]); const onRunQuery = (): void => { - setRunQueryId(Math.random().toString(36).substring(2, 15)); setManualStagedQuery(stagedQuery); }; @@ -179,6 +147,7 @@ function FormAlertRules({ // onQueryCategoryChange handles changes to query category // in state as well as sets additional defaults const onQueryCategoryChange = (val: EQueryType): void => { + console.log('onQueryCategoryChange', val); setQueryCategory(val); if (val === EQueryType.PROM) { setAlertDef({ @@ -190,6 +159,15 @@ function FormAlertRules({ evalWindow: defaultEvalWindow, }); } + + const sq: StagedQuery = prepareStagedQuery( + val, + queryBuilderData.queryData, + queryBuilderData.queryFormulas, + promQueries, + chQueries, + ); + setManualStagedQuery(sq); }; const { notifications } = useNotifications(); @@ -244,10 +222,9 @@ function FormAlertRules({ }, [t, chQueries, queryCategory, notifications]); const validateQBParams = useCallback((): boolean => { - let retval = true; if (queryCategory !== EQueryType.QUERY_BUILDER) return true; - if (!metricQueries || Object.keys(metricQueries).length === 0) { + if (!queryBuilderData.queryData || queryBuilderData.queryData.length === 0) { notifications.error({ message: 'Error', description: t('condition_required'), @@ -255,7 +232,7 @@ function FormAlertRules({ return false; } - if (!alertDef.condition?.target) { + if (alertDef.condition?.target !== 0 && !alertDef.condition?.target) { notifications.error({ message: 'Error', description: t('target_missing'), @@ -263,27 +240,8 @@ function FormAlertRules({ return false; } - Object.keys(metricQueries).forEach((key) => { - if (metricQueries[key].metricName === '') { - notifications.error({ - message: 'Error', - description: t('metricname_missing', { where: metricQueries[key].name }), - }); - retval = false; - } - }); - - Object.keys(formulaQueries).forEach((key) => { - if (formulaQueries[key].expression === '') { - notifications.error({ - message: 'Error', - description: t('expression_missing', formulaQueries[key].name), - }); - retval = false; - } - }); - return retval; - }, [t, alertDef, queryCategory, metricQueries, formulaQueries, notifications]); + return true; + }, [t, alertDef, queryCategory, queryBuilderData, notifications]); const isFormValid = useCallback((): boolean => { if (!alertDef.alert || alertDef.alert === '') { @@ -321,11 +279,12 @@ function FormAlertRules({ queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule', condition: { ...alertDef.condition, - compositeMetricQuery: { - builderQueries: prepareBuilderQueries(metricQueries, formulaQueries), + compositeQuery: { + builderQueries: mapQueryDataToApi(queryBuilderData).data, promQueries, chQueries, queryType: queryCategory, + panelType: initQuery.panelType, }, }, }; @@ -335,13 +294,17 @@ function FormAlertRules({ const memoizedPreparePostData = useCallback(preparePostData, [ queryCategory, alertDef, - metricQueries, - formulaQueries, + queryBuilderData, promQueries, chQueries, alertType, + initQuery, ]); + const isAlertAvialable = useIsFeatureDisabled( + FeatureKeys.QUERY_BUILDER_ALERTS, + ); + const saveRule = useCallback(async () => { if (!isFormValid()) { return; @@ -458,7 +421,7 @@ function FormAlertRules({ headline={} name="" threshold={alertDef.condition?.target} - query={debouncedStagedQuery} + query={manualStagedQuery} selectedInterval={toChartInterval(alertDef.evalWindow)} /> ); @@ -468,7 +431,7 @@ function FormAlertRules({ headline={} name="Chart Preview" threshold={alertDef.condition?.target} - query={debouncedStagedQuery} + query={manualStagedQuery} /> ); @@ -478,10 +441,15 @@ function FormAlertRules({ name="Chart Preview" threshold={alertDef.condition?.target} query={manualStagedQuery} - userQueryKey={runQueryId} selectedInterval={toChartInterval(alertDef.evalWindow)} /> ); + + const isNewRule = ruleId === 0; + + const isAlertAvialableToSave = + isAlertAvialable && isNewRule && queryCategory === EQueryType.QUERY_BUILDER; + return ( <> {Element} @@ -498,10 +466,6 @@ function FormAlertRules({ - } - > - {ruleId > 0 ? t('button_savechanges') : t('button_createrule')} - + + } + disabled={isAlertAvialableToSave} + > + {isNewRule ? t('button_createrule') : t('button_savechanges')} + + + ): void => { + const handleChange = (e: ChangeEvent): void => { setCurrentVal(e.target?.value); }; diff --git a/frontend/src/container/FormAlertRules/styles.ts b/frontend/src/container/FormAlertRules/styles.ts index 23cf11c8ac..e5cd42001d 100644 --- a/frontend/src/container/FormAlertRules/styles.ts +++ b/frontend/src/container/FormAlertRules/styles.ts @@ -82,7 +82,6 @@ export const InputSmall = styled(Input)` `; export const FormContainer = styled(Card)` - padding: 2em; margin-top: 1rem; display: flex; flex-direction: column; diff --git a/frontend/src/container/FormAlertRules/useDebounce.js b/frontend/src/container/FormAlertRules/useDebounce.js deleted file mode 100644 index e430f55d63..0000000000 --- a/frontend/src/container/FormAlertRules/useDebounce.js +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable */ -// @ts-ignore -// @ts-nocheck - -import { useEffect, useState } from 'react'; - -// see https://github.com/tannerlinsley/react-query/issues/293 -// see https://usehooks.com/useDebounce/ -export default function useDebounce(value, delay) { - // State and setters for debounced value - const [debouncedValue, setDebouncedValue] = useState(value); - - useEffect( - () => { - // Update debounced value after delay - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - // Cancel the timeout if value changes (also on delay change or unmount) - // This is how we prevent debounced value from updating if value is changed ... - // .. within the delay period. Timeout gets cleared and restarted. - return () => { - clearTimeout(handler); - }; - }, - [value, delay] // Only re-call effect if value or delay changes - ); - - return debouncedValue; -} diff --git a/frontend/src/container/FormAlertRules/utils.ts b/frontend/src/container/FormAlertRules/utils.ts index a3978a9007..abbfb55df5 100644 --- a/frontend/src/container/FormAlertRules/utils.ts +++ b/frontend/src/container/FormAlertRules/utils.ts @@ -1,102 +1,27 @@ import { Time } from 'container/TopNav/DateTimeSelection/config'; import { - IBuilderQueries, IChQueries, IChQuery, - IFormulaQueries, - IFormulaQuery, - IMetricQueries, - IMetricQuery, IPromQueries, IPromQuery, } from 'types/api/alerts/compositeQuery'; +import { Query as IStagedQuery } from 'types/api/dashboard/getAll'; import { - IMetricsBuilderQuery, - Query as IStagedQuery, -} from 'types/api/dashboard/getAll'; + IBuilderFormula, + IBuilderQuery, +} from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; -export const toFormulaQueries = (b: IBuilderQueries): IFormulaQueries => { - const f: IFormulaQueries = {}; - if (!b) return f; - Object.keys(b).forEach((key) => { - if (key === 'F1') { - f[key] = b[key] as IFormulaQuery; - } - }); - - return f; -}; - -export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => { - const m: IMetricQueries = {}; - if (!b) return m; - Object.keys(b).forEach((key) => { - if (key !== 'F1') { - m[key] = b[key] as IMetricQuery; - } - }); - - return m; -}; - -export const toIMetricsBuilderQuery = ( - q: IMetricQuery, -): IMetricsBuilderQuery => ({ - name: q.name, - metricName: q.metricName, - tagFilters: q.tagFilters, - groupBy: q.groupBy, - aggregateOperator: q.aggregateOperator, - disabled: q.disabled, - legend: q.legend, -}); - -export const prepareBuilderQueries = ( - m: IMetricQueries, - f: IFormulaQueries, -): IBuilderQueries => { - if (!m) return {}; - const b: IBuilderQueries = { - ...m, - }; - - Object.keys(f).forEach((key) => { - b[key] = { - ...f[key], - aggregateOperator: undefined, - metricName: '', - }; - }); - return b; -}; - export const prepareStagedQuery = ( t: EQueryType, - m: IMetricQueries, - f: IFormulaQueries, + m: IBuilderQuery[], + f: IBuilderFormula[], p: IPromQueries, c: IChQueries, ): IStagedQuery => { - const qbList: IMetricQuery[] = []; - const formulaList: IFormulaQuery[] = []; const promList: IPromQuery[] = []; const chQueryList: IChQuery[] = []; - // convert map[string]IMetricQuery to IMetricQuery[] - if (m) { - Object.keys(m).forEach((key) => { - qbList.push(m[key]); - }); - } - - // convert map[string]IFormulaQuery to IFormulaQuery[] - if (f) { - Object.keys(f).forEach((key) => { - formulaList.push(f[key]); - }); - } - // convert map[string]IPromQuery to IPromQuery[] if (p) { Object.keys(p).forEach((key) => { @@ -112,12 +37,12 @@ export const prepareStagedQuery = ( return { queryType: t, - promQL: promList, - metricsBuilder: { - formulas: formulaList, - queryBuilder: qbList, + promql: promList, + builder: { + queryFormulas: f, + queryData: m, }, - clickHouse: chQueryList, + clickhouse_sql: chQueryList, }; }; diff --git a/frontend/src/container/GantChart/SpanLength/index.tsx b/frontend/src/container/GantChart/SpanLength/index.tsx index efe62deaff..9e3611bb01 100644 --- a/frontend/src/container/GantChart/SpanLength/index.tsx +++ b/frontend/src/container/GantChart/SpanLength/index.tsx @@ -1,6 +1,5 @@ import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import React from 'react'; import { toFixed } from 'utils/toFixed'; import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles'; diff --git a/frontend/src/container/GantChart/SpanName/index.tsx b/frontend/src/container/GantChart/SpanName/index.tsx index 7f536624b9..90c6ffa80d 100644 --- a/frontend/src/container/GantChart/SpanName/index.tsx +++ b/frontend/src/container/GantChart/SpanName/index.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { Container, Service, Span, SpanWrapper } from './styles'; function SpanNameComponent({ diff --git a/frontend/src/container/GantChart/Trace/index.tsx b/frontend/src/container/GantChart/Trace/index.tsx index e419283757..d38e3594ae 100644 --- a/frontend/src/container/GantChart/Trace/index.tsx +++ b/frontend/src/container/GantChart/Trace/index.tsx @@ -4,7 +4,15 @@ import { StyledCol, StyledRow } from 'components/Styled'; import { IIntervalUnit } from 'container/TraceDetail/utils'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { + Dispatch, + MouseEventHandler, + SetStateAction, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { ITraceTree } from 'types/api/trace/getTraceItem'; import { ITraceMetaData } from '..'; @@ -71,7 +79,7 @@ function Trace(props: TraceProps): JSX.Element { const ref = useRef(null); - React.useEffect(() => { + useEffect(() => { if (activeSelectedId === id) { ref.current?.scrollIntoView({ block: 'nearest', @@ -97,7 +105,7 @@ function Trace(props: TraceProps): JSX.Element { setActiveSelectedId(id); }; - const onClickTreeExpansion: React.MouseEventHandler = ( + const onClickTreeExpansion: MouseEventHandler = ( event, ): void => { event.stopPropagation(); @@ -207,8 +215,8 @@ interface ITraceGlobal { interface TraceProps extends ITraceTree, ITraceGlobal { activeHoverId: string; - setActiveHoverId: React.Dispatch>; - setActiveSelectedId: React.Dispatch>; + setActiveHoverId: Dispatch>; + setActiveSelectedId: Dispatch>; activeSelectedId: string; level: number; activeSpanPath: string[]; diff --git a/frontend/src/container/GantChart/index.tsx b/frontend/src/container/GantChart/index.tsx index dbe707c2d7..0d54a837f2 100644 --- a/frontend/src/container/GantChart/index.tsx +++ b/frontend/src/container/GantChart/index.tsx @@ -1,6 +1,6 @@ import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons'; import { IIntervalUnit } from 'container/TraceDetail/utils'; -import React, { useEffect, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { ITraceTree } from 'types/api/trace/getTraceItem'; import { CardContainer, CardWrapper, CollapseButton } from './styles'; @@ -79,8 +79,8 @@ export interface GanttChartProps { traceMetaData: ITraceMetaData; activeSelectedId: string; activeHoverId: string; - setActiveHoverId: React.Dispatch>; - setActiveSelectedId: React.Dispatch>; + setActiveHoverId: Dispatch>; + setActiveSelectedId: Dispatch>; spanId: string; intervalUnit: IIntervalUnit; } diff --git a/frontend/src/container/GeneralSettings/GeneralSettings.tsx b/frontend/src/container/GeneralSettings/GeneralSettings.tsx index 7530364d65..5491e1394b 100644 --- a/frontend/src/container/GeneralSettings/GeneralSettings.tsx +++ b/frontend/src/container/GeneralSettings/GeneralSettings.tsx @@ -6,7 +6,7 @@ import TextToolTip from 'components/TextToolTip'; import useComponentPermission from 'hooks/useComponentPermission'; import { useNotifications } from 'hooks/useNotifications'; import find from 'lodash-es/find'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; @@ -519,7 +519,7 @@ function GeneralSettings({ category.retentionFields.length > 0 ) { return ( - + @@ -576,7 +576,7 @@ function GeneralSettings({ - + ); } return null; diff --git a/frontend/src/container/GeneralSettings/Retention.tsx b/frontend/src/container/GeneralSettings/Retention.tsx index 806f811bba..6228391503 100644 --- a/frontend/src/container/GeneralSettings/Retention.tsx +++ b/frontend/src/container/GeneralSettings/Retention.tsx @@ -1,6 +1,13 @@ import { Col, Row, Select } from 'antd'; import { find } from 'lodash-es'; -import React, { useEffect, useRef, useState } from 'react'; +import { + ChangeEvent, + Dispatch, + SetStateAction, + useEffect, + useRef, + useState, +} from 'react'; import { Input, @@ -62,8 +69,8 @@ function Retention({ }, [selectedTimeUnit, selectedValue, setRetentionValue]); const onChangeHandler = ( - e: React.ChangeEvent, - func: React.Dispatch>, + e: ChangeEvent, + func: Dispatch>, ): void => { interacted.current = true; const { value } = e.target; @@ -111,7 +118,7 @@ function Retention({ interface RetentionProps { retentionValue: number | null; text: string; - setRetentionValue: React.Dispatch>; + setRetentionValue: Dispatch>; hide: boolean; } diff --git a/frontend/src/container/GeneralSettings/StatusMessage.tsx b/frontend/src/container/GeneralSettings/StatusMessage.tsx index bb62f34007..a673bb8bb8 100644 --- a/frontend/src/container/GeneralSettings/StatusMessage.tsx +++ b/frontend/src/container/GeneralSettings/StatusMessage.tsx @@ -1,7 +1,7 @@ import { green, orange, volcano } from '@ant-design/colors'; import { InfoCircleOutlined } from '@ant-design/icons'; import { Card, Col, Row } from 'antd'; -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { TStatus } from 'types/api/settings/getRetention'; diff --git a/frontend/src/container/GeneralSettings/index.tsx b/frontend/src/container/GeneralSettings/index.tsx index 2e803563e4..d82889f694 100644 --- a/frontend/src/container/GeneralSettings/index.tsx +++ b/frontend/src/container/GeneralSettings/index.tsx @@ -2,12 +2,14 @@ import { Typography } from 'antd'; import getDisks from 'api/disks/getDisks'; import getRetentionPeriodApi from 'api/settings/getRetention'; import Spinner from 'components/Spinner'; -import React from 'react'; import { useTranslation } from 'react-i18next'; import { useQueries } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { TTTLType } from 'types/api/settings/common'; import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention'; +import AppReducer from 'types/reducer/app'; import GeneralSettingsContainer from './GeneralSettings'; @@ -17,6 +19,7 @@ type TRetentionAPIReturn = Promise< function GeneralSettings(): JSX.Element { const { t } = useTranslation('common'); + const { user } = useSelector((state) => state.app); const [ getRetentionPeriodMetricsApiResponse, @@ -27,20 +30,20 @@ function GeneralSettings(): JSX.Element { { queryFn: (): TRetentionAPIReturn<'metrics'> => getRetentionPeriodApi('metrics'), - queryKey: 'getRetentionPeriodApiMetrics', + queryKey: ['getRetentionPeriodApiMetrics', user?.accessJwt], }, { queryFn: (): TRetentionAPIReturn<'traces'> => getRetentionPeriodApi('traces'), - queryKey: 'getRetentionPeriodApiTraces', + queryKey: ['getRetentionPeriodApiTraces', user?.accessJwt], }, { queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'), - queryKey: 'getRetentionPeriodApiLogs', + queryKey: ['getRetentionPeriodApiLogs', user?.accessJwt], }, { queryFn: getDisks, - queryKey: 'getDisks', + queryKey: ['getDisks', user?.accessJwt], }, ]); diff --git a/frontend/src/container/GridGraphComponent/index.tsx b/frontend/src/container/GridGraphComponent/index.tsx index f5612b0a40..22fa25f98d 100644 --- a/frontend/src/container/GridGraphComponent/index.tsx +++ b/frontend/src/container/GridGraphComponent/index.tsx @@ -3,9 +3,9 @@ import { ChartData } from 'chart.js'; import Graph, { GraphOnClickHandler, StaticLineProps } from 'components/Graph'; import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import ValueGraph from 'components/ValueGraph'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import history from 'lib/history'; -import React from 'react'; import { TitleContainer, ValueContainer } from './styles'; @@ -25,7 +25,7 @@ function GridGraphComponent({ const isDashboardPage = location.split('/').length === 3; - if (GRAPH_TYPES === 'TIME_SERIES') { + if (GRAPH_TYPES === PANEL_TYPES.TIME_SERIES) { return ( (''); const [hovered, setHovered] = useState(false); const [modal, setModal] = useState(false); const [deleteModal, setDeleteModal] = useState(false); - const { minTime, maxTime } = useSelector( - (state) => state.globalTime, - ); - const { selectedTime: globalSelectedInterval } = useSelector< + const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector< AppState, GlobalReducer >((state) => state.globalTime); + const { featureResponse } = useSelector( + (state) => state.app, + ); const { dashboards } = useSelector( (state) => state.dashboards, ); @@ -111,7 +124,7 @@ function GridCardGraph({ const prevChartDataSetRef = usePreviousValue(chartData); const onToggleModal = useCallback( - (func: React.Dispatch>) => { + (func: Dispatch>) => { func((value) => !value); }, [], @@ -122,9 +135,27 @@ function GridCardGraph({ const widgetId = isEmptyWidget ? layout[0].i : widget?.id; - deleteWidget({ widgetId, setLayout }); - onToggleModal(setDeleteModal); - }, [deleteWidget, layout, onToggleModal, setLayout, widget]); + featureResponse + .refetch() + .then(() => { + deleteWidget({ widgetId, setLayout }); + onToggleModal(setDeleteModal); + }) + .catch(() => { + notifications.error({ + message: t('common:something_went_wrong'), + }); + }); + }, [ + widget, + layout, + featureResponse, + deleteWidget, + setLayout, + onToggleModal, + notifications, + t, + ]); const getModals = (): JSX.Element => ( <> @@ -294,7 +325,7 @@ interface GridCardGraphProps extends DispatchProps { // eslint-disable-next-line react/require-default-props layout?: Layout[]; // eslint-disable-next-line react/require-default-props - setLayout?: React.Dispatch>; + setLayout?: Dispatch>; onDragSelect?: (start: number, end: number) => void; } diff --git a/frontend/src/container/GridGraphLayout/GraphLayout.tsx b/frontend/src/container/GridGraphLayout/GraphLayout.tsx index d615fb1a13..02cbb7ed6c 100644 --- a/frontend/src/container/GridGraphLayout/GraphLayout.tsx +++ b/frontend/src/container/GridGraphLayout/GraphLayout.tsx @@ -1,7 +1,7 @@ import { PlusOutlined, SaveFilled } from '@ant-design/icons'; import useComponentPermission from 'hooks/useComponentPermission'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import React from 'react'; +import { Dispatch, SetStateAction } from 'react'; import { Layout } from 'react-grid-layout'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; @@ -102,7 +102,7 @@ interface GraphLayoutProps { onAddPanelHandler: VoidFunction; onLayoutChangeHandler: (layout: Layout[]) => Promise; widgets: Widgets[] | undefined; - setLayout: React.Dispatch>; + setLayout: Dispatch>; } export default GraphLayout; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/config.ts b/frontend/src/container/GridGraphLayout/WidgetHeader/config.ts index 2d9771d235..0ac1105ba6 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/config.ts +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/config.ts @@ -1,6 +1,7 @@ import { themeColors } from 'constants/theme'; +import { CSSProperties } from 'react'; -const positionCss: React.CSSProperties['position'] = 'fixed'; +const positionCss: CSSProperties['position'] = 'fixed'; export const spinnerStyles = { position: positionCss, right: '0.5rem' }; export const tooltipStyles = { @@ -13,7 +14,7 @@ export const tooltipStyles = { export const errorTooltipPosition = 'top'; -export const overlayStyles: React.CSSProperties = { +export const overlayStyles: CSSProperties = { display: 'flex', flexDirection: 'column', alignItems: 'center', diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx index 6a3795e6a2..ed7eab478d 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx @@ -10,7 +10,7 @@ import { MenuItemType } from 'antd/es/menu/hooks/useItems'; import Spinner from 'components/Spinner'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; -import React, { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx index 2d57c98eda..d299e01e7e 100644 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -3,11 +3,17 @@ import updateDashboardApi from 'api/dashboard/update'; import useComponentPermission from 'hooks/useComponentPermission'; import { useNotifications } from 'hooks/useNotifications'; -import React, { useCallback, useEffect, useState } from 'react'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from 'react'; import { Layout } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; import { connect, useDispatch, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; +import { bindActionCreators, Dispatch as ReduxDispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { AppDispatch } from 'store'; import { UpdateTimeInterval } from 'store/actions'; @@ -66,7 +72,7 @@ function GridGraph(props: Props): JSX.Element { const [selectedDashboard] = dashboards; const { data } = selectedDashboard; const { widgets } = data; - const dispatch: AppDispatch = useDispatch>(); + const dispatch: AppDispatch = useDispatch>(); const [layouts, setLayout] = useState( getPreLayouts(widgets, selectedDashboard.data.layout || []), @@ -84,6 +90,8 @@ function GridGraph(props: Props): JSX.Element { [dispatch], ); + const { notifications } = useNotifications(); + useEffect(() => { (async (): Promise => { if (!isAddWidget) { @@ -119,6 +127,12 @@ function GridGraph(props: Props): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const { featureResponse } = useSelector( + (state) => state.app, + ); + + const errorMessage = t('common:something_went_wrong'); + const onLayoutSaveHandler = useCallback( async (layout: Layout[]) => { try { @@ -128,44 +142,62 @@ function GridGraph(props: Props): JSX.Element { errorMessage: '', loading: true, })); - const updatedDashboard: Dashboard = { - ...selectedDashboard, - data: { - title: data.title, - description: data.description, - name: data.name, - tags: data.tags, - widgets: data.widgets, - variables: data.variables, - layout, - }, - uuid: selectedDashboard.uuid, - }; - // Save layout only when users has the has the permission to do so. - if (saveLayoutPermission) { - const response = await updateDashboardApi(updatedDashboard); - if (response.statusCode === 200) { - setSaveLayoutState((state) => ({ - ...state, - error: false, - errorMessage: '', - loading: false, - })); - dispatch({ - type: UPDATE_DASHBOARD, - payload: updatedDashboard, - }); - } else { + + featureResponse + .refetch() + .then(async () => { + const updatedDashboard: Dashboard = { + ...selectedDashboard, + data: { + title: data.title, + description: data.description, + name: data.name, + tags: data.tags, + widgets: data.widgets, + variables: data.variables, + layout, + }, + uuid: selectedDashboard.uuid, + }; + // Save layout only when users has the has the permission to do so. + if (saveLayoutPermission) { + const response = await updateDashboardApi(updatedDashboard); + if (response.statusCode === 200) { + setSaveLayoutState((state) => ({ + ...state, + error: false, + errorMessage: '', + loading: false, + })); + dispatch({ + type: UPDATE_DASHBOARD, + payload: updatedDashboard, + }); + } else { + setSaveLayoutState((state) => ({ + ...state, + error: true, + errorMessage: response.error || errorMessage, + loading: false, + })); + } + } + }) + .catch(() => { setSaveLayoutState((state) => ({ ...state, error: true, - errorMessage: response.error || 'Something went wrong', + errorMessage, loading: false, })); - } - } + notifications.error({ + message: errorMessage, + }); + }); } catch (error) { - console.error(error); + notifications.error({ + message: errorMessage, + }); } }, [ @@ -176,6 +208,9 @@ function GridGraph(props: Props): JSX.Element { data.variables, data.widgets, dispatch, + errorMessage, + featureResponse, + notifications, saveLayoutPermission, selectedDashboard, ], @@ -207,8 +242,6 @@ function GridGraph(props: Props): JSX.Element { [widgets, onDragSelect], ); - const { notifications } = useNotifications(); - const onEmptyWidgetHandler = useCallback(async () => { try { const id = 'empty'; @@ -239,10 +272,10 @@ function GridGraph(props: Props): JSX.Element { setLayoutFunction(layout); } catch (error) { notifications.error({ - message: error instanceof Error ? error.toString() : 'Something went wrong', + message: error instanceof Error ? error.toString() : errorMessage, }); } - }, [data, selectedDashboard, setLayoutFunction, notifications]); + }, [data, selectedDashboard, setLayoutFunction, notifications, errorMessage]); const onLayoutChangeHandler = async (layout: Layout[]): Promise => { setLayoutFunction(layout); @@ -253,49 +286,63 @@ function GridGraph(props: Props): JSX.Element { const onAddPanelHandler = useCallback(() => { try { setAddPanelLoading(true); - const isEmptyLayoutPresent = - layouts.find((e) => e.i === 'empty') !== undefined; + featureResponse + .refetch() + .then(() => { + const isEmptyLayoutPresent = + layouts.find((e) => e.i === 'empty') !== undefined; - if (!isEmptyLayoutPresent) { - onEmptyWidgetHandler() - .then(() => { - setAddPanelLoading(false); + if (!isEmptyLayoutPresent) { + onEmptyWidgetHandler() + .then(() => { + setAddPanelLoading(false); + toggleAddWidget(true); + }) + .catch(() => { + notifications.error({ + message: errorMessage, + }); + }); + } else { toggleAddWidget(true); - }) - .catch(() => { - notifications.error(t('something_went_wrong')); - }); - } else { - toggleAddWidget(true); - setAddPanelLoading(false); - } + setAddPanelLoading(false); + } + }) + .catch(() => + notifications.error({ + message: errorMessage, + }), + ); } catch (error) { - if (typeof error === 'string') { - notifications.error({ - message: error || t('something_went_wrong'), - }); - } + notifications.error({ + message: errorMessage, + }); } - }, [layouts, onEmptyWidgetHandler, t, toggleAddWidget, notifications]); + }, [ + featureResponse, + layouts, + onEmptyWidgetHandler, + toggleAddWidget, + notifications, + errorMessage, + ]); return ( ); } interface ComponentProps { - setLayout: React.Dispatch>; + setLayout: Dispatch>; } export interface LayoutProps extends Layout { @@ -312,7 +359,7 @@ export interface State { interface DispatchProps { toggleAddWidget: ( props: ToggleAddWidgetProps, - ) => (dispatch: Dispatch) => void; + ) => (dispatch: ReduxDispatch) => void; } const mapDispatchToProps = ( diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts index 95d3574b7e..04944451fa 100644 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ b/frontend/src/container/GridGraphLayout/utils.ts @@ -3,8 +3,8 @@ import updateDashboardApi from 'api/dashboard/update'; import { ClickHouseQueryTemplate, PromQLQueryTemplate, - QueryBuilderQueryTemplate, } from 'constants/dashboard'; +import { initialQueryBuilderFormValues } from 'constants/queryBuilder'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import GetQueryName from 'lib/query/GetQueryName'; import { Layout } from 'react-grid-layout'; @@ -42,26 +42,21 @@ export const UpdateDashboard = async ( panelTypes: graphType, query: { queryType: EQueryType.QUERY_BUILDER, - promQL: [ + promql: [ { name: GetQueryName([]) || '', ...PromQLQueryTemplate, }, ], - clickHouse: [ + clickhouse_sql: [ { name: GetQueryName([]) || '', ...ClickHouseQueryTemplate, }, ], - metricsBuilder: { - formulas: [], - queryBuilder: [ - { - name: GetQueryName([]) || '', - ...QueryBuilderQueryTemplate, - }, - ], + builder: { + queryFormulas: [], + queryData: [initialQueryBuilderFormValues], }, }, queryData: { diff --git a/frontend/src/container/Header/CurrentOrganization/index.tsx b/frontend/src/container/Header/CurrentOrganization/index.tsx index 7b32c3f913..dcf001c024 100644 --- a/frontend/src/container/Header/CurrentOrganization/index.tsx +++ b/frontend/src/container/Header/CurrentOrganization/index.tsx @@ -4,7 +4,6 @@ import { INVITE_MEMBERS_HASH } from 'constants/app'; import ROUTES from 'constants/routes'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; -import React from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; diff --git a/frontend/src/container/Header/ManageLicense/index.tsx b/frontend/src/container/Header/ManageLicense/index.tsx index 37c776ce2a..377af48103 100644 --- a/frontend/src/container/Header/ManageLicense/index.tsx +++ b/frontend/src/container/Header/ManageLicense/index.tsx @@ -1,9 +1,7 @@ -import { Typography } from 'antd'; -import { FeatureKeys } from 'constants/features'; +import { Spin, Typography } from 'antd'; import ROUTES from 'constants/routes'; -import useFeatureFlags from 'hooks/useFeatureFlag'; +import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense'; import history from 'lib/history'; -import React from 'react'; import { FreePlanIcon, @@ -12,7 +10,22 @@ import { } from './styles'; function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element { - const isEnterprise = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN); + const { data, isLoading } = useLicense(); + + const onManageLicense = (): void => { + onToggle(); + history.push(ROUTES.LIST_LICENSES); + }; + + if (isLoading || data?.payload === undefined) { + return ; + } + + const isEnterprise = data?.payload?.some( + (license) => + license.isCurrent && license.planKey === LICENSE_PLAN_KEY.ENTERPRISE_PLAN, + ); + return ( <> SIGNOZ STATUS @@ -23,14 +36,7 @@ function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element { {!isEnterprise ? 'Free Plan' : 'Enterprise Plan'} - { - onToggle(); - history.push(ROUTES.LIST_LICENSES); - }} - > - Manage Licenses - + Manage Licenses ); diff --git a/frontend/src/container/Header/SignedIn/index.tsx b/frontend/src/container/Header/SignedIn/index.tsx index b1804ea93a..33caab1670 100644 --- a/frontend/src/container/Header/SignedIn/index.tsx +++ b/frontend/src/container/Header/SignedIn/index.tsx @@ -1,7 +1,7 @@ import { Avatar, Typography } from 'antd'; import ROUTES from 'constants/routes'; import history from 'lib/history'; -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; diff --git a/frontend/src/container/Header/index.tsx b/frontend/src/container/Header/index.tsx index a34287e665..8b906eab74 100644 --- a/frontend/src/container/Header/index.tsx +++ b/frontend/src/container/Header/index.tsx @@ -3,14 +3,14 @@ import { CaretUpFilled, LogoutOutlined, } from '@ant-design/icons'; -import type { MenuProps } from 'antd'; -import { Divider, Dropdown, Space, Typography } from 'antd'; +import { Button, Divider, Dropdown, MenuProps, Space, Typography } from 'antd'; import { Logout } from 'api/utils'; import ROUTES from 'constants/routes'; import Config from 'container/ConfigDropdown'; import { useIsDarkMode, useThemeMode } from 'hooks/useDarkMode'; -import React, { +import { Dispatch, + KeyboardEvent, SetStateAction, useCallback, useMemo, @@ -50,14 +50,11 @@ function HeaderContainer(): JSX.Element { [], ); - const onLogoutKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === 'Space') { - Logout(); - } - }, - [], - ); + const onLogoutKeyDown = useCallback((e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === 'Space') { + Logout(); + } + }, []); const menu: MenuProps = useMemo( () => ({ @@ -91,6 +88,13 @@ function HeaderContainer(): JSX.Element { [onToggleHandler, onLogoutKeyDown], ); + const onClickSignozCloud = (): void => { + window.open( + 'https://signoz.io/pricing/?utm_source=product_navbar&utm_medium=frontend', + '_blank', + ); + }; + return (
@@ -106,7 +110,11 @@ function HeaderContainer(): JSX.Element { - + + + >(); - const { refetch } = useQuery({ - queryFn: getFeaturesFlags, - queryKey: 'getFeatureFlags', - enabled: false, - }); + const { featureResponse } = useSelector( + (state) => state.app, + ); const { notifications } = useNotifications(); @@ -47,16 +43,8 @@ function ApplyLicenseForm({ }); if (response.statusCode === 200) { - const [featureFlagsResponse] = await Promise.all([ - refetch(), - licenseRefetch(), - ]); - if (featureFlagsResponse.data?.payload) { - dispatch({ - type: UPDATE_FEATURE_FLAGS, - payload: featureFlagsResponse.data.payload, - }); - } + await Promise.all([featureResponse?.refetch(), licenseRefetch()]); + notifications.success({ message: 'Success', description: t('license_applied'), diff --git a/frontend/src/container/Licenses/ListLicenses.tsx b/frontend/src/container/Licenses/ListLicenses.tsx index 950eabdfc3..d0ca5f0782 100644 --- a/frontend/src/container/Licenses/ListLicenses.tsx +++ b/frontend/src/container/Licenses/ListLicenses.tsx @@ -1,6 +1,5 @@ import { ColumnsType } from 'antd/lib/table'; import { ResizeTable } from 'components/ResizeTable'; -import React from 'react'; import { useTranslation } from 'react-i18next'; import { License } from 'types/api/licenses/def'; import { PayloadProps } from 'types/api/licenses/getAll'; diff --git a/frontend/src/container/Licenses/index.tsx b/frontend/src/container/Licenses/index.tsx index d7dc4ab22b..b4d068d908 100644 --- a/frontend/src/container/Licenses/index.tsx +++ b/frontend/src/container/Licenses/index.tsx @@ -1,19 +1,14 @@ import { Tabs, Typography } from 'antd'; -import getAll from 'api/licenses/getAll'; import Spinner from 'components/Spinner'; -import React from 'react'; +import useLicense from 'hooks/useLicense'; import { useTranslation } from 'react-i18next'; -import { useQuery } from 'react-query'; import ApplyLicenseForm from './ApplyLicenseForm'; import ListLicenses from './ListLicenses'; function Licenses(): JSX.Element { const { t } = useTranslation(['licenses']); - const { data, isError, isLoading, refetch } = useQuery({ - queryFn: getAll, - queryKey: 'getAllLicenses', - }); + const { data, isError, isLoading, refetch } = useLicense(); if (isError || data?.error) { return {data?.error}; diff --git a/frontend/src/container/ListAlertRules/DeleteAlert.tsx b/frontend/src/container/ListAlertRules/DeleteAlert.tsx index 8ff3927d68..8f960b6ba7 100644 --- a/frontend/src/container/ListAlertRules/DeleteAlert.tsx +++ b/frontend/src/container/ListAlertRules/DeleteAlert.tsx @@ -1,9 +1,12 @@ import { NotificationInstance } from 'antd/es/notification/interface'; import deleteAlerts from 'api/alerts/delete'; import { State } from 'hooks/useFetch'; -import React, { useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; import { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete'; import { GettableAlert } from 'types/api/alerts/get'; +import AppReducer from 'types/reducer/app'; import { ColumnButton } from './styles'; @@ -22,15 +25,14 @@ function DeleteAlert({ payload: undefined, }); + const { featureResponse } = useSelector( + (state) => state.app, + ); + const defaultErrorMessage = 'Something went wrong'; const onDeleteHandler = async (id: number): Promise => { try { - setDeleteAlertState((state) => ({ - ...state, - loading: true, - })); - const response = await deleteAlerts({ id, }); @@ -72,11 +74,32 @@ function DeleteAlert({ } }; + const onClickHandler = (): void => { + setDeleteAlertState((state) => ({ + ...state, + loading: true, + })); + featureResponse + .refetch() + .then(() => { + onDeleteHandler(id); + }) + .catch(() => { + setDeleteAlertState((state) => ({ + ...state, + loading: false, + })); + notifications.error({ + message: defaultErrorMessage, + }); + }); + }; + return ( => onDeleteHandler(id)} + onClick={onClickHandler} type="link" > Delete @@ -86,7 +109,7 @@ function DeleteAlert({ interface DeleteAlertProps { id: GettableAlert['id']; - setData: React.Dispatch>; + setData: Dispatch>; notifications: NotificationInstance; } diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index be54f0e914..6d02cae58b 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -9,7 +9,7 @@ import useComponentPermission from 'hooks/useComponentPermission'; import useInterval from 'hooks/useInterval'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useCallback, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; @@ -26,7 +26,9 @@ import ToggleAlertState from './ToggleAlertState'; function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { const [data, setData] = useState(allAlertRules || []); const { t } = useTranslation('common'); - const { role } = useSelector((state) => state.app); + const { role, featureResponse } = useSelector( + (state) => state.app, + ); const [addNewAlert, action] = useComponentPermission( ['add_new_alert', 'action'], role, @@ -48,12 +50,28 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { })(); }, 30000); + const handleError = useCallback((): void => { + notificationsApi.error({ + message: t('something_went_wrong'), + }); + }, [notificationsApi, t]); + const onClickNewAlertHandler = useCallback(() => { - history.push(ROUTES.ALERTS_NEW); - }, []); + featureResponse + .refetch() + .then(() => { + history.push(ROUTES.ALERTS_NEW); + }) + .catch(handleError); + }, [featureResponse, handleError]); const onEditHandler = (id: string): void => { - history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`); + featureResponse + .refetch() + .then(() => { + history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`); + }) + .catch(handleError); }; const columns: ColumnsType = [ diff --git a/frontend/src/container/ListAlertRules/TableComponents/Status.tsx b/frontend/src/container/ListAlertRules/TableComponents/Status.tsx index d935b8d5ba..94e56723af 100644 --- a/frontend/src/container/ListAlertRules/TableComponents/Status.tsx +++ b/frontend/src/container/ListAlertRules/TableComponents/Status.tsx @@ -1,5 +1,4 @@ import { Tag } from 'antd'; -import React from 'react'; import { GettableAlert } from 'types/api/alerts/get'; function Status({ status }: StatusProps): JSX.Element { diff --git a/frontend/src/container/ListAlertRules/ToggleAlertState.tsx b/frontend/src/container/ListAlertRules/ToggleAlertState.tsx index 5410159226..edb894abe8 100644 --- a/frontend/src/container/ListAlertRules/ToggleAlertState.tsx +++ b/frontend/src/container/ListAlertRules/ToggleAlertState.tsx @@ -1,7 +1,7 @@ import patchAlert from 'api/alerts/patch'; import { State } from 'hooks/useFetch'; import { useNotifications } from 'hooks/useNotifications'; -import React, { useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import { GettableAlert } from 'types/api/alerts/get'; import { PayloadProps as PatchPayloadProps } from 'types/api/alerts/patch'; @@ -104,7 +104,7 @@ function ToggleAlertState({ interface ToggleAlertStateProps { id: GettableAlert['id']; disabled: boolean; - setData: React.Dispatch>; + setData: Dispatch>; } export default ToggleAlertState; diff --git a/frontend/src/container/ListAlertRules/index.tsx b/frontend/src/container/ListAlertRules/index.tsx index 078769141e..3880a7c2e6 100644 --- a/frontend/src/container/ListAlertRules/index.tsx +++ b/frontend/src/container/ListAlertRules/index.tsx @@ -3,7 +3,7 @@ import getAll from 'api/alerts/getAll'; import ReleaseNote from 'components/ReleaseNote'; import Spinner from 'components/Spinner'; import { useNotifications } from 'hooks/useNotifications'; -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; import { useLocation } from 'react-router-dom'; diff --git a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx index 75fa6a581d..b737f7d0f9 100644 --- a/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx +++ b/frontend/src/container/ListOfDashboard/ImportJSON/index.tsx @@ -4,9 +4,10 @@ import { Button, Modal, Space, Typography, Upload, UploadProps } from 'antd'; import createDashboard from 'api/dashboard/create'; import Editor from 'components/Editor'; import ROUTES from 'constants/routes'; +import { MESSAGE } from 'hooks/useFeatureFlag'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { generatePath } from 'react-router-dom'; @@ -28,6 +29,8 @@ function ImportJSON({ const [isCreateDashboardError, setIsCreateDashboardError] = useState( false, ); + const [isFeatureAlert, setIsFeatureAlert] = useState(false); + const dispatch = useDispatch>(); const [dashboardCreating, setDashboardCreating] = useState(false); @@ -99,6 +102,15 @@ function ImportJSON({ }), ); }, 10); + } else if (response.error === 'feature usage exceeded') { + setIsFeatureAlert(true); + notifications.error({ + message: + response.error || + t('something_went_wrong', { + ns: 'common', + }), + }); } else { setIsCreateDashboardError(true); notifications.error({ @@ -112,6 +124,7 @@ function ImportJSON({ setDashboardCreating(false); } catch { setDashboardCreating(false); + setIsFeatureAlert(false); setIsCreateDashboardError(true); } @@ -124,6 +137,13 @@ function ImportJSON({ ); + const onCancelHandler = (): void => { + setIsUploadJSONError(false); + setIsCreateDashboardError(false); + setIsFeatureAlert(false); + onModalHandler(); + }; + return ( {t('import_json')} @@ -148,6 +168,11 @@ function ImportJSON({ {t('load_json')} {isCreateDashboardError && getErrorNode(t('error_loading_json'))} + {isFeatureAlert && ( + + {MESSAGE.CREATE_DASHBOARD} + + )} } > diff --git a/frontend/src/container/ListOfDashboard/SearchFilter/QueryChip.tsx b/frontend/src/container/ListOfDashboard/SearchFilter/QueryChip.tsx index 04825b7a81..5405660523 100644 --- a/frontend/src/container/ListOfDashboard/SearchFilter/QueryChip.tsx +++ b/frontend/src/container/ListOfDashboard/SearchFilter/QueryChip.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { QueryChipContainer, QueryChipItem } from './styles'; import { IQueryStructure } from './types'; diff --git a/frontend/src/container/ListOfDashboard/SearchFilter/index.tsx b/frontend/src/container/ListOfDashboard/SearchFilter/index.tsx index b8e16cd4db..eeddb1b6f3 100644 --- a/frontend/src/container/ListOfDashboard/SearchFilter/index.tsx +++ b/frontend/src/container/ListOfDashboard/SearchFilter/index.tsx @@ -4,7 +4,13 @@ import { Button, Select } from 'antd'; import { RefSelectProps } from 'antd/lib/select'; import history from 'lib/history'; import { filter, map } from 'lodash-es'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { + MutableRefObject, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { Dashboard } from 'types/api/dashboard/getAll'; import { v4 as uuidv4 } from 'uuid'; @@ -31,7 +37,7 @@ function SearchFilter({ const [optionsData, setOptionsData] = useState( OptionsSchemas.attribute, ); - const selectRef = useRef() as React.MutableRefObject; + const selectRef = useRef() as MutableRefObject; const [selectedValues, setSelectedValues] = useState([]); const [staging, setStaging] = useState([]); const [queries, setQueries] = useState([]); diff --git a/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx b/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx index faa5d45e32..d463f80c03 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx @@ -1,7 +1,6 @@ import { Typography } from 'antd'; import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; import getFormattedDate from 'lib/getFormatedDate'; -import React from 'react'; import { Data } from '..'; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx index 6b93b4b220..c96ac1ebf1 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx @@ -1,7 +1,6 @@ import { Typography } from 'antd'; import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; import getFormattedDate from 'lib/getFormatedDate'; -import React from 'react'; import { Data } from '..'; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx index fd9ef16001..800dd8d1e6 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -1,6 +1,5 @@ import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal } from 'antd'; -import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx index e54431063f..af53580926 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx @@ -1,6 +1,5 @@ import ROUTES from 'constants/routes'; import history from 'lib/history'; -import React from 'react'; import { generatePath } from 'react-router-dom'; import { Data } from '..'; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx index 40ecbe7c65..bc698487d2 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx @@ -1,6 +1,5 @@ /* eslint-disable react/destructuring-assignment */ import { Tag } from 'antd'; -import React from 'react'; import { Data } from '../index'; diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index ca5d68d9bb..9a91d72331 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -16,8 +16,9 @@ import ROUTES from 'constants/routes'; import SearchFilter from 'container/ListOfDashboard/SearchFilter'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; -import React, { +import { Dispatch, + Key, useCallback, useEffect, useMemo, @@ -74,49 +75,52 @@ function ListOfAllDashboard(): JSX.Element { errorMessage: '', }); - const columns: TableColumnProps[] = [ - { - title: 'Name', - dataIndex: 'name', - width: 100, - render: Name, - }, - { - title: 'Description', - width: 100, - dataIndex: 'description', - }, - { - title: 'Tags (can be multiple)', - dataIndex: 'tags', - width: 80, - render: Tags, - }, - { - title: 'Created At', - dataIndex: 'createdBy', - width: 80, - sorter: (a: Data, b: Data): number => { - const prev = new Date(a.createdBy).getTime(); - const next = new Date(b.createdBy).getTime(); - - return prev - next; + const columns: TableColumnProps[] = useMemo( + () => [ + { + title: 'Name', + dataIndex: 'name', + width: 100, + render: Name, }, - render: Createdby, - }, - { - title: 'Last Updated Time', - width: 90, - dataIndex: 'lastUpdatedTime', - sorter: (a: Data, b: Data): number => { - const prev = new Date(a.lastUpdatedTime).getTime(); - const next = new Date(b.lastUpdatedTime).getTime(); - - return prev - next; + { + title: 'Description', + width: 100, + dataIndex: 'description', }, - render: DateComponent, - }, - ]; + { + title: 'Tags (can be multiple)', + dataIndex: 'tags', + width: 80, + render: Tags, + }, + { + title: 'Created At', + dataIndex: 'createdBy', + width: 80, + sorter: (a: Data, b: Data): number => { + const prev = new Date(a.createdBy).getTime(); + const next = new Date(b.createdBy).getTime(); + + return prev - next; + }, + render: Createdby, + }, + { + title: 'Last Updated Time', + width: 90, + dataIndex: 'lastUpdatedTime', + sorter: (a: Data, b: Data): number => { + const prev = new Date(a.lastUpdatedTime).getTime(); + const next = new Date(b.lastUpdatedTime).getTime(); + + return prev - next; + }, + render: DateComponent, + }, + ], + [], + ); if (action) { columns.push({ @@ -199,7 +203,7 @@ function ListOfAllDashboard(): JSX.Element { setUploadedGrafana(uploadedGrafana); }; - const getMenuItems = useCallback(() => { + const getMenuItems = useMemo(() => { const menuItems: ItemType[] = []; if (createNewDashboard) { menuItems.push({ @@ -227,7 +231,7 @@ function ListOfAllDashboard(): JSX.Element { const menu: MenuProps = useMemo( () => ({ - items: getMenuItems(), + items: getMenuItems, }), [getMenuItems], ); @@ -245,7 +249,7 @@ function ListOfAllDashboard(): JSX.Element { }} /> {newDashboard && ( - + } type="primary" @@ -260,11 +264,12 @@ function ListOfAllDashboard(): JSX.Element { ), [ - getText, newDashboard, - newDashboardState.error, - newDashboardState.loading, + loading, menu, + newDashboardState.loading, + newDashboardState.error, + getText, ], ); @@ -304,7 +309,7 @@ function ListOfAllDashboard(): JSX.Element { } export interface Data { - key: React.Key; + key: Key; name: string; description: string; tags: string[]; diff --git a/frontend/src/container/LogControls/index.tsx b/frontend/src/container/LogControls/index.tsx index 41bfeafb53..f688e2866f 100644 --- a/frontend/src/container/LogControls/index.tsx +++ b/frontend/src/container/LogControls/index.tsx @@ -7,7 +7,7 @@ import { Button, Divider, Select } from 'antd'; import { getGlobalTime } from 'container/LogsSearchFilter/utils'; import { getMinMax } from 'container/TopNav/AutoRefresh/config'; import { defaultSelectStyle } from 'pages/Logs/config'; -import React, { memo, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; diff --git a/frontend/src/container/LogDetailedView/ActionItem.tsx b/frontend/src/container/LogDetailedView/ActionItem.tsx index 10a7419bff..28e429d7e2 100644 --- a/frontend/src/container/LogDetailedView/ActionItem.tsx +++ b/frontend/src/container/LogDetailedView/ActionItem.tsx @@ -2,7 +2,7 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { Button, Col, Popover } from 'antd'; import getStep from 'lib/getStep'; import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; -import React, { memo, useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { connect, useDispatch, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; diff --git a/frontend/src/container/LogDetailedView/JsonView.tsx b/frontend/src/container/LogDetailedView/JsonView.tsx index 95e5c558a1..e510d46d10 100644 --- a/frontend/src/container/LogDetailedView/JsonView.tsx +++ b/frontend/src/container/LogDetailedView/JsonView.tsx @@ -2,7 +2,7 @@ import { blue } from '@ant-design/colors'; import { CopyFilled } from '@ant-design/icons'; import { Button, Row } from 'antd'; import Editor from 'components/Editor'; -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { useCopyToClipboard } from 'react-use'; import { ILog } from 'types/api/logs/log'; diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index eea61f2a32..2959be7dcc 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -1,18 +1,25 @@ import { blue, orange } from '@ant-design/colors'; -import { Input } from 'antd'; +import { LinkOutlined } from '@ant-design/icons'; +import { Input, Space, Tooltip } from 'antd'; import { ColumnsType } from 'antd/es/table'; import Editor from 'components/Editor'; import AddToQueryHOC from 'components/Logs/AddToQueryHOC'; import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; import { ResizeTable } from 'components/ResizeTable'; -import flatten from 'flat'; +import ROUTES from 'constants/routes'; +import history from 'lib/history'; import { fieldSearchFilter } from 'lib/logs/fieldSearch'; import { isEmpty } from 'lodash-es'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { generatePath } from 'react-router-dom'; +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; +import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { ILog } from 'types/api/logs/log'; import ActionItem from './ActionItem'; -import { recursiveParseJSON } from './utils'; +import { flattenObject, recursiveParseJSON } from './utils'; // Fields which should be restricted from adding it to query const RESTRICTED_FIELDS = ['timestamp']; @@ -23,8 +30,10 @@ interface TableViewProps { function TableView({ logData }: TableViewProps): JSX.Element | null { const [fieldSearchInput, setFieldSearchInput] = useState(''); - const flattenLogData: Record | null = useMemo( - () => (logData ? flatten(logData) : null), + const dispatch = useDispatch>(); + + const flattenLogData: Record | null = useMemo( + () => (logData ? flattenObject(logData) : null), [logData], ); if (logData === null) { @@ -41,6 +50,29 @@ function TableView({ logData }: TableViewProps): JSX.Element | null { value: JSON.stringify(flattenLogData[key]), })); + const onTraceHandler = (record: DataType) => (): void => { + if (flattenLogData === null) return; + + const traceId = flattenLogData[record.field]; + + const spanId = flattenLogData?.span_id; + + if (traceId) { + dispatch({ + type: SET_DETAILED_LOG_DATA, + payload: null, + }); + + const basePath = generatePath(ROUTES.TRACE_DETAIL, { + id: traceId, + }); + + const route = spanId ? `${basePath}?spanId=${spanId}` : basePath; + + history.push(route); + } + }; + if (!dataSource) { return null; } @@ -62,11 +94,38 @@ function TableView({ logData }: TableViewProps): JSX.Element | null { dataIndex: 'field', key: 'field', width: 30, + align: 'left', ellipsis: true, - render: (field: string): JSX.Element => { + render: (field: string, record): JSX.Element => { const fieldKey = field.split('.').slice(-1); const renderedField = {field}; + if (record.field === 'trace_id') { + const traceId = flattenLogData[record.field]; + + return ( + + {renderedField} + + {traceId && ( + +
+ +
+
+ )} +
+ ); + } + if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { return ( @@ -89,7 +148,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null { if (!isEmpty(parsedBody)) { return ( { it('should return an empty object if the input is not valid JSON', () => { @@ -45,3 +45,104 @@ describe('recursiveParseJSON', () => { expect(result).toEqual({ name: 'John", "Doe', age: 30 }); }); }); + +describe('flattenObject in the objects recursively', () => { + it('should flatten nested objects correctly', () => { + const nestedObj = { + a: { + b: { + c: 1, + d: 2, + }, + e: 3, + }, + f: 4, + }; + const expected = { + 'a.b.c': 1, + 'a.b.d': 2, + 'a.e': 3, + f: 4, + }; + + expect(flattenObject(nestedObj)).toEqual(expected); + }); + + it('should return an empty object when input is empty', () => { + const nestedObj = {}; + const expected = {}; + + expect(flattenObject(nestedObj)).toEqual(expected); + }); + + it('should handle non-nested objects correctly', () => { + const nestedObj = { + a: 1, + b: 2, + c: 3, + }; + const expected = { + a: 1, + b: 2, + c: 3, + }; + + expect(flattenObject(nestedObj)).toEqual(expected); + }); + + it('should handle null and undefined correctly', () => { + const nestedObj = { + a: null, + b: undefined, + }; + const expected = { + a: null, + b: undefined, + }; + + expect(flattenObject(nestedObj)).toEqual(expected); + }); + + it('should handle arrays correctly', () => { + const objWithArray = { + a: [1, 2, 3], + b: 2, + }; + const expected = { + a: [1, 2, 3], + b: 2, + }; + + expect(flattenObject(objWithArray)).toEqual(expected); + }); + + it('should handle nested objects in arrays correctly', () => { + const objWithArray = { + a: [{ b: 1 }, { c: 2 }], + d: 3, + }; + const expected = { + a: [{ b: 1 }, { c: 2 }], + d: 3, + }; + + expect(flattenObject(objWithArray)).toEqual(expected); + }); + + it('should handle objects with arrays and nested objects correctly', () => { + const complexObj = { + a: { + b: [1, 2, { c: 3 }], + d: 4, + }, + e: 5, + }; + const expected = { + 'a.b': [1, 2, { c: 3 }], + 'a.d': 4, + e: 5, + }; + + expect(flattenObject(complexObj)).toEqual(expected); + }); +}); diff --git a/frontend/src/container/LogDetailedView/utils.ts b/frontend/src/container/LogDetailedView/utils.ts index 00a89c96d1..4a73c61933 100644 --- a/frontend/src/container/LogDetailedView/utils.ts +++ b/frontend/src/container/LogDetailedView/utils.ts @@ -4,8 +4,31 @@ export const recursiveParseJSON = (obj: string): Record => { if (typeof value === 'string') { return recursiveParseJSON(value); } + if (typeof value === 'object') { + Object.entries(value).forEach(([key, val]) => { + if (typeof val === 'string') { + value[key] = val.trim(); + } else if (typeof val === 'object') { + value[key] = recursiveParseJSON(JSON.stringify(val)); + } + }); + } return value; } catch (e) { return {}; } }; + +type AnyObject = { [key: string]: any }; + +export function flattenObject(obj: AnyObject, prefix = ''): AnyObject { + return Object.keys(obj).reduce((acc: AnyObject, k: string): AnyObject => { + const pre = prefix.length ? `${prefix}.` : ''; + if (typeof obj[k] === 'object' && obj[k] !== null && !Array.isArray(obj[k])) { + Object.assign(acc, flattenObject(obj[k], pre + k)); + } else { + acc[pre + k] = obj[k]; + } + return acc; + }, {}); +} diff --git a/frontend/src/container/LogLiveTail/index.tsx b/frontend/src/container/LogLiveTail/index.tsx index 1139342a9d..08faeda94c 100644 --- a/frontend/src/container/LogLiveTail/index.tsx +++ b/frontend/src/container/LogLiveTail/index.tsx @@ -11,7 +11,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import getStep from 'lib/getStep'; import { throttle } from 'lodash-es'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { connect, useDispatch, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; diff --git a/frontend/src/container/Login/index.tsx b/frontend/src/container/Login/index.tsx index cb9775fbe8..2d0d5854dc 100644 --- a/frontend/src/container/Login/index.tsx +++ b/frontend/src/container/Login/index.tsx @@ -6,10 +6,13 @@ import afterLogin from 'AppRoutes/utils'; import ROUTES from 'constants/routes'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; import { PayloadProps as PrecheckResultType } from 'types/api/user/loginPrecheck'; +import AppReducer from 'types/reducer/app'; import { FormContainer, FormWrapper, Label, ParentContainer } from './styles'; @@ -34,6 +37,7 @@ function Login({ }: LoginProps): JSX.Element { const { t } = useTranslation(['login']); const [isLoading, setIsLoading] = useState(false); + const { user } = useSelector((state) => state.app); const [precheckResult, setPrecheckResult] = useState({ sso: false, @@ -49,7 +53,7 @@ function Login({ const getUserVersionResponse = useQuery({ queryFn: getUserVersion, - queryKey: 'getUserVersion', + queryKey: ['getUserVersion', user?.accessJwt], enabled: true, }); diff --git a/frontend/src/container/LogsAggregate/index.tsx b/frontend/src/container/LogsAggregate/index.tsx index c1217c0e46..9dc0871129 100644 --- a/frontend/src/container/LogsAggregate/index.tsx +++ b/frontend/src/container/LogsAggregate/index.tsx @@ -4,7 +4,7 @@ import Spinner from 'components/Spinner'; import dayjs from 'dayjs'; import useInterval from 'hooks/useInterval'; import getStep from 'lib/getStep'; -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; diff --git a/frontend/src/container/LogsFilters/FieldItem.tsx b/frontend/src/container/LogsFilters/FieldItem.tsx index a62b16d1fc..dd788e8671 100644 --- a/frontend/src/container/LogsFilters/FieldItem.tsx +++ b/frontend/src/container/LogsFilters/FieldItem.tsx @@ -1,7 +1,7 @@ import { LoadingOutlined } from '@ant-design/icons'; import { Button, Popover, Spin, Typography } from 'antd'; import { useIsDarkMode } from 'hooks/useDarkMode'; -import React, { useCallback, useMemo, useState } from 'react'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; import { IField, IInterestingFields, @@ -70,7 +70,7 @@ function FieldItem({ interface FieldItemProps { name: string; - buttonIcon: React.ReactNode; + buttonIcon: ReactNode; buttonOnClick: (props: { fieldData: IInterestingFields | ISelectedFields; fieldIndex: number; diff --git a/frontend/src/container/LogsFilters/index.tsx b/frontend/src/container/LogsFilters/index.tsx index 26c3bd739a..1980e4f115 100644 --- a/frontend/src/container/LogsFilters/index.tsx +++ b/frontend/src/container/LogsFilters/index.tsx @@ -2,7 +2,7 @@ import { CloseOutlined, PlusCircleFilled } from '@ant-design/icons'; import { Col, Input } from 'antd'; import CategoryHeading from 'components/Logs/CategoryHeading'; import { fieldSearchFilter } from 'lib/logs/fieldSearch'; -import React, { useCallback, useState } from 'react'; +import { ChangeEvent, useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { ILogsReducer } from 'types/reducer/logs'; @@ -24,7 +24,7 @@ function LogsFilters(): JSX.Element { >([]); const [filterValuesInput, setFilterValuesInput] = useState(''); - const handleSearch = (e: React.ChangeEvent): void => { + const handleSearch = (e: ChangeEvent): void => { setFilterValuesInput((e.target as HTMLInputElement).value); }; diff --git a/frontend/src/container/LogsFilters/types.ts b/frontend/src/container/LogsFilters/types.ts index ece94aeaec..a062701e2a 100644 --- a/frontend/src/container/LogsFilters/types.ts +++ b/frontend/src/container/LogsFilters/types.ts @@ -1,10 +1,11 @@ +import { SetStateAction } from 'react'; import { IField, IInterestingFields, ISelectedFields, } from 'types/api/logs/fields'; -type SetLoading = (value: React.SetStateAction) => void; +type SetLoading = (value: SetStateAction) => void; export type IHandleInterestProps = { fieldData: IInterestingFields; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/ActionBar.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/ActionBar.tsx index 05ba17b8f6..1c223fb721 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/ActionBar.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/ActionBar.tsx @@ -1,5 +1,4 @@ import { Button, Row } from 'antd'; -import React from 'react'; interface SearchFieldsActionBarProps { applyUpdate: VoidFunction; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/FieldKey.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/FieldKey.tsx index f454267cd7..eac4f1302d 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/FieldKey.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/FieldKey.tsx @@ -1,5 +1,4 @@ import { Typography } from 'antd'; -import React from 'react'; interface FieldKeyProps { name: string; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx index 9aff0b6dd7..4b01ba02e2 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx @@ -6,7 +6,7 @@ import { QueryOperatorsMultiVal, QueryOperatorsSingleVal, } from 'lib/logql/tokens'; -import React, { useCallback, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { ILogsReducer } from 'types/reducer/logs'; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/Suggestions.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/Suggestions.tsx index 56255d1ed1..dccb4c0c1b 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/Suggestions.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/Suggestions.tsx @@ -1,7 +1,6 @@ import { Button } from 'antd'; import CategoryHeading from 'components/Logs/CategoryHeading'; import map from 'lodash-es/map'; -import React from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; // import { ADD_SEARCH_FIELD_QUERY_STRING } from 'types/actions/logs'; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/index.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/index.tsx index 07662da8e2..2630f65a4b 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/index.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/index.tsx @@ -1,7 +1,7 @@ import { useNotifications } from 'hooks/useNotifications'; import { reverseParser } from 'lib/logql'; import { flatten } from 'lodash-es'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { ILogsReducer } from 'types/reducer/logs'; diff --git a/frontend/src/container/LogsSearchFilter/index.tsx b/frontend/src/container/LogsSearchFilter/index.tsx index efe7a39534..d3de26658b 100644 --- a/frontend/src/container/LogsSearchFilter/index.tsx +++ b/frontend/src/container/LogsSearchFilter/index.tsx @@ -2,14 +2,7 @@ import { Input, InputRef, Popover } from 'antd'; import useUrlQuery from 'hooks/useUrlQuery'; import getStep from 'lib/getStep'; import debounce from 'lodash-es/debounce'; -import React, { - memo, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { connect, useDispatch, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; diff --git a/frontend/src/container/LogsSearchFilter/useSearchParser.ts b/frontend/src/container/LogsSearchFilter/useSearchParser.ts index a3dab8a75c..54b483f4e7 100644 --- a/frontend/src/container/LogsSearchFilter/useSearchParser.ts +++ b/frontend/src/container/LogsSearchFilter/useSearchParser.ts @@ -3,7 +3,7 @@ import useUrlQuery from 'hooks/useUrlQuery'; import history from 'lib/history'; import { parseQuery } from 'lib/logql'; import isEqual from 'lodash-es/isEqual'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; @@ -28,7 +28,7 @@ export function useSearchParser(): { } = useSelector((store) => store.logs); const urlQuery = useUrlQuery(); - const parsedFilters = useMemo(() => urlQuery.get('q'), [urlQuery]); + const parsedFilters = urlQuery.get('q'); const { minTime, maxTime, selectedTime } = useSelector< AppState, @@ -62,16 +62,12 @@ export function useSearchParser(): { }, // need to hide this warning as we don't want to update the query string on every change // eslint-disable-next-line react-hooks/exhaustive-deps - [dispatch, parsedQuery, selectedTime], + [dispatch, parsedQuery, selectedTime, queryString], ); useEffect(() => { - if (!queryString && parsedFilters) { - updateQueryString(parsedFilters); - } else if (queryString) { - updateQueryString(queryString); - } - }, [queryString, updateQueryString, parsedFilters]); + updateQueryString(parsedFilters || ''); + }, [parsedFilters, updateQueryString]); return { queryString, diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 2b0013035d..464258877e 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -6,7 +6,7 @@ import LogsTableView from 'components/Logs/TableView'; import Spinner from 'components/Spinner'; import { contentStyle } from 'container/Trace/Search/config'; import useFontFaceObserver from 'hooks/useFontObserver'; -import React, { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { Virtuoso } from 'react-virtuoso'; // interfaces diff --git a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts index 0175aafac8..82b3856f98 100644 --- a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts +++ b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts @@ -1,3 +1,4 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; import { Widgets } from 'types/api/dashboard/getAll'; import { v4 } from 'uuid'; @@ -7,7 +8,7 @@ export const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => ({ isStacked: false, nullZeroValues: '', opacity: '0', - panelTypes: 'TIME_SERIES', + panelTypes: PANEL_TYPES.TIME_SERIES, query, queryData: { data: { queryData: [] }, diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts index 0b0f9ad4c8..e85b4d4ace 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts @@ -1,8 +1,6 @@ -import { - IMetricsBuilderFormula, - IMetricsBuilderQuery, - IQueryBuilderTagFilterItems, -} from 'types/api/dashboard/getAll'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { QueryBuilderData } from 'types/common/queryBuilder'; import { getQueryBuilderQueries, @@ -13,16 +11,25 @@ export const databaseCallsRPS = ({ servicename, legend, tagFilterItems, -}: DatabaseCallsRPSProps): { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} => { - const metricName = 'signoz_db_latency_count'; - const groupBy = ['db_system']; - const itemsA = [ +}: DatabaseCallsRPSProps): QueryBuilderData => { + const metricName: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_db_latency_count', + type: null, + }; + const groupBy: BaseAutocompleteData[] = [ + { dataType: 'string', isColumn: false, key: 'db_system', type: 'tag' }, + ]; + const itemsA: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, @@ -40,20 +47,32 @@ export const databaseCallsRPS = ({ export const databaseCallsAvgDuration = ({ servicename, tagFilterItems, -}: DatabaseCallProps): { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} => { - const metricNameA = 'signoz_db_latency_sum'; - const metricNameB = 'signoz_db_latency_count'; +}: DatabaseCallProps): QueryBuilderData => { + const metricNameA: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_db_latency_sum', + type: null, + }; + const metricNameB: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_db_latency_count', + type: null, + }; const expression = 'A/B'; const legendFormula = 'Average Duration'; const legend = ''; const disabled = true; - const additionalItemsA = [ + const additionalItemsA: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, @@ -79,5 +98,5 @@ interface DatabaseCallsRPSProps extends DatabaseCallProps { interface DatabaseCallProps { servicename: string | undefined; - tagFilterItems: IQueryBuilderTagFilterItems[] | []; + tagFilterItems: TagFilterItem[]; } diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts index c202fefe65..69af5042f2 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts @@ -1,45 +1,67 @@ -import { - IMetricsBuilderFormula, - IMetricsBuilderQuery, - IQueryBuilderTagFilterItems, -} from 'types/api/dashboard/getAll'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { QueryBuilderData } from 'types/common/queryBuilder'; import { getQueryBuilderQueries, getQueryBuilderQuerieswithFormula, } from './MetricsPageQueriesFactory'; -const groupBy = ['address']; +const groupBy: BaseAutocompleteData[] = [ + { dataType: 'string', isColumn: false, key: 'address', type: 'tag' }, +]; export const externalCallErrorPercent = ({ servicename, legend, tagFilterItems, -}: ExternalCallDurationByAddressProps): { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} => { - const metricNameA = 'signoz_external_call_latency_count'; - const metricNameB = 'signoz_external_call_latency_count'; - const additionalItemsA = [ +}: ExternalCallDurationByAddressProps): QueryBuilderData => { + const metricNameA: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_external_call_latency_count', + type: null, + }; + const metricNameB: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_external_call_latency_count', + type: null, + }; + const additionalItemsA: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, { id: '', - key: 'status_code', + key: { + dataType: 'int64', + isColumn: false, + key: 'status_code', + type: 'tag', + }, op: 'IN', value: ['STATUS_CODE_ERROR'], }, ...tagFilterItems, ]; - const additionalItemsB = [ + const additionalItemsB: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, @@ -64,20 +86,32 @@ export const externalCallErrorPercent = ({ export const externalCallDuration = ({ servicename, tagFilterItems, -}: ExternalCallProps): { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} => { - const metricNameA = 'signoz_external_call_latency_sum'; - const metricNameB = 'signoz_external_call_latency_count'; +}: ExternalCallProps): QueryBuilderData => { + const metricNameA: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_external_call_latency_sum', + type: null, + }; + const metricNameB: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_external_call_latency_count', + type: null, + }; const expression = 'A/B'; const legendFormula = 'Average Duration'; const legend = ''; const disabled = true; - const additionalItemsA = [ + const additionalItemsA: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, @@ -101,15 +135,22 @@ export const externalCallRpsByAddress = ({ servicename, legend, tagFilterItems, -}: ExternalCallDurationByAddressProps): { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} => { - const metricName = 'signoz_external_call_latency_count'; - const itemsA = [ +}: ExternalCallDurationByAddressProps): QueryBuilderData => { + const metricName: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_external_call_latency_count', + type: null, + }; + const itemsA: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, @@ -127,19 +168,31 @@ export const externalCallDurationByAddress = ({ servicename, legend, tagFilterItems, -}: ExternalCallDurationByAddressProps): { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} => { - const metricNameA = 'signoz_external_call_latency_sum'; - const metricNameB = 'signoz_external_call_latency_count'; +}: ExternalCallDurationByAddressProps): QueryBuilderData => { + const metricNameA: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_external_call_latency_sum', + type: null, + }; + const metricNameB: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_external_call_latency_count', + type: null, + }; const expression = 'A/B'; const legendFormula = legend; const disabled = true; - const additionalItemsA = [ + const additionalItemsA: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, @@ -166,5 +219,5 @@ interface ExternalCallDurationByAddressProps extends ExternalCallProps { export interface ExternalCallProps { servicename: string | undefined; - tagFilterItems: IQueryBuilderTagFilterItems[]; + tagFilterItems: TagFilterItem[]; } diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts index 4654ebf20f..57d4829ea3 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts @@ -1,29 +1,31 @@ import { - IMetricsBuilderFormula, - IMetricsBuilderQuery, - IQueryBuilderTagFilterItems, -} from 'types/api/dashboard/getAll'; + initialFormulaBuilderFormValues, + initialQueryBuilderFormValues, +} from 'constants/queryBuilder'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { + MetricAggregateOperator, + QueryBuilderData, +} from 'types/common/queryBuilder'; export const getQueryBuilderQueries = ({ metricName, - groupBy, + groupBy = [], legend, itemsA, -}: BuilderQueriesProps): { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} => ({ - formulas: [], - queryBuilder: [ +}: BuilderQueriesProps): QueryBuilderData => ({ + queryFormulas: [], + queryData: [ { - aggregateOperator: 18, + ...initialQueryBuilderFormValues, + aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled: false, groupBy, + aggregateAttribute: metricName, legend, - metricName, - name: 'A', - reduceTo: 1, - tagFilters: { + reduceTo: 'sum', + filters: { items: itemsA, op: 'AND', }, @@ -37,45 +39,43 @@ export const getQueryBuilderQuerieswithFormula = ({ additionalItemsA, additionalItemsB, legend, - groupBy, + groupBy = [], disabled, expression, legendFormula, -}: BuilderQuerieswithFormulaProps): { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} => ({ - formulas: [ +}: BuilderQuerieswithFormulaProps): QueryBuilderData => ({ + queryFormulas: [ { - disabled: false, + ...initialFormulaBuilderFormValues, expression, - name: 'F1', legend: legendFormula, }, ], - queryBuilder: [ + queryData: [ { - aggregateOperator: 18, + ...initialQueryBuilderFormValues, + aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled, groupBy, legend, - metricName: metricNameA, - name: 'A', - reduceTo: 1, - tagFilters: { + aggregateAttribute: metricNameA, + reduceTo: 'sum', + filters: { items: additionalItemsA, op: 'AND', }, }, { - aggregateOperator: 18, + ...initialQueryBuilderFormValues, + aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled, groupBy, legend, - metricName: metricNameB, - name: 'B', - reduceTo: 1, - tagFilters: { + aggregateAttribute: metricNameB, + queryName: 'B', + expression: 'B', + reduceTo: 'sum', + filters: { items: additionalItemsB, op: 'AND', }, @@ -84,20 +84,20 @@ export const getQueryBuilderQuerieswithFormula = ({ }); interface BuilderQueriesProps { - metricName: string; - groupBy?: string[]; + metricName: BaseAutocompleteData; + groupBy?: BaseAutocompleteData[]; legend: string; - itemsA: IQueryBuilderTagFilterItems[]; + itemsA: TagFilterItem[]; } interface BuilderQuerieswithFormulaProps { - metricNameA: string; - metricNameB: string; + metricNameA: BaseAutocompleteData; + metricNameB: BaseAutocompleteData; legend: string; disabled: boolean; - groupBy?: string[]; + groupBy?: BaseAutocompleteData[]; expression: string; legendFormula: string; - additionalItemsA: IQueryBuilderTagFilterItems[]; - additionalItemsB: IQueryBuilderTagFilterItems[]; + additionalItemsA: TagFilterItem[]; + additionalItemsB: TagFilterItem[]; } diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts index 8c5f95f943..97a72c4fd2 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts @@ -1,8 +1,6 @@ -import { - IMetricsBuilderFormula, - IMetricsBuilderQuery, - IQueryBuilderTagFilterItems, -} from 'types/api/dashboard/getAll'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { QueryBuilderData } from 'types/common/queryBuilder'; import { getQueryBuilderQueries, @@ -13,19 +11,35 @@ export const operationPerSec = ({ servicename, tagFilterItems, topLevelOperations, -}: OperationPerSecProps): IOverviewQueries => { - const metricName = 'signoz_latency_count'; +}: OperationPerSecProps): QueryBuilderData => { + const metricName: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_latency_count', + type: null, + }; const legend = 'Operations'; - const itemsA = [ + + const itemsA: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, { id: '', - key: 'operation', + key: { + dataType: 'string', + isColumn: false, + key: 'operation', + type: 'tag', + }, op: 'IN', value: topLevelOperations, }, @@ -43,41 +57,76 @@ export const errorPercentage = ({ servicename, tagFilterItems, topLevelOperations, -}: OperationPerSecProps): IOverviewQueries => { - const metricNameA = 'signoz_calls_total'; - const metricNameB = 'signoz_calls_total'; - const additionalItemsA = [ +}: OperationPerSecProps): QueryBuilderData => { + const metricNameA: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_calls_total', + type: null, + }; + const metricNameB: BaseAutocompleteData = { + dataType: 'float64', + isColumn: true, + key: 'signoz_calls_total', + type: null, + }; + const additionalItemsA: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, { id: '', - key: 'operation', + key: { + dataType: 'string', + isColumn: false, + key: 'operation', + type: 'tag', + }, op: 'IN', value: topLevelOperations, }, { id: '', - key: 'status_code', + key: { + dataType: 'int64', + isColumn: false, + key: 'status_code', + type: 'tag', + }, op: 'IN', value: ['STATUS_CODE_ERROR'], }, ...tagFilterItems, ]; - const additionalItemsB = [ + const additionalItemsB: TagFilterItem[] = [ { id: '', - key: 'service_name', + key: { + dataType: 'string', + isColumn: false, + key: 'service_name', + type: 'resource', + }, op: 'IN', value: [`${servicename}`], }, { id: '', - key: 'operation', + key: { + dataType: 'string', + isColumn: false, + key: 'operation', + type: 'tag', + }, op: 'IN', value: topLevelOperations, }, @@ -102,11 +151,6 @@ export const errorPercentage = ({ export interface OperationPerSecProps { servicename: string | undefined; - tagFilterItems: IQueryBuilderTagFilterItems[]; + tagFilterItems: TagFilterItem[]; topLevelOperations: string[]; } - -interface IOverviewQueries { - formulas: IMetricsBuilderFormula[]; - queryBuilder: IMetricsBuilderQuery[]; -} diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index f5ce2a0188..ca4c374125 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -9,9 +9,11 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Widgets } from 'types/api/dashboard/getAll'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { EQueryType } from 'types/common/dashboard'; import { Card, GraphContainer, GraphTitle, Row } from '../styles'; import { Button } from './styles'; @@ -27,7 +29,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); const { queries } = useResourceAttribute(); - const tagFilterItems = useMemo( + const tagFilterItems: TagFilterItem[] = useMemo( () => handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [], [queries], @@ -46,27 +48,27 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { const databaseCallsRPSWidget = useMemo( () => getWidgetQueryBuilder({ - queryType: 1, - promQL: [], - metricsBuilder: databaseCallsRPS({ + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: databaseCallsRPS({ servicename, legend, tagFilterItems, }), - clickHouse: [], + clickhouse_sql: [], }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); const databaseCallsAverageDurationWidget = useMemo( () => getWidgetQueryBuilder({ - queryType: 1, - promQL: [], - metricsBuilder: databaseCallsAvgDuration({ + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: databaseCallsAvgDuration({ servicename, tagFilterItems, }), - clickHouse: [], + clickhouse_sql: [], }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index c615179d65..1a4a511653 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -11,9 +11,10 @@ import { convertRawQueriesToTraceSelectedTags, resourceAttributesToTagFilterItems, } from 'hooks/useResourceAttribute/utils'; -import React, { useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Widgets } from 'types/api/dashboard/getAll'; +import { EQueryType } from 'types/common/dashboard'; import { Card, GraphContainer, GraphTitle, Row } from '../styles'; import { legend } from './constant'; @@ -39,14 +40,14 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { const externalCallErrorWidget = useMemo( () => getWidgetQueryBuilder({ - queryType: 1, - promQL: [], - metricsBuilder: externalCallErrorPercent({ + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: externalCallErrorPercent({ servicename, legend: legend.address, tagFilterItems, }), - clickHouse: [], + clickhouse_sql: [], }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -59,13 +60,13 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { const externalCallDurationWidget = useMemo( () => getWidgetQueryBuilder({ - queryType: 1, - promQL: [], - metricsBuilder: externalCallDuration({ + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: externalCallDuration({ servicename, tagFilterItems, }), - clickHouse: [], + clickhouse_sql: [], }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -73,14 +74,14 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { const externalCallRPSWidget = useMemo( () => getWidgetQueryBuilder({ - queryType: 1, - promQL: [], - metricsBuilder: externalCallRpsByAddress({ + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: externalCallRpsByAddress({ servicename, legend: legend.address, tagFilterItems, }), - clickHouse: [], + clickhouse_sql: [], }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -88,14 +89,14 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { const externalCallDurationAddressWidget = useMemo( () => getWidgetQueryBuilder({ - queryType: 1, - promQL: [], - metricsBuilder: externalCallDurationByAddress({ + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: externalCallDurationByAddress({ servicename, legend: legend.address, tagFilterItems, }), - clickHouse: [], + clickhouse_sql: [], }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index f540a69896..3fb81df2e0 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -1,8 +1,10 @@ import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js'; import Graph from 'components/Graph'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; +import { QueryParams } from 'constants/query'; import ROUTES from 'constants/routes'; import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder'; +import { routeConfig } from 'container/SideNav/config'; +import { getQueryString } from 'container/SideNav/helper'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { convertRawQueriesToTraceSelectedTags, @@ -11,12 +13,13 @@ import { import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond'; import { colors } from 'lib/getRandomColor'; import history from 'lib/history'; -import React, { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router-dom'; import { UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; import { Widgets } from 'types/api/dashboard/getAll'; +import { EQueryType } from 'types/common/dashboard'; import MetricReducer from 'types/reducer/metrics'; import { @@ -35,6 +38,7 @@ import { function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { const { servicename } = useParams<{ servicename?: string }>(); const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); + const { search } = useLocation(); const handleSetTimeStamp = useCallback((selectTime: number) => { setSelectedTimeStamp(selectTime); @@ -79,14 +83,14 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { const operationPerSecWidget = useMemo( () => getWidgetQueryBuilder({ - queryType: 1, - promQL: [], - metricsBuilder: operationPerSec({ + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: operationPerSec({ servicename, tagFilterItems, topLevelOperations, }), - clickHouse: [], + clickhouse_sql: [], }), [getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems], ); @@ -94,14 +98,14 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { const errorPercentageWidget = useMemo( () => getWidgetQueryBuilder({ - queryType: 1, - promQL: [], - metricsBuilder: errorPercentage({ + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: errorPercentage({ servicename, tagFilterItems, topLevelOperations, }), - clickHouse: [], + clickhouse_sql: [], }), [servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder], ); @@ -122,14 +126,19 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { const currentTime = timestamp; const tPlusOne = timestamp + 60 * 1000; - const urlParams = new URLSearchParams(); - urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); - urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); + const urlParams = new URLSearchParams(search); + urlParams.set(QueryParams.startTime, currentTime.toString()); + urlParams.set(QueryParams.endTime, tPlusOne.toString()); + + const avialableParams = routeConfig[ROUTES.TRACE]; + const queryString = getQueryString(avialableParams, urlParams); history.replace( `${ ROUTES.TRACE - }?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1`, + }?selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&isFilterExclude={"serviceName":false,"status":false}&userSelectedFilter={"serviceName":["${servicename}"],"status":["error"]}&spanAggregateCurrentPage=1&${queryString.join( + '', + )}`, ); }; diff --git a/frontend/src/container/MetricsApplication/Tabs/util.ts b/frontend/src/container/MetricsApplication/Tabs/util.ts index f6e78a6d6f..b33bbf0767 100644 --- a/frontend/src/container/MetricsApplication/Tabs/util.ts +++ b/frontend/src/container/MetricsApplication/Tabs/util.ts @@ -1,8 +1,11 @@ import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; +import { QueryParams } from 'constants/query'; import ROUTES from 'constants/routes'; +import { routeConfig } from 'container/SideNav/config'; +import { getQueryString } from 'container/SideNav/helper'; import history from 'lib/history'; -import { IQueryBuilderTagFilterItems } from 'types/api/dashboard/getAll'; +import { Dispatch, SetStateAction } from 'react'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { Tags } from 'types/reducer/trace'; export const dbSystemTags: Tags[] = [ @@ -31,24 +34,24 @@ export function onViewTracePopupClick({ const currentTime = timestamp; const tPlusOne = timestamp + 60 * 1000; - const urlParams = new URLSearchParams(); - urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); - urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); + const urlParams = new URLSearchParams(window.location.search); + urlParams.set(QueryParams.startTime, currentTime.toString()); + urlParams.set(QueryParams.endTime, tPlusOne.toString()); + const avialableParams = routeConfig[ROUTES.TRACE]; + const queryString = getQueryString(avialableParams, urlParams); history.replace( `${ ROUTES.TRACE }?${urlParams.toString()}&selected={"serviceName":["${servicename}"]}&filterToFetchData=["duration","status","serviceName"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&spanAggregateCurrentPage=1${ isExternalCall ? '&spanKind=3' : '' - }`, + }&${queryString.join('&')}`, ); }; } export function onGraphClickHandler( - setSelectedTimeStamp: ( - n: number, - ) => void | React.Dispatch>, + setSelectedTimeStamp: (n: number) => void | Dispatch>, ) { return async ( event: ChartEvent, @@ -86,9 +89,7 @@ export function onGraphClickHandler( }; } -export const handleNonInQueryRange = ( - tags: IQueryBuilderTagFilterItems[], -): IQueryBuilderTagFilterItems[] => +export const handleNonInQueryRange = (tags: TagFilterItem[]): TagFilterItem[] => tags.map((tag) => { if (tag.op === 'Not IN') { return { diff --git a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx index 010a60c8e2..540bc0d546 100644 --- a/frontend/src/container/MetricsApplication/TopOperationsTable.tsx +++ b/frontend/src/container/MetricsApplication/TopOperationsTable.tsx @@ -1,12 +1,11 @@ import { Tooltip, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { ResizeTable } from 'components/ResizeTable'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; +import { QueryParams } from 'constants/query'; import ROUTES from 'constants/routes'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import history from 'lib/history'; -import React from 'react'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; @@ -29,14 +28,8 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { const handleOnClick = (operation: string): void => { const urlParams = new URLSearchParams(); const { servicename } = params; - urlParams.set( - METRICS_PAGE_QUERY_PARAM.startTime, - (minTime / 1000000).toString(), - ); - urlParams.set( - METRICS_PAGE_QUERY_PARAM.endTime, - (maxTime / 1000000).toString(), - ); + urlParams.set(QueryParams.startTime, (minTime / 1000000).toString()); + urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString()); history.push( `${ @@ -45,7 +38,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { ); }; - const columns: ColumnsType = [ + const columns: ColumnsType = [ { title: 'Name', dataIndex: 'name', @@ -64,7 +57,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { dataIndex: 'p50', key: 'p50', width: 50, - sorter: (a: DataProps, b: DataProps): number => a.p50 - b.p50, + sorter: (a: TopOperationList, b: TopOperationList): number => a.p50 - b.p50, render: (value: number): string => (value / 1000000).toFixed(2), }, { @@ -72,7 +65,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { dataIndex: 'p95', key: 'p95', width: 50, - sorter: (a: DataProps, b: DataProps): number => a.p95 - b.p95, + sorter: (a: TopOperationList, b: TopOperationList): number => a.p95 - b.p95, render: (value: number): string => (value / 1000000).toFixed(2), }, { @@ -80,7 +73,7 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { dataIndex: 'p99', key: 'p99', width: 50, - sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99, + sorter: (a: TopOperationList, b: TopOperationList): number => a.p99 - b.p99, render: (value: number): string => (value / 1000000).toFixed(2), }, { @@ -88,9 +81,19 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { dataIndex: 'numCalls', key: 'numCalls', width: 50, - sorter: (a: TopOperationListItem, b: TopOperationListItem): number => + sorter: (a: TopOperationList, b: TopOperationList): number => a.numCalls - b.numCalls, }, + { + title: 'Error Rate', + dataIndex: 'errorCount', + key: 'errorCount', + width: 50, + sorter: (a: TopOperationList, b: TopOperationList): number => + a.errorCount - b.errorCount, + render: (value: number, record: TopOperationList): string => + `${((value / record.numCalls) * 100).toFixed(2)} %`, + }, ]; return ( @@ -105,18 +108,17 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { ); } -interface TopOperationListItem { +export interface TopOperationList { p50: number; p95: number; p99: number; numCalls: number; name: string; + errorCount: number; } -type DataProps = TopOperationListItem; - interface TopOperationsTableProps { - data: TopOperationListItem[]; + data: TopOperationList[]; } export default TopOperationsTable; diff --git a/frontend/src/container/MetricsApplication/index.tsx b/frontend/src/container/MetricsApplication/index.tsx index 0e189fb1bd..e0d76100ec 100644 --- a/frontend/src/container/MetricsApplication/index.tsx +++ b/frontend/src/container/MetricsApplication/index.tsx @@ -1,7 +1,7 @@ import RouteTab from 'components/RouteTab'; import ROUTES from 'constants/routes'; import ResourceAttributesFilter from 'container/ResourceAttributesFilter'; -import React, { memo } from 'react'; +import { memo } from 'react'; import { generatePath, useParams } from 'react-router-dom'; import { useLocation } from 'react-use'; diff --git a/frontend/src/container/MetricsTable/Metrics.test.tsx b/frontend/src/container/MetricsTable/Metrics.test.tsx new file mode 100644 index 0000000000..74239c19f7 --- /dev/null +++ b/frontend/src/container/MetricsTable/Metrics.test.tsx @@ -0,0 +1,70 @@ +import { render, RenderResult, screen, waitFor } from '@testing-library/react'; +import { ReactElement } from 'react'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { + combineReducers, + legacy_createStore as createStore, + Store, +} from 'redux'; + +import { InitialValue } from '../../store/reducers/metric'; +import Metrics from './index'; + +const rootReducer = combineReducers({ + metrics: (state = InitialValue) => state, +}); + +const mockStore = createStore(rootReducer); + +const renderWithReduxAndRouter = (mockStore: Store) => ( + component: ReactElement, +): RenderResult => + render( + + {component} + , + ); + +describe('Metrics Component', () => { + it('renders without errors', async () => { + renderWithReduxAndRouter(mockStore)(); + + await waitFor(() => { + expect(screen.getByText(/application/i)).toBeInTheDocument(); + expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument(); + expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument(); + expect(screen.getByText(/operations per second/i)).toBeInTheDocument(); + }); + }); + + it('renders loading when required conditions are met', async () => { + const customStore = createStore(rootReducer, { + metrics: { + services: [], + loading: true, + error: false, + }, + }); + + const { container } = renderWithReduxAndRouter(customStore)(); + + const spinner = container.querySelector('.ant-spin-nested-loading'); + + expect(spinner).toBeInTheDocument(); + }); + + it('renders no data when required conditions are met', async () => { + const customStore = createStore(rootReducer, { + metrics: { + services: [], + loading: false, + error: false, + }, + }); + + renderWithReduxAndRouter(customStore)(); + + expect(screen.getByText('No data')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx b/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx index 2018d49a9d..aedc3d4e43 100644 --- a/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx +++ b/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx @@ -1,6 +1,5 @@ import { Button, Typography } from 'antd'; import Modal from 'components/Modal'; -import React from 'react'; function SkipOnBoardingModal({ onContinueClick }: Props): JSX.Element { return ( diff --git a/frontend/src/container/MetricsTable/index.tsx b/frontend/src/container/MetricsTable/index.tsx index c815769f54..9fee92c811 100644 --- a/frontend/src/container/MetricsTable/index.tsx +++ b/frontend/src/container/MetricsTable/index.tsx @@ -11,7 +11,9 @@ import localStorageSet from 'api/browser/localstorage/set'; import { ResizeTable } from 'components/ResizeTable'; import { SKIP_ONBOARDING } from 'constants/onboarding'; import ROUTES from 'constants/routes'; -import React, { useCallback, useMemo, useState } from 'react'; +import { routeConfig } from 'container/SideNav/config'; +import { getQueryString } from 'container/SideNav/helper'; +import { useCallback, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { Link, useLocation } from 'react-router-dom'; import { AppState } from 'store/reducers'; @@ -88,11 +90,17 @@ function Metrics(): JSX.Element { .toString() .toLowerCase() .includes(value.toString().toLowerCase()), - render: (text: string): JSX.Element => ( - - {text} - - ), + render: (metrics: string): JSX.Element => { + const urlParams = new URLSearchParams(search); + const avialableParams = routeConfig[ROUTES.SERVICE_METRICS]; + const queryString = getQueryString(avialableParams, urlParams); + + return ( + + {metrics} + + ); + }, }), [filterDropdown, FilterIcon, search], ); diff --git a/frontend/src/container/MySettings/Password/index.tsx b/frontend/src/container/MySettings/Password/index.tsx index 1d810de6cb..33b8f9c5f2 100644 --- a/frontend/src/container/MySettings/Password/index.tsx +++ b/frontend/src/container/MySettings/Password/index.tsx @@ -2,7 +2,7 @@ import { Button, Space, Typography } from 'antd'; import changeMyPassword from 'api/user/changeMyPassword'; import { useNotifications } from 'hooks/useNotifications'; import { isPasswordNotValidMessage, isPasswordValid } from 'pages/SignUp/utils'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; diff --git a/frontend/src/container/MySettings/UpdateName/index.tsx b/frontend/src/container/MySettings/UpdateName/index.tsx index df20b7141d..6da15a237a 100644 --- a/frontend/src/container/MySettings/UpdateName/index.tsx +++ b/frontend/src/container/MySettings/UpdateName/index.tsx @@ -1,7 +1,7 @@ import { Button, Space, Typography } from 'antd'; import editUser from 'api/user/editUser'; import { useNotifications } from 'hooks/useNotifications'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { Dispatch } from 'redux'; diff --git a/frontend/src/container/MySettings/index.tsx b/frontend/src/container/MySettings/index.tsx index 7ce9e02e42..f0938e6440 100644 --- a/frontend/src/container/MySettings/index.tsx +++ b/frontend/src/container/MySettings/index.tsx @@ -1,5 +1,4 @@ import { Space, Typography } from 'antd'; -import React from 'react'; import { useTranslation } from 'react-i18next'; import Password from './Password'; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index c7936db261..3051fa07c3 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -3,7 +3,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import React, { useCallback } from 'react'; +import { CSSProperties, useCallback } from 'react'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; @@ -54,7 +54,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { [data, toggleAddWidget, notifications], ); const isDarkMode = useIsDarkMode(); - const fillColor: React.CSSProperties['color'] = isDarkMode ? 'white' : 'black'; + const fillColor: CSSProperties['color'] = isDarkMode ? 'white' : 'black'; return ( diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts index 12eeab0751..5395b491a3 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts @@ -1,20 +1,22 @@ import TimeSeries from 'assets/Dashboard/TimeSeries'; import ValueIcon from 'assets/Dashboard/Value'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { CSSProperties } from 'react'; const Items: ItemsProps[] = [ { - name: 'TIME_SERIES', + name: PANEL_TYPES.TIME_SERIES, Icon: TimeSeries, display: 'Time Series', }, { - name: 'VALUE', + name: PANEL_TYPES.VALUE, Icon: ValueIcon, display: 'Value', }, ]; -export type ITEMS = 'TIME_SERIES' | 'VALUE' | 'EMPTY_WIDGET'; +export type ITEMS = 'graph' | 'value' | 'list' | 'table' | 'EMPTY_WIDGET'; interface ItemsProps { name: ITEMS; @@ -23,7 +25,7 @@ interface ItemsProps { } interface IconProps { - fillColor: React.CSSProperties['color']; + fillColor: CSSProperties['color']; } export default Items; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx index b7e2caf881..15b89f4b8e 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/AddTags/index.tsx @@ -1,7 +1,7 @@ import { PlusOutlined } from '@ant-design/icons'; import { Col, Tooltip, Typography } from 'antd'; import Input from 'components/Input'; -import React, { useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import { InputContainer, NewTagContainer, TagsContainer } from './styles'; @@ -38,7 +38,7 @@ function AddTags({ tags, setTags }: AddTagsProps): JSX.Element { const onChangeHandler = ( value: string, - func: React.Dispatch>, + func: Dispatch>, ): void => { func(value); }; @@ -113,7 +113,7 @@ function AddTags({ tags, setTags }: AddTagsProps): JSX.Element { interface AddTagsProps { tags: string[]; - setTags: React.Dispatch>; + setTags: Dispatch>; } export default AddTags; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/Description/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/Description/index.tsx index aad663d7a3..849ad8791f 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/Description/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/Description/index.tsx @@ -1,5 +1,5 @@ import { Input } from 'antd'; -import React, { useCallback } from 'react'; +import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react'; import { Container } from './styles'; @@ -10,7 +10,7 @@ function Description({ setDescription, }: DescriptionProps): JSX.Element { const onChangeHandler = useCallback( - (e: React.ChangeEvent) => { + (e: ChangeEvent) => { setDescription(e.target.value); }, [setDescription], @@ -29,7 +29,7 @@ function Description({ interface DescriptionProps { description: string; - setDescription: React.Dispatch>; + setDescription: Dispatch>; } export default Description; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx index 4a8fce57a2..a8f1892fa4 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/General/index.tsx @@ -1,7 +1,7 @@ import { SaveOutlined } from '@ant-design/icons'; import { Col, Divider, Input, Space, Typography } from 'antd'; import AddTags from 'container/NewDashboard/DashboardSettings/General/AddTags'; -import React, { useCallback, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx index 80b77283aa..05802a9b9e 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/VariableItem/VariableItem.tsx @@ -15,7 +15,7 @@ import Editor from 'components/Editor'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import { map } from 'lodash-es'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { IDashboardVariable, TSortVariableValuesType, diff --git a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx index 9ca7c6fb83..3f90bf565b 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/Variables/index.tsx @@ -4,7 +4,7 @@ import { Button, Modal, Row, Space, Tag } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import { ResizeTable } from 'components/ResizeTable'; import { useNotifications } from 'hooks/useNotifications'; -import React, { useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; diff --git a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx index 40176c3d1e..50a69495fa 100644 --- a/frontend/src/container/NewDashboard/DashboardSettings/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardSettings/index.tsx @@ -1,5 +1,4 @@ import { Tabs } from 'antd'; -import React from 'react'; import GeneralDashboardSettings from './General'; import VariablesSetting from './Variables'; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx new file mode 100644 index 0000000000..7543821b60 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx @@ -0,0 +1,168 @@ +import '@testing-library/jest-dom/extend-expect'; + +import { fireEvent, render, screen } from '@testing-library/react'; +import React, { useEffect } from 'react'; +import { IDashboardVariable } from 'types/api/dashboard/getAll'; + +import VariableItem from './VariableItem'; + +const mockVariableData: IDashboardVariable = { + description: 'Test Variable', + type: 'TEXTBOX', + textboxValue: 'defaultValue', + sort: 'DISABLED', + multiSelect: false, + showALLOption: false, + name: 'testVariable', +}; + +// New mock data for a custom variable +const mockCustomVariableData: IDashboardVariable = { + ...mockVariableData, + name: 'customVariable', + type: 'CUSTOM', + customValue: 'option1,option2,option3', +}; + +const mockOnValueUpdate = jest.fn(); +const mockOnAllSelectedUpdate = jest.fn(); + +describe('VariableItem', () => { + let useEffectSpy: jest.SpyInstance; + + beforeEach(() => { + useEffectSpy = jest.spyOn(React, 'useEffect'); + }); + + afterEach(() => { + jest.clearAllMocks(); + useEffectSpy.mockRestore(); + }); + + test('renders component with default props', () => { + render( + , + ); + + expect(screen.getByText('$testVariable')).toBeInTheDocument(); + }); + + test('renders Input when the variable type is TEXTBOX', () => { + render( + , + ); + expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument(); + }); + + test('calls onChange event handler when Input value changes', () => { + render( + , + ); + const inputElement = screen.getByPlaceholderText('Enter value'); + fireEvent.change(inputElement, { target: { value: 'newValue' } }); + + expect(mockOnValueUpdate).toHaveBeenCalledTimes(1); + expect(mockOnValueUpdate).toHaveBeenCalledWith('testVariable', 'newValue'); + expect(mockOnAllSelectedUpdate).toHaveBeenCalledTimes(1); + expect(mockOnAllSelectedUpdate).toHaveBeenCalledWith('testVariable', false); + }); + + test('renders a Select element when variable type is CUSTOM', () => { + render( + , + ); + + expect(screen.getByText('$customVariable')).toBeInTheDocument(); + expect(screen.getByTestId('variable-select')).toBeInTheDocument(); + }); + + test('renders a Select element with all selected', async () => { + const customVariableData = { + ...mockCustomVariableData, + allSelected: true, + }; + + render( + , + ); + + expect(screen.getByTitle('ALL')).toBeInTheDocument(); + }); + + test('calls useEffect when the component mounts', () => { + render( + , + ); + + expect(useEffect).toHaveBeenCalled(); + }); + + test('calls useEffect only once when the component mounts', () => { + // Render the component + const { rerender } = render( + , + ); + + // Create an updated version of the mock data + const updatedMockCustomVariableData = { + ...mockCustomVariableData, + selectedValue: 'option1', + }; + + // Re-render the component with the updated data + rerender( + , + ); + + // Check if the useEffect is called with the correct arguments + expect(useEffectSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index 0aa6e61b90..ef85297d92 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -4,32 +4,24 @@ import { Input, Popover, Select, Typography } from 'antd'; import query from 'api/dashboard/variables/query'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import sortValues from 'lib/dashbaordVariables/sortVariableValues'; -import { map } from 'lodash-es'; -import React, { memo, useCallback, useEffect, useState } from 'react'; +import map from 'lodash-es/map'; +import { memo, useCallback, useEffect, useState } from 'react'; import { IDashboardVariable } from 'types/api/dashboard/getAll'; import { variablePropsToPayloadVariables } from '../utils'; import { SelectItemStyle, VariableContainer, VariableName } from './styles'; import { areArraysEqual } from './util'; -const { Option } = Select; - const ALL_SELECT_VALUE = '__ALL__'; interface VariableItemProps { variableData: IDashboardVariable; existingVariables: Record; onValueUpdate: ( - name: string | undefined, - arg1: - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined, + name: string, + arg1: IDashboardVariable['selectedValue'], ) => void; - onAllSelectedUpdate: (name: string | undefined, arg1: boolean) => void; + onAllSelectedUpdate: (name: string, arg1: boolean) => void; lastUpdatedVar: string; } function VariableItem({ @@ -101,8 +93,10 @@ function VariableItem({ } else { [value] = newOptionsData; } - onValueUpdate(variableData.name, value); - onAllSelectedUpdate(variableData.name, allSelected); + if (variableData.name) { + onValueUpdate(variableData.name, value); + onAllSelectedUpdate(variableData.name, allSelected); + } } setOptionsData(newOptionsData); } @@ -133,17 +127,18 @@ function VariableItem({ }, [variableData, existingVariables]); const handleChange = (value: string | string[]): void => { - if ( - value === ALL_SELECT_VALUE || - (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || - (Array.isArray(value) && value.length === 0) - ) { - onValueUpdate(variableData.name, optionsData); - onAllSelectedUpdate(variableData.name, true); - } else { - onValueUpdate(variableData.name, value); - onAllSelectedUpdate(variableData.name, false); - } + if (variableData.name) + if ( + value === ALL_SELECT_VALUE || + (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || + (Array.isArray(value) && value.length === 0) + ) { + onValueUpdate(variableData.name, optionsData); + onAllSelectedUpdate(variableData.name, true); + } else { + onValueUpdate(variableData.name, value); + onAllSelectedUpdate(variableData.name, false); + } }; const selectValue = variableData.allSelected @@ -182,10 +177,21 @@ function VariableItem({ style={SelectItemStyle} loading={isLoading} showArrow + data-testid="variable-select" > - {enableSelectAll && } + {enableSelectAll && ( + + ALL + + )} {map(optionsData, (option) => ( - + + {option.toString()} + ))} ) diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx index 9652782fd5..67d961f5c9 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/index.tsx @@ -2,7 +2,7 @@ import { Row } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import { useNotifications } from 'hooks/useNotifications'; import { map, sortBy } from 'lodash-es'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/utils.test.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/utils.test.ts new file mode 100644 index 0000000000..c258849a74 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/utils.test.ts @@ -0,0 +1,33 @@ +import { areArraysEqual } from './util'; + +describe('areArraysEqual', () => { + it('should return true for equal arrays with same order', () => { + const array1 = [1, 'a', true, 5, 'hello']; + const array2 = [1, 'a', true, 5, 'hello']; + expect(areArraysEqual(array1, array2)).toBe(true); + }); + + it('should return false for equal arrays with different order', () => { + const array1 = [1, 'a', true, 5, 'hello']; + const array2 = ['hello', 1, true, 'a', 5]; + expect(areArraysEqual(array1, array2)).toBe(false); + }); + + it('should return false for arrays with different lengths', () => { + const array1 = [1, 'a', true, 5, 'hello']; + const array2 = [1, 'a', true, 5]; + expect(areArraysEqual(array1, array2)).toBe(false); + }); + + it('should return false for arrays with different elements', () => { + const array1 = [1, 'a', true, 5, 'hello']; + const array2 = [1, 'a', true, 5, 'world']; + expect(areArraysEqual(array1, array2)).toBe(false); + }); + + it('should return true for empty arrays', () => { + const array1: string[] = []; + const array2: string[] = []; + expect(areArraysEqual(array1, array2)).toBe(true); + }); +}); diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx index a8b8871190..32f78d7ec2 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx @@ -1,12 +1,12 @@ import Input from 'components/Input'; -import React, { useCallback } from 'react'; +import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react'; function NameOfTheDashboard({ setName, name, }: NameOfTheDashboardProps): JSX.Element { const onChangeHandler = useCallback( - (e: React.ChangeEvent) => { + (e: ChangeEvent) => { setName(e.target.value); }, [setName], @@ -24,7 +24,7 @@ function NameOfTheDashboard({ interface NameOfTheDashboardProps { name: string; - setName: React.Dispatch>; + setName: Dispatch>; } export default NameOfTheDashboard; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx index b8f6617204..33af3bdb3a 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx @@ -1,6 +1,6 @@ import { SettingOutlined } from '@ant-design/icons'; import { Button } from 'antd'; -import React, { useState } from 'react'; +import { useState } from 'react'; import DashboardSettingsContent from '../DashboardSettings'; import { DrawerContainer } from './styles'; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/ShareModal.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/ShareModal.tsx index 6e5bd00e9f..51d83b23a3 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/ShareModal.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/ShareModal.tsx @@ -1,7 +1,7 @@ import { Button, Modal, Typography } from 'antd'; import Editor from 'components/Editor'; import { useNotifications } from 'hooks/useNotifications'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useCopyToClipboard } from 'react-use'; import { DashboardData } from 'types/api/dashboard/getAll'; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx index c1a98cce8f..2faafd7cbb 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx @@ -1,7 +1,7 @@ import { ShareAltOutlined } from '@ant-design/icons'; import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd'; import useComponentPermission from 'hooks/useComponentPermission'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; diff --git a/frontend/src/container/NewDashboard/GridGraphs/index.tsx b/frontend/src/container/NewDashboard/GridGraphs/index.tsx index 5436f43704..b6fb01700a 100644 --- a/frontend/src/container/NewDashboard/GridGraphs/index.tsx +++ b/frontend/src/container/NewDashboard/GridGraphs/index.tsx @@ -1,6 +1,5 @@ import GridGraphLayout from 'container/GridGraphLayout'; import ComponentsSlider from 'container/NewDashboard/ComponentsSlider'; -import React from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import DashboardReducer from 'types/reducer/dashboards'; diff --git a/frontend/src/container/NewDashboard/index.tsx b/frontend/src/container/NewDashboard/index.tsx index 8fbe49de84..5eff095c6f 100644 --- a/frontend/src/container/NewDashboard/index.tsx +++ b/frontend/src/container/NewDashboard/index.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import Description from './DescriptionOfDashboard'; import GridGraphs from './GridGraphs'; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/Options.ts b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/Options.ts deleted file mode 100644 index 1ee8680391..0000000000 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/Options.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { EAggregateOperator } from 'types/common/dashboard'; - -export const AggregateFunctions = Object.keys(EAggregateOperator) - .filter((key) => Number.isNaN(parseInt(key, 10))) - .map((key) => ({ - label: key, - value: EAggregateOperator[key as keyof typeof EAggregateOperator], - })); - -export const TagKeyOperator = [ - { label: 'In', value: 'IN' }, - { label: 'Not In', value: 'NIN' }, - { label: 'Like', value: 'LIKE' }, - { label: 'Not Like', value: 'NLIKE' }, - // { label: 'Equal', value: 'EQ' }, - // { label: 'Not Equal', value: 'NEQ' }, - // { label: 'REGEX', value: 'REGEX' }, -]; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/QueryHeader.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/QueryHeader.tsx index 6584cdc390..b6bc5a6f17 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/QueryHeader.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/QueryHeader.tsx @@ -6,7 +6,7 @@ import { RightOutlined, } from '@ant-design/icons'; import { Button, Row } from 'antd'; -import React, { useState } from 'react'; +import { ReactNode, useState } from 'react'; import { QueryWrapper } from '../styles'; @@ -15,7 +15,7 @@ interface IQueryHeaderProps { onDisable: VoidFunction; name: string; onDelete: VoidFunction; - children: React.ReactNode; + children: ReactNode; } function QueryHeader({ diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx index 95b0fa8bfd..ac3d7b6d55 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx @@ -1,10 +1,9 @@ import { PlusOutlined } from '@ant-design/icons'; import { ClickHouseQueryTemplate } from 'constants/dashboard'; import GetQueryName from 'lib/query/GetQueryName'; -import React from 'react'; import { Query } from 'types/api/dashboard/getAll'; +import { EQueryType } from 'types/common/dashboard'; -import { WIDGET_CLICKHOUSE_QUERY_KEY_NAME } from '../../constants'; import { QueryButton } from '../../styles'; import { IHandleUpdatedQuery } from '../../types'; import ClickHouseQueryBuilder from './query'; @@ -13,7 +12,7 @@ import { IClickHouseQueryHandleChange } from './types'; interface IClickHouseQueryContainerProps { queryData: Query; updateQueryData: (args: IHandleUpdatedQuery) => void; - clickHouseQueries: Query['clickHouse']; + clickHouseQueries: Query['clickhouse_sql']; } function ClickHouseQueryContainer({ queryData, @@ -35,7 +34,7 @@ function ClickHouseQueryContainer({ // hence, this method is only applies when queryIndex is in number format. if (typeof queryIndex === 'number') { - const allQueries = queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]; + const allQueries = queryData[EQueryType.CLICKHOUSE]; const currentIndexQuery = allQueries[queryIndex]; @@ -57,8 +56,8 @@ function ClickHouseQueryContainer({ } }; const addQueryHandler = (): void => { - queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME].push({ - name: GetQueryName(queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]) || '', + queryData[EQueryType.CLICKHOUSE].push({ + name: GetQueryName(queryData[EQueryType.CLICKHOUSE]) || '', ...ClickHouseQueryTemplate, }); updateQueryData({ updatedQuery: { ...queryData } }); diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx index 389b7e15c4..539a72e3a2 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx @@ -1,6 +1,5 @@ import { Input } from 'antd'; import MonacoEditor from 'components/Editor'; -import React from 'react'; import { IClickHouseQuery } from 'types/api/dashboard/getAll'; import QueryHeader from '../QueryHeader'; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/index.tsx index 55adbd740b..dde28d8af7 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/index.tsx @@ -1,10 +1,9 @@ import { PlusOutlined } from '@ant-design/icons'; import { PromQLQueryTemplate } from 'constants/dashboard'; import GetQueryName from 'lib/query/GetQueryName'; -import React from 'react'; import { IPromQLQuery, Query } from 'types/api/dashboard/getAll'; +import { EQueryType } from 'types/common/dashboard'; -import { WIDGET_PROMQL_QUERY_KEY_NAME } from '../../constants'; import { QueryButton } from '../../styles'; import { IHandleUpdatedQuery } from '../../types'; import PromQLQueryBuilder from './query'; @@ -28,7 +27,7 @@ function PromQLQueryContainer({ toggleDisable, toggleDelete, }: IPromQLQueryHandleChange): void => { - const allQueries = queryData[WIDGET_PROMQL_QUERY_KEY_NAME]; + const allQueries = queryData[EQueryType.PROM]; const currentIndexQuery = allQueries[queryIndex as number]; if (query !== undefined) currentIndexQuery.query = query; if (legend !== undefined) currentIndexQuery.legend = legend; @@ -42,8 +41,8 @@ function PromQLQueryContainer({ updateQueryData({ updatedQuery: { ...queryData } }); }; const addQueryHandler = (): void => { - queryData[WIDGET_PROMQL_QUERY_KEY_NAME].push({ - name: GetQueryName(queryData[WIDGET_PROMQL_QUERY_KEY_NAME]) || '', + queryData[EQueryType.PROM].push({ + name: GetQueryName(queryData[EQueryType.PROM]) || '', ...PromQLQueryTemplate, }); updateQueryData({ updatedQuery: { ...queryData } }); diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query.tsx index 6cffd55d8d..3781fc71de 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query.tsx @@ -1,5 +1,4 @@ import { Input } from 'antd'; -import React from 'react'; import { IPromQLQuery } from 'types/api/dashboard/getAll'; import QueryHeader from '../QueryHeader'; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.ts b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.ts deleted file mode 100644 index f082f4091f..0000000000 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createMachine } from 'xstate'; - -export const ResourceAttributesFilterMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */ - createMachine({ - tsTypes: {} as import('./MetricTagKey.machine.typegen').Typegen0, - initial: 'Idle', - states: { - TagKey: { - on: { - NEXT: { - actions: 'onSelectOperator', - target: 'Operator', - }, - onBlur: { - actions: 'onBlurPurge', - target: 'Idle', - }, - RESET: { - target: 'Idle', - }, - }, - }, - Operator: { - on: { - NEXT: { - actions: 'onSelectTagValue', - target: 'TagValue', - }, - // onBlur: { - // actions: 'onBlurPurge', - // target: 'Idle', - // }, - RESET: { - target: 'Idle', - }, - }, - }, - TagValue: { - on: { - onBlur: { - actions: ['onValidateQuery'], - // target: 'Idle', - }, - RESET: { - target: 'Idle', - }, - }, - }, - Idle: { - on: { - NEXT: { - actions: 'onSelectTagKey', - description: 'Select Category', - target: 'TagKey', - }, - }, - }, - }, - id: 'Dashboard Search And Filter', - }); diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.typegen.ts b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.typegen.ts deleted file mode 100644 index f2019aaa26..0000000000 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/MetricTagKey.machine.typegen.ts +++ /dev/null @@ -1,32 +0,0 @@ -// This file was automatically generated. Edits will be overwritten - -export interface Typegen0 { - '@@xstate/typegen': true; - internalEvents: { - 'xstate.init': { type: 'xstate.init' }; - }; - invokeSrcNameMap: {}; - missingImplementations: { - actions: - | 'onBlurPurge' - | 'onSelectOperator' - | 'onSelectTagKey' - | 'onSelectTagValue' - | 'onValidateQuery'; - delays: never; - guards: never; - services: never; - }; - eventsCausingActions: { - onBlurPurge: 'onBlur'; - onSelectOperator: 'NEXT'; - onSelectTagKey: 'NEXT'; - onSelectTagValue: 'NEXT'; - onValidateQuery: 'onBlur'; - }; - eventsCausingDelays: {}; - eventsCausingGuards: {}; - eventsCausingServices: {}; - matchesStates: 'Idle' | 'Operator' | 'TagKey' | 'TagValue'; - tags: never; -} diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/QueryChip.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/QueryChip.tsx deleted file mode 100644 index e992536b74..0000000000 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/QueryChip.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; - -import { QueryChipContainer, QueryChipItem } from './styles'; -import { ITagKeyValueQuery } from './types'; - -interface IQueryChipProps { - queryData: ITagKeyValueQuery; - onClose: (id: string) => void; - disabled?: boolean; -} - -export default function QueryChip({ - queryData, - onClose, - disabled, -}: IQueryChipProps): JSX.Element { - return ( - - {queryData.key} - {queryData.op} - { - if (!disabled) onClose(queryData.id); - }} - > - {queryData.value.join(', ')} - - - ); -} -QueryChip.defaultProps = { - disabled: false, -}; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/index.tsx deleted file mode 100644 index d25c4e73dc..0000000000 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/MetricTagKeyFilter/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { CloseCircleFilled } from '@ant-design/icons'; -import { useMachine } from '@xstate/react'; -import { Button, Select, Spin } from 'antd'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import { map } from 'lodash-es'; -import React, { useCallback, useEffect, useState } from 'react'; -import { IMetricsBuilderQuery } from 'types/api/dashboard/getAll'; -import { v4 as uuid } from 'uuid'; - -import { ResourceAttributesFilterMachine } from './MetricTagKey.machine'; -import QueryChip from './QueryChip'; -import { QueryChipItem, SearchContainer } from './styles'; -import { IOption, ITagKeyValueQuery } from './types'; -import { - createQuery, - GetTagKeys, - GetTagValues, - OperatorSchema, - SingleValueOperators, -} from './utils'; - -interface IMetricTagKeyFilterProps { - metricName: IMetricsBuilderQuery['metricName']; - onSetQuery: (args: IMetricsBuilderQuery['tagFilters']['items']) => void; - selectedTagFilters: IMetricsBuilderQuery['tagFilters']['items']; -} - -function MetricTagKeyFilter({ - metricName, - onSetQuery, - selectedTagFilters: selectedTagQueries, -}: IMetricTagKeyFilterProps): JSX.Element | null { - const isDarkMode = useIsDarkMode(); - const [loading, setLoading] = useState(true); - const [selectedValues, setSelectedValues] = useState([]); - const [staging, setStaging] = useState([]); - const [queries, setQueries] = useState([]); - const [optionsData, setOptionsData] = useState<{ - mode: undefined | 'tags' | 'multiple'; - options: IOption[]; - }>({ - mode: undefined, - options: [], - }); - - const dispatchQueries = ( - updatedQueries: IMetricsBuilderQuery['tagFilters']['items'], - ): void => { - onSetQuery(updatedQueries); - setQueries(updatedQueries); - }; - const handleLoading = (isLoading: boolean): void => { - setLoading(isLoading); - if (isLoading) { - setOptionsData({ mode: undefined, options: [] }); - } - }; - const [state, send] = useMachine(ResourceAttributesFilterMachine, { - actions: { - onSelectTagKey: () => { - handleLoading(true); - GetTagKeys(metricName || '') - .then((tagKeys) => setOptionsData({ options: tagKeys, mode: undefined })) - .finally(() => { - handleLoading(false); - }); - }, - onSelectOperator: () => { - setOptionsData({ options: OperatorSchema, mode: undefined }); - }, - onSelectTagValue: () => { - handleLoading(true); - - GetTagValues(staging[0], metricName || '') - .then((tagValuesOptions) => - setOptionsData({ options: tagValuesOptions, mode: 'tags' }), - ) - .finally(() => { - handleLoading(false); - }); - }, - onBlurPurge: () => { - setSelectedValues([]); - setStaging([]); - }, - onValidateQuery: (): void => { - if (staging.length < 2 || selectedValues.length === 0) { - return; - } - - const generatedQuery = createQuery([...staging, selectedValues]); - - if (generatedQuery) { - dispatchQueries([...queries, generatedQuery]); - setSelectedValues([]); - setStaging([]); - send('RESET'); - } - }, - }, - }); - - useEffect(() => { - setQueries(selectedTagQueries); - }, [selectedTagQueries]); - - const handleFocus = (): void => { - if (state.value === 'Idle') { - send('NEXT'); - } - }; - - const handleBlur = useCallback((): void => { - send('onBlur'); - }, [send]); - - useEffect(() => { - handleBlur(); - }, [handleBlur, metricName]); - - const handleChange = (value: never | string[]): void => { - if (!optionsData.mode) { - setStaging((prevStaging) => [...prevStaging, String(value)]); - setSelectedValues([]); - send('NEXT'); - return; - } - if ( - state.value === 'TagValue' && - SingleValueOperators.includes(staging[staging.length - 1]) && - Array.isArray(value) - ) { - setSelectedValues([value[value.length - 1]]); - return; - } - - setSelectedValues([...value]); - }; - - const handleClose = (id: string): void => { - dispatchQueries(queries.filter((queryData) => queryData.id !== id)); - }; - - const handleClearAll = (): void => { - send('RESET'); - dispatchQueries([]); - setStaging([]); - setSelectedValues([]); - }; - - return ( - -
- {queries.length > 0 && - map( - queries, - (query): JSX.Element => ( - - ), - )} -
-
- {map(staging, (item) => ( - {item} - ))} -
- -
-