Merge pull request #2955 from SigNoz/release/v0.21.0

Release/v0.21.0
This commit is contained in:
Ankit Nayan 2023-06-22 00:56:11 +05:30 committed by GitHub
commit 2ee7817685
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
158 changed files with 4290 additions and 1406 deletions

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install dependencies - name: Install dependencies
run: cd frontend && yarn install run: cd frontend && yarn install
- name: Run ESLint - name: Run ESLint
@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Run tests - name: Run tests
shell: bash shell: bash
run: | run: |
@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Build EE query-service image - name: Build EE query-service image
shell: bash shell: bash
run: | run: |

View File

@ -39,11 +39,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -54,7 +54,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -68,4 +68,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v2

View File

@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Codebase - name: Checkout Codebase
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
repository: signoz/gh-bot repository: signoz/gh-bot
- name: Use Node v16 - name: Use Node v16
uses: actions/setup-node@v2 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 16
- name: Setup Cache & Install Dependencies - name: Setup Cache & Install Dependencies

View File

@ -13,7 +13,7 @@ jobs:
DOCKER_TAG: pull-${{ github.event.number }} DOCKER_TAG: pull-${{ github.event.number }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Build query-service image - name: Build query-service image
env: env:
@ -69,12 +69,14 @@ jobs:
--restart='OnFailure' -i --rm --command -- curl -X POST -F \ --restart='OnFailure' -i --rm --command -- curl -X POST -F \
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm 'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
- name: Get short commit SHA and display tunnel URL - name: Get short commit SHA, display tunnel URL and IP Address of the worker node
id: get-subdomain id: get-subdomain
run: | run: |
subdomain="pr-$(git rev-parse --short HEAD)" subdomain="pr-$(git rev-parse --short HEAD)"
echo "URL for tunnelling: https://$subdomain.loca.lt" echo "URL for tunnelling: https://$subdomain.loca.lt"
echo "::set-output name=subdomain::$subdomain" echo "subdomain=$subdomain" >> $GITHUB_OUTPUT
worker_ip="$(curl -4 -s ipconfig.io/ip)"
echo "Worker node IP address: $worker_ip"
- name: Start tunnel - name: Start tunnel
env: env:

View File

@ -9,8 +9,8 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
node-version: "16.x" node-version: "16.x"
- name: Install dependencies - name: Install dependencies

View File

@ -14,6 +14,6 @@ jobs:
name: Ensure Pull Request has a linked issue. name: Ensure Pull Request has a linked issue.
steps: steps:
- name: Verify Linked Issue - name: Verify Linked Issue
uses: srikanthccv/verify-linked-issue-action@v0.70 uses: srikanthccv/verify-linked-issue-action@v0.71
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -14,19 +14,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
with: with:
version: latest version: latest
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: benjlevesque/short-sha@v1.2 - uses: benjlevesque/short-sha@v2.2
id: short-sha id: short-sha
- name: Get branch name - name: Get branch name
id: branch-name id: branch-name
@ -49,19 +49,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
with: with:
version: latest version: latest
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: benjlevesque/short-sha@v1.2 - uses: benjlevesque/short-sha@v2.2
id: short-sha id: short-sha
- name: Get branch name - name: Get branch name
id: branch-name id: branch-name
@ -84,7 +84,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install dependencies - name: Install dependencies
working-directory: frontend working-directory: frontend
run: yarn install run: yarn install
@ -97,15 +97,15 @@ jobs:
run: npm run lint run: npm run lint
continue-on-error: true continue-on-error: true
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
with: with:
version: latest version: latest
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: benjlevesque/short-sha@v1.2 - uses: benjlevesque/short-sha@v2.2
id: short-sha id: short-sha
- name: Get branch name - name: Get branch name
id: branch-name id: branch-name

View File

@ -12,6 +12,12 @@ on:
jobs: jobs:
update_release_draft: update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# (Optional) GitHub Enterprise requires GHE_HOST variable set # (Optional) GitHub Enterprise requires GHE_HOST variable set

View File

@ -8,9 +8,15 @@ jobs:
remove: remove:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Remove label - name: Remove label ok-to-test from PR
uses: buildsville/add-remove-label@v1 uses: buildsville/add-remove-label@v2.0.0
with: with:
label: ok-to-test,testing-deploy label: ok-to-test
type: remove
token: ${{ secrets.GITHUB_TOKEN }}
- name: Remove label testing-deploy from PR
uses: buildsville/add-remove-label@v2.0.0
with:
label: testing-deploy
type: remove type: remove
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -3,7 +3,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- v* - develop
paths: paths:
- 'frontend/**' - 'frontend/**'
defaults: defaults:
@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Sonar analysis - name: Sonar analysis

View File

@ -54,7 +54,7 @@ build-push-frontend:
@echo "--> Building and pushing frontend docker image" @echo "--> Building and pushing frontend docker image"
@echo "------------------" @echo "------------------"
@cd $(FRONTEND_DIRECTORY) && \ @cd $(FRONTEND_DIRECTORY) && \
docker buildx build --file Dockerfile --progress plane --push --platform linux/arm64,linux/amd64 \ docker buildx build --file Dockerfile --progress plain --push --platform linux/arm64,linux/amd64 \
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) . --tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
# Steps to build and push docker image of query service # Steps to build and push docker image of query service
@ -73,7 +73,7 @@ build-push-query-service:
@echo "------------------" @echo "------------------"
@echo "--> Building and pushing query-service docker image" @echo "--> Building and pushing query-service docker image"
@echo "------------------" @echo "------------------"
@docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plane \ @docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plain \
--push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS="$(LD_FLAGS)" \ --push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS="$(LD_FLAGS)" \
--tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) . --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) .
@ -98,7 +98,7 @@ build-push-ee-query-service:
@echo "--> Building and pushing query-service docker image" @echo "--> Building and pushing query-service docker image"
@echo "------------------" @echo "------------------"
@docker buildx build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \ @docker buildx build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \
--progress plane --push --platform linux/arm64,linux/amd64 \ --progress plain --push --platform linux/arm64,linux/amd64 \
--build-arg LD_FLAGS="$(LD_FLAGS)" --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) . --build-arg LD_FLAGS="$(LD_FLAGS)" --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) .
dev-setup: dev-setup:
@ -136,9 +136,18 @@ clear-swarm-data:
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \ @docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*" sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*"
clear-standalone-ch:
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*"
clear-swarm-ch:
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf clickhouse*/* zookeeper-*/*"
test: test:
go test ./pkg/query-service/app/metrics/... go test ./pkg/query-service/app/metrics/...
go test ./pkg/query-service/cache/... go test ./pkg/query-service/cache/...
go test ./pkg/query-service/app/... go test ./pkg/query-service/app/...
go test ./pkg/query-service/app/querier/...
go test ./pkg/query-service/converter/... go test ./pkg/query-service/converter/...
go test ./pkg/query-service/formatter/... go test ./pkg/query-service/formatter/...

View File

@ -137,7 +137,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.20.2 image: signoz/query-service:0.21.0
command: ["-config=/root/config/prometheus.yml"] command: ["-config=/root/config/prometheus.yml"]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend <<: *clickhouse-depend
frontend: frontend:
image: signoz/frontend:0.20.2 image: signoz/frontend:0.21.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.76.1 image: signoz/signoz-otel-collector:0.79.1
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
@ -208,7 +208,7 @@ services:
<<: *clickhouse-depend <<: *clickhouse-depend
otel-collector-metrics: otel-collector-metrics:
image: signoz/signoz-otel-collector:0.76.1 image: signoz/signoz-otel-collector:0.79.1
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -41,7 +41,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: otel-collector container_name: otel-collector
image: signoz/signoz-otel-collector:0.76.1 image: signoz/signoz-otel-collector:0.79.1
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
# user: root # required for reading docker container logs # user: root # required for reading docker container logs
volumes: volumes:
@ -67,7 +67,7 @@ services:
otel-collector-metrics: otel-collector-metrics:
container_name: otel-collector-metrics container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.76.1 image: signoz/signoz-otel-collector:0.79.1
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -153,7 +153,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.20.2} image: signoz/query-service:${DOCKER_TAG:-0.21.0}
container_name: query-service container_name: query-service
command: ["-config=/root/config/prometheus.yml"] command: ["-config=/root/config/prometheus.yml"]
# ports: # ports:
@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend <<: *clickhouse-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.20.2} image: signoz/frontend:${DOCKER_TAG:-0.21.0}
container_name: frontend container_name: frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -193,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1}
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
@ -219,7 +219,7 @@ services:
<<: *clickhouse-depend <<: *clickhouse-depend
otel-collector-metrics: otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1}
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
volumes: volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -277,7 +277,7 @@ func loggingMiddleware(next http.Handler) http.Handler {
path, _ := route.GetPathTemplate() path, _ := route.GetPathTemplate()
startTime := time.Now() startTime := time.Now()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
zap.S().Info(path, "\ttimeTaken: ", time.Now().Sub(startTime)) zap.L().Info(path+"\ttimeTaken:"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path))
}) })
} }
@ -289,7 +289,7 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler {
path, _ := route.GetPathTemplate() path, _ := route.GetPathTemplate()
startTime := time.Now() startTime := time.Now()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
zap.S().Info(path, "\tprivatePort: true", "\ttimeTaken: ", time.Now().Sub(startTime)) zap.L().Info(path+"\tprivatePort: true \ttimeTaken"+time.Now().Sub(startTime).String(), zap.Duration("timeTaken", time.Now().Sub(startTime)), zap.String("path", path), zap.Bool("tprivatePort", true))
}) })
} }

View File

@ -3,25 +3,73 @@ package main
import ( import (
"context" "context"
"flag" "flag"
"log"
"os" "os"
"os/signal" "os/signal"
"strconv"
"syscall" "syscall"
"time"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app" "go.signoz.io/signoz/ee/query-service/app"
"go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/constants"
baseconst "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/version" "go.signoz.io/signoz/pkg/query-service/version"
"google.golang.org/grpc"
zapotlpencoder "github.com/SigNoz/zap_otlp/zap_otlp_encoder"
zapotlpsync "github.com/SigNoz/zap_otlp/zap_otlp_sync"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
func initZapLog() *zap.Logger { func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
config := zap.NewDevelopmentConfig() config := zap.NewDevelopmentConfig()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
config.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
otlpEncoder := zapotlpencoder.NewOTLPEncoder(config.EncoderConfig)
consoleEncoder := zapcore.NewConsoleEncoder(config.EncoderConfig)
defaultLogLevel := zapcore.DebugLevel
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.EncoderConfig.TimeKey = "timestamp" config.EncoderConfig.TimeKey = "timestamp"
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
logger, _ := config.Build()
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("query-service"),
)
core := zapcore.NewTee(
zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel),
)
if enableQueryServiceLogOTLPExport == true {
conn, err := grpc.DialContext(ctx, constants.OTLPTarget, grpc.WithBlock(), grpc.WithInsecure(), grpc.WithTimeout(time.Second*30))
if err != nil {
log.Println("failed to connect to otlp collector to export query service logs with error:", err)
} else {
logExportBatchSizeInt, err := strconv.Atoi(baseconst.LogExportBatchSize)
if err != nil {
logExportBatchSizeInt = 1000
}
ws := zapcore.AddSync(zapotlpsync.NewOtlpSyncer(conn, zapotlpsync.Options{
BatchSize: logExportBatchSizeInt,
ResourceSchema: semconv.SchemaURL,
Resource: res,
}))
core = zapcore.NewTee(
zapcore.NewCore(consoleEncoder, os.Stdout, defaultLogLevel),
zapcore.NewCore(otlpEncoder, zapcore.NewMultiWriteSyncer(ws), defaultLogLevel),
)
}
}
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
return logger return logger
} }
@ -34,12 +82,15 @@ func main() {
// the url used to build link in the alert messages in slack and other systems // the url used to build link in the alert messages in slack and other systems
var ruleRepoURL string var ruleRepoURL string
var enableQueryServiceLogOTLPExport bool
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)") flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)") flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.Parse() flag.Parse()
loggerMgr := initZapLog() loggerMgr := initZapLog(enableQueryServiceLogOTLPExport)
zap.ReplaceGlobals(loggerMgr) zap.ReplaceGlobals(loggerMgr)
defer loggerMgr.Sync() // flushes buffer, if any defer loggerMgr.Sync() // flushes buffer, if any

56
frontend/CONTRIBUTIONS.md Normal file
View File

@ -0,0 +1,56 @@
# **Frontend Guidelines**
Embrace the spirit of collaboration and contribute to the success of our open-source project by adhering to these frontend development guidelines with precision and passion.
### React and Components
- Strive to create small and modular components, ensuring they are divided into individual pieces for improved maintainability and reusability.
- Avoid passing inline objects or functions as props to React components, as they are recreated with each render cycle.
Utilize careful memoization of functions and variables, balancing optimization efforts to prevent potential performance issues. [When to useMemo and useCallback](https://kentcdodds.com/blog/usememo-and-usecallback) by Kent C. Dodds is quite helpful for this scenario.
- Minimize the use of inline functions whenever possible to enhance code readability and improve overall comprehension.
- Employ the appropriate usage of useMemo and useCallback hooks for effective memoization of values and functions.
- Determine the appropriate placement of components:
- Pages should contain an aggregation of all components and containers.
- Commonly used components should reside in the 'components' directory.
- Parent components responsible for data manipulation should be placed in the 'container' directory.
- Strategically decide where to store data, either in global state or local components:
- Begin by storing data in local components and gradually transition to global state as necessary.
- Avoid importing default namespace `React` as the project is using `v18` and `import React from 'react'` is not needed anymore.
- When a function requires more than three arguments (except when memoized), encapsulate them within an object to enhance readability and reduce potential parameter complexity.
### API and Services
- Avoid incorporating business logic within API/Service files to maintain flexibility for consumers to handle it according to their specific needs.
- Employ the use of the useQuery hook for fetching data and the useMutation hook for updating data, ensuring a consistent and efficient approach.
- Utilize the useQueryClient hook when updating the cache, facilitating smooth and effective management of data within the application.
**Note -** In our project, we are utilizing React Query v3. To gain a comprehensive understanding of its features and implementation, we recommend referring to the [official documentation](https://tanstack.com/query/v3/docs/react/overview) as a valuable resource.
### Styling
- Refrain from using inline styling within React components to maintain separation of concerns and promote a more maintainable codebase.
- Opt for using the rem unit instead of px values to ensure better scalability and responsiveness across different devices and screen sizes.
### Linting and Setup
- It is crucial to refrain from disabling ESLint and TypeScript errors within the project. If there is a specific rule that needs to be disabled, provide a clear and justified explanation for doing so. Maintaining the integrity of the linting and type-checking processes ensures code quality and consistency throughout the codebase.
- In our project, we rely on several essential ESLint plugins, namely:
- [plugin:@typescript-eslint](https://typescript-eslint.io/rules/)
- [airbnb styleguide](https://github.com/airbnb/javascript)
- [plugin:sonarjs](https://github.com/SonarSource/eslint-plugin-sonarjs)
To ensure compliance with our coding standards and best practices, we encourage you to refer to the documentation of these plugins. Familiarizing yourself with the ESLint rules they provide will help maintain code quality and consistency throughout the project.
### Naming Conventions
- Ensure that component names are written in Capital Case, while the folder names should be in lowercase.
- Keep all other elements, such as variables, functions, and file names, in lowercase.
### Miscellaneous
- Ensure that functions are modularized and follow the Single Responsibility Principle (SRP). The function's name should accurately convey its purpose and functionality.
- Semantic division of functions into smaller units should be prioritized for improved readability and maintainability.
Aim to keep functions concise and avoid exceeding a maximum length of 40 lines to enhance code understandability and ease of maintenance.
- Eliminate the use of hard-coded strings or enums, favoring a more flexible and maintainable approach.
- Strive to internationalize all strings within the codebase to support localization and improve accessibility for users across different languages.
- Minimize the usage of multiple if statements or switch cases within a function. Consider creating a mapper and separating logic into multiple functions for better code organization.

View File

@ -0,0 +1,11 @@
{
"options_menu": {
"options": "Options",
"format": "Format",
"row": "Row",
"default": "Default",
"column": "Column",
"maxLines": "Max lines per Row",
"addColumn": "Add a column"
}
}

View File

@ -0,0 +1,11 @@
{
"options_menu": {
"options": "Options",
"format": "Format",
"row": "Row",
"default": "Default",
"column": "Column",
"maxLines": "Max lines per Row",
"addColumn": "Add a column"
}
}

View File

@ -15,6 +15,11 @@ export const ServiceMapPage = Loadable(
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'), () => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
); );
export const TracesExplorer = Loadable(
() =>
import(/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesExplorer'),
);
export const TraceFilter = Loadable( export const TraceFilter = Loadable(
() => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'), () => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'),
); );
@ -101,6 +106,10 @@ export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/Logs'), () => import(/* webpackChunkName: "Logs" */ 'pages/Logs'),
); );
export const LogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
);
export const Login = Loadable( export const Login = Loadable(
() => import(/* webpackChunkName: "Login" */ 'pages/Login'), () => import(/* webpackChunkName: "Login" */ 'pages/Login'),
); );

View File

@ -16,6 +16,7 @@ import {
ListAllALertsPage, ListAllALertsPage,
Login, Login,
Logs, Logs,
LogsExplorer,
MySettings, MySettings,
NewDashboardPage, NewDashboardPage,
OrganizationSettings, OrganizationSettings,
@ -29,6 +30,7 @@ import {
StatusPage, StatusPage,
TraceDetail, TraceDetail,
TraceFilter, TraceFilter,
TracesExplorer,
UnAuthorized, UnAuthorized,
UsageExplorerPage, UsageExplorerPage,
} from './pageComponents'; } from './pageComponents';
@ -139,6 +141,13 @@ const routes: AppRoutes[] = [
isPrivate: true, isPrivate: true,
key: 'TRACE', key: 'TRACE',
}, },
{
path: ROUTES.TRACES_EXPLORER,
exact: true,
component: TracesExplorer,
isPrivate: true,
key: 'TRACES_EXPLORER',
},
{ {
path: ROUTES.CHANNELS_NEW, path: ROUTES.CHANNELS_NEW,
exact: true, exact: true,
@ -209,6 +218,13 @@ const routes: AppRoutes[] = [
key: 'LOGS', key: 'LOGS',
isPrivate: true, isPrivate: true,
}, },
{
path: ROUTES.LOGS_EXPLORER,
exact: true,
component: LogsExplorer,
key: 'LOGS_EXPLORER',
isPrivate: true,
},
{ {
path: ROUTES.LOGIN, path: ROUTES.LOGIN,
exact: true, exact: true,

View File

@ -16,7 +16,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
return { return {
statusCode, statusCode,
payload: null, payload: null,
error: 'Not Found', error: data.errorType,
message: null, message: null,
}; };
} }

View File

@ -1,5 +1,6 @@
// ** Helpers // ** Helpers
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { import {
BaseAutocompleteData, BaseAutocompleteData,
@ -18,6 +19,7 @@ import { EQueryType } from 'types/common/dashboard';
import { import {
BoolOperators, BoolOperators,
DataSource, DataSource,
LogsAggregatorOperator,
MetricAggregateOperator, MetricAggregateOperator,
NumberOperators, NumberOperators,
PanelTypeKeys, PanelTypeKeys,
@ -25,6 +27,7 @@ import {
QueryBuilderData, QueryBuilderData,
ReduceOperators, ReduceOperators,
StringOperators, StringOperators,
TracesAggregatorOperator,
} from 'types/common/queryBuilder'; } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select'; import { SelectOption } from 'types/common/select';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -100,14 +103,17 @@ export const initialHavingValues: HavingForm = {
}; };
export const initialAutocompleteData: BaseAutocompleteData = { export const initialAutocompleteData: BaseAutocompleteData = {
id: uuid(), id: createIdFromObjectFields(
{ dataType: null, key: '', isColumn: null, type: null },
baseAutoCompleteIdKeysOrder,
),
dataType: null, dataType: null,
key: '', key: '',
isColumn: null, isColumn: null,
type: null, type: null,
}; };
export const initialQueryBuilderFormValues: IBuilderQuery = { const initialQueryBuilderFormValues: IBuilderQuery = {
dataSource: DataSource.METRICS, dataSource: DataSource.METRICS,
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MetricAggregateOperator.NOOP, aggregateOperator: MetricAggregateOperator.NOOP,
@ -127,6 +133,27 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
reduceTo: 'sum', reduceTo: 'sum',
}; };
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
...initialQueryBuilderFormValues,
aggregateOperator: LogsAggregatorOperator.COUNT,
dataSource: DataSource.LOGS,
};
const initialQueryBuilderFormTracesValues: IBuilderQuery = {
...initialQueryBuilderFormValues,
aggregateOperator: TracesAggregatorOperator.COUNT,
dataSource: DataSource.TRACES,
};
export const initialQueryBuilderFormValuesMap: Record<
DataSource,
IBuilderQuery
> = {
metrics: initialQueryBuilderFormValues,
logs: initialQueryBuilderFormLogsValues,
traces: initialQueryBuilderFormTracesValues,
};
export const initialFormulaBuilderFormValues: IBuilderFormula = { export const initialFormulaBuilderFormValues: IBuilderFormula = {
queryName: createNewBuilderItemName({ queryName: createNewBuilderItemName({
existNames: [], existNames: [],
@ -161,17 +188,39 @@ export const initialSingleQueryMap: Record<
IClickHouseQuery | IPromQLQuery IClickHouseQuery | IPromQLQuery
> = { clickhouse_sql: initialClickHouseData, promql: initialQueryPromQLData }; > = { clickhouse_sql: initialClickHouseData, promql: initialQueryPromQLData };
export const initialQuery: QueryState = { export const initialQueryState: QueryState = {
id: uuid(),
builder: initialQueryBuilderData, builder: initialQueryBuilderData,
clickhouse_sql: [initialClickHouseData], clickhouse_sql: [initialClickHouseData],
promql: [initialQueryPromQLData], promql: [initialQueryPromQLData],
}; };
export const initialQueryWithType: Query = { const initialQueryWithType: Query = {
...initialQuery, ...initialQueryState,
queryType: EQueryType.QUERY_BUILDER, queryType: EQueryType.QUERY_BUILDER,
}; };
const initialQueryLogsWithType: Query = {
...initialQueryWithType,
builder: {
...initialQueryWithType.builder,
queryData: [initialQueryBuilderFormValuesMap.logs],
},
};
const initialQueryTracesWithType: Query = {
...initialQueryWithType,
builder: {
...initialQueryWithType.builder,
queryData: [initialQueryBuilderFormValuesMap.traces],
},
};
export const initialQueriesMap: Record<DataSource, Query> = {
metrics: initialQueryWithType,
logs: initialQueryLogsWithType,
traces: initialQueryTracesWithType,
};
export const operatorsByTypes: Record<LocalDataType, string[]> = { export const operatorsByTypes: Record<LocalDataType, string[]> = {
string: Object.values(StringOperators), string: Object.values(StringOperators),
number: Object.values(NumberOperators), number: Object.values(NumberOperators),

View File

@ -1 +1,2 @@
export const COMPOSITE_QUERY = 'compositeQuery'; export const COMPOSITE_QUERY = 'compositeQuery';
export const PANEL_TYPES_QUERY = 'panelTypes';

View File

@ -1,3 +1,5 @@
export const REACT_QUERY_KEY = { export const REACT_QUERY_KEY = {
GET_ALL_LICENCES: 'GET_ALL_LICENCES', GET_ALL_LICENCES: 'GET_ALL_LICENCES',
GET_QUERY_RANGE: 'GET_QUERY_RANGE',
GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS',
}; };

View File

@ -5,6 +5,7 @@ const ROUTES = {
SERVICE_MAP: '/service-map', SERVICE_MAP: '/service-map',
TRACE: '/trace', TRACE: '/trace',
TRACE_DETAIL: '/trace/:id', TRACE_DETAIL: '/trace/:id',
TRACES_EXPLORER: '/traces-explorer',
SETTINGS: '/settings', SETTINGS: '/settings',
INSTRUMENTATION: '/get-started', INSTRUMENTATION: '/get-started',
USAGE_EXPLORER: '/usage-explorer', USAGE_EXPLORER: '/usage-explorer',
@ -27,6 +28,7 @@ const ROUTES = {
UN_AUTHORIZED: '/un-authorized', UN_AUTHORIZED: '/un-authorized',
NOT_FOUND: '/not-found', NOT_FOUND: '/not-found',
LOGS: '/logs', LOGS: '/logs',
LOGS_EXPLORER: '/logs-explorer',
HOME_PAGE: '/', HOME_PAGE: '/',
PASSWORD_RESET: '/password-reset', PASSWORD_RESET: '/password-reset',
LIST_LICENSES: '/licenses', LIST_LICENSES: '/licenses',

View File

@ -36,8 +36,11 @@ const themeColors = {
royalGrey: '#888888', royalGrey: '#888888',
matterhornGrey: '#555555', matterhornGrey: '#555555',
whiteCream: '#ffffffd5', whiteCream: '#ffffffd5',
white: '#ffffff',
black: '#000000', black: '#000000',
lightBlack: '#141414',
lightgrey: '#ddd', lightgrey: '#ddd',
lightWhite: '#ffffffd9',
borderLightGrey: '#d9d9d9', borderLightGrey: '#d9d9d9',
borderDarkGrey: '#424242', borderDarkGrey: '#424242',
}; };

View File

@ -0,0 +1,7 @@
import { CSSProperties } from 'react';
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
export const defaultSelectStyle: CSSProperties = {
minWidth: '6rem',
};

View File

@ -0,0 +1,69 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Select } from 'antd';
import { memo, useMemo } from 'react';
import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config';
import { Container } from './styles';
interface ControlsProps {
count: number;
countPerPage: number;
isLoading: boolean;
handleNavigatePrevious: () => void;
handleNavigateNext: () => void;
handleCountItemsPerPageChange: (e: number) => void;
}
function Controls(props: ControlsProps): JSX.Element | null {
const {
count,
isLoading,
countPerPage,
handleNavigatePrevious,
handleNavigateNext,
handleCountItemsPerPageChange,
} = props;
const isNextAndPreviousDisabled = useMemo(
() => isLoading || countPerPage === 0 || count === 0 || count < countPerPage,
[isLoading, countPerPage, count],
);
return (
<Container>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigatePrevious}
>
<LeftOutlined /> Previous
</Button>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigateNext}
>
Next <RightOutlined />
</Button>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={countPerPage}
onChange={handleCountItemsPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
</Container>
);
}
export default memo(Controls);

View File

@ -0,0 +1,7 @@
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
align-items: center;
gap: 0.5rem;
`;

View File

@ -1,5 +1,5 @@
import { import {
initialQueryBuilderFormValues, initialQueryBuilderFormValuesMap,
initialQueryPromQLData, initialQueryPromQLData,
PANEL_TYPES, PANEL_TYPES,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
@ -11,11 +11,6 @@ import {
defaultMatchType, defaultMatchType,
} from 'types/api/alerts/def'; } from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import {
DataSource,
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
const defaultAlertDescription = const defaultAlertDescription =
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})'; 'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})';
@ -32,7 +27,7 @@ export const alertDefaults: AlertDef = {
condition: { condition: {
compositeQuery: { compositeQuery: {
builderQueries: { builderQueries: {
A: initialQueryBuilderFormValues, A: initialQueryBuilderFormValuesMap.metrics,
}, },
promQueries: { A: initialQueryPromQLData }, promQueries: { A: initialQueryPromQLData },
chQueries: { chQueries: {
@ -61,11 +56,7 @@ export const logAlertDefaults: AlertDef = {
condition: { condition: {
compositeQuery: { compositeQuery: {
builderQueries: { builderQueries: {
A: { A: initialQueryBuilderFormValuesMap.logs,
...initialQueryBuilderFormValues,
aggregateOperator: LogsAggregatorOperator.COUNT,
dataSource: DataSource.LOGS,
},
}, },
promQueries: { A: initialQueryPromQLData }, promQueries: { A: initialQueryPromQLData },
chQueries: { chQueries: {
@ -95,11 +86,7 @@ export const traceAlertDefaults: AlertDef = {
condition: { condition: {
compositeQuery: { compositeQuery: {
builderQueries: { builderQueries: {
A: { A: initialQueryBuilderFormValuesMap.traces,
...initialQueryBuilderFormValues,
aggregateOperator: TracesAggregatorOperator.COUNT,
dataSource: DataSource.TRACES,
},
}, },
promQueries: { A: initialQueryPromQLData }, promQueries: { A: initialQueryPromQLData },
chQueries: { chQueries: {
@ -129,11 +116,7 @@ export const exceptionAlertDefaults: AlertDef = {
condition: { condition: {
compositeQuery: { compositeQuery: {
builderQueries: { builderQueries: {
A: { A: initialQueryBuilderFormValuesMap.traces,
...initialQueryBuilderFormValues,
aggregateOperator: TracesAggregatorOperator.COUNT,
dataSource: DataSource.TRACES,
},
}, },
promQueries: { A: initialQueryPromQLData }, promQueries: { A: initialQueryPromQLData },
chQueries: { chQueries: {

View File

@ -1,16 +1,11 @@
import { Form, Row } from 'antd'; import { Form, Row } from 'antd';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
import FormAlertRules from 'container/FormAlertRules'; import FormAlertRules from 'container/FormAlertRules';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useState } from 'react'; import { useState } from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def'; import { AlertDef } from 'types/api/alerts/def';
import { import {
alertDefaults, alertDefaults,
ALERTS_VALUES_MAP,
exceptionAlertDefaults, exceptionAlertDefaults,
logAlertDefaults, logAlertDefaults,
traceAlertDefaults, traceAlertDefaults,
@ -18,18 +13,12 @@ import {
import SelectAlertType from './SelectAlertType'; import SelectAlertType from './SelectAlertType';
function CreateRules(): JSX.Element { function CreateRules(): JSX.Element {
const [initValues, setInitValues] = useState<AlertDef>(alertDefaults); const [initValues, setInitValues] = useState<AlertDef | null>(null);
const [alertType, setAlertType] = useState<AlertTypes>( const [alertType, setAlertType] = useState<AlertTypes>(
AlertTypes.METRICS_BASED_ALERT, AlertTypes.METRICS_BASED_ALERT,
); );
const [formInstance] = Form.useForm(); const [formInstance] = Form.useForm();
const urlQuery = useUrlQuery();
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
const { redirectWithQueryBuilderData } = useQueryBuilder();
const onSelectType = (typ: AlertTypes): void => { const onSelectType = (typ: AlertTypes): void => {
setAlertType(typ); setAlertType(typ);
switch (typ) { switch (typ) {
@ -45,15 +34,9 @@ function CreateRules(): JSX.Element {
default: default:
setInitValues(alertDefaults); setInitValues(alertDefaults);
} }
const value = ALERTS_VALUES_MAP[typ].condition.compositeQuery;
const compositeQuery = mapQueryDataFromApi(value);
redirectWithQueryBuilderData(compositeQuery);
}; };
if (!compositeQuery) { if (!initValues) {
return ( return (
<Row wrap={false}> <Row wrap={false}>
<SelectAlertType onSelect={onSelectType} /> <SelectAlertType onSelect={onSelectType} />

View File

@ -0,0 +1,113 @@
import { Button, Typography } from 'antd';
import createDashboard from 'api/dashboard/create';
import getAll from 'api/dashboard/getAll';
import axios from 'axios';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useNotifications } from 'hooks/useNotifications';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { ExportPanelProps } from '.';
import {
DashboardSelect,
NewDashboardButton,
SelectWrapper,
Title,
Wrapper,
} from './styles';
import { getSelectOptions } from './utils';
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
const { notifications } = useNotifications();
const { t } = useTranslation(['dashboard']);
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
null,
);
const { data, isLoading, refetch } = useQuery({
queryFn: getAll,
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
});
const {
mutate: createNewDashboard,
isLoading: createDashboardLoading,
} = useMutation(createDashboard, {
onSuccess: () => {
refetch();
},
onError: (error) => {
if (axios.isAxiosError(error)) {
notifications.error({
message: error.message,
});
}
},
});
const options = useMemo(() => getSelectOptions(data?.payload || []), [data]);
const handleExportClick = useCallback((): void => {
const currentSelectedDashboard = data?.payload?.find(
({ uuid }) => uuid === selectedDashboardId,
);
onExport(currentSelectedDashboard || null);
}, [data, selectedDashboardId, onExport]);
const handleSelect = useCallback(
(selectedDashboardValue: string): void => {
setSelectedDashboardId(selectedDashboardValue);
},
[setSelectedDashboardId],
);
const handleNewDashboard = useCallback(async () => {
createNewDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
});
}, [t, createNewDashboard]);
return (
<Wrapper direction="vertical">
<Title>Export Panel</Title>
<SelectWrapper direction="horizontal">
<DashboardSelect
placeholder="Select Dashboard"
options={options}
loading={isLoading || createDashboardLoading}
disabled={isLoading || createDashboardLoading}
value={selectedDashboardId}
onSelect={handleSelect}
/>
<Button
type="primary"
disabled={isLoading || !options?.length || !selectedDashboardId}
onClick={handleExportClick}
>
Export
</Button>
</SelectWrapper>
<Typography>
Or create dashboard with this panel -
<NewDashboardButton
disabled={createDashboardLoading}
loading={createDashboardLoading}
type="link"
onClick={handleNewDashboard}
>
New Dashboard
</NewDashboardButton>
</Typography>
</Wrapper>
);
}
export default ExportPanel;

View File

@ -0,0 +1,9 @@
export const MENU_KEY = {
EXPORT: 'export',
CREATE_ALERTS: 'create-alerts',
};
export const MENU_LABEL = {
EXPORT: 'Export Panel',
CREATE_ALERTS: 'Create Alerts',
};

View File

@ -0,0 +1,70 @@
import { Button, Dropdown, MenuProps, Modal } from 'antd';
import { useCallback, useMemo, useState } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll';
import { MENU_KEY, MENU_LABEL } from './config';
import ExportPanelContainer from './ExportPanel';
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
const [isExport, setIsExport] = useState<boolean>(false);
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
}, []);
const onMenuClickHandler: MenuProps['onClick'] = useCallback(
(e: OnClickProps) => {
if (e.key === MENU_KEY.EXPORT) {
onModalToggle(true);
}
},
[onModalToggle],
);
const menu: MenuProps = useMemo(
() => ({
items: [
{
key: MENU_KEY.EXPORT,
label: MENU_LABEL.EXPORT,
},
{
key: MENU_KEY.CREATE_ALERTS,
label: MENU_LABEL.CREATE_ALERTS,
},
],
onClick: onMenuClickHandler,
}),
[onMenuClickHandler],
);
const onCancel = (value: boolean) => (): void => {
onModalToggle(value);
};
return (
<>
<Dropdown trigger={['click']} menu={menu}>
<Button>Actions</Button>
</Dropdown>
<Modal
onOk={onCancel(false)}
onCancel={onCancel(false)}
open={isExport}
centered
>
<ExportPanelContainer onExport={onExport} />
</Modal>
</>
);
}
interface OnClickProps {
key: string;
}
export interface ExportPanelProps {
onExport: (dashboard: Dashboard | null) => void;
}
export default ExportPanel;

View File

@ -0,0 +1,33 @@
import { Button, Select, SelectProps, Space, Typography } from 'antd';
import { FunctionComponent } from 'react';
import styled from 'styled-components';
export const DashboardSelect: FunctionComponent<SelectProps> = styled(
Select,
)<SelectProps>`
width: 100%;
`;
export const SelectWrapper = styled(Space)`
width: 100%;
margin-bottom: 1rem;
.ant-space-item:first-child {
width: 100%;
max-width: 20rem;
}
`;
export const Wrapper = styled(Space)`
width: 100%;
`;
export const NewDashboardButton = styled(Button)`
&&& {
padding: 0 0.125rem;
}
`;
export const Title = styled(Typography.Text)`
font-size: 1rem;
`;

View File

@ -0,0 +1,10 @@
import { SelectProps } from 'antd';
import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll';
export const getSelectOptions = (
data: AllDashboardsData,
): SelectProps['options'] =>
data.map(({ uuid, data }) => ({
label: data.title,
value: uuid,
}));

View File

@ -1,6 +1,7 @@
import { Form, Select } from 'antd'; import { Form, Select } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AlertDef, Labels } from 'types/api/alerts/def'; import { AlertDef, Labels } from 'types/api/alerts/def';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import ChannelSelect from './ChannelSelect'; import ChannelSelect from './ChannelSelect';
import LabelSelect from './labels'; import LabelSelect from './labels';
@ -54,7 +55,15 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
</SeveritySelect> </SeveritySelect>
</Form.Item> </Form.Item>
<Form.Item label={t('field_alert_name')} labelAlign="left" name="alert"> <Form.Item
required
name="alert"
labelAlign="left"
label={t('field_alert_name')}
rules={[
{ required: true, message: requireErrorMessage(t('field_alert_name')) },
]}
>
<InputSmall <InputSmall
onChange={(e): void => { onChange={(e): void => {
setAlertDef({ setAlertDef({
@ -97,10 +106,10 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
<FormItemMedium label="Notification Channels"> <FormItemMedium label="Notification Channels">
<ChannelSelect <ChannelSelect
currentValue={alertDef.preferredChannels} currentValue={alertDef.preferredChannels}
onSelectChannels={(s: string[]): void => { onSelectChannels={(preferredChannels): void => {
setAlertDef({ setAlertDef({
...alertDef, ...alertDef,
preferredChannels: s, preferredChannels,
}); });
}} }}
/> />

View File

@ -1,16 +1,15 @@
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { StaticLineProps } from 'components/Graph'; import { StaticLineProps } from 'components/Graph';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import GridGraphComponent from 'container/GridGraphComponent'; import GridGraphComponent from 'container/GridGraphComponent';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config'; import { Time } from 'container/TopNav/DateTimeSelection/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import getChartData from 'lib/getChartData'; import getChartData from 'lib/getChartData';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
@ -18,7 +17,7 @@ import { ChartContainer, FailedMessageContainer } from './styles';
export interface ChartPreviewProps { export interface ChartPreviewProps {
name: string; name: string;
query: Query | undefined; query: Query | null;
graphType?: GRAPH_TYPES; graphType?: GRAPH_TYPES;
selectedTime?: timePreferenceType; selectedTime?: timePreferenceType;
selectedInterval?: Time; selectedInterval?: Time;
@ -26,9 +25,6 @@ export interface ChartPreviewProps {
threshold?: number | undefined; threshold?: number | undefined;
userQueryKey?: string; userQueryKey?: string;
} }
interface QueryResponseError {
message?: string;
}
function ChartPreview({ function ChartPreview({
name, name,
@ -76,39 +72,30 @@ function ChartPreview({
} }
}, [query]); }, [query]);
const queryResponse = useQuery({ const queryResponse = useGetQueryRange(
{
query: query || initialQueriesMap.metrics,
globalSelectedInterval: selectedInterval,
graphType,
selectedTime,
},
{
queryKey: [ queryKey: [
'chartPreview', 'chartPreview',
userQueryKey || JSON.stringify(query), userQueryKey || JSON.stringify(query),
selectedInterval, selectedInterval,
], ],
queryFn: () =>
GetMetricQueryRange({
query: query || {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: {
queryFormulas: [],
queryData: [],
},
clickhouse_sql: [],
},
globalSelectedInterval: selectedInterval,
graphType,
selectedTime,
}),
retry: false, retry: false,
enabled: canQuery, enabled: canQuery,
}); },
);
const chartDataSet = queryResponse.isError const chartDataSet = queryResponse.isError
? null ? null
: getChartData({ : getChartData({
queryData: [ queryData: [
{ {
queryData: queryResponse?.data?.payload?.data?.result queryData: queryResponse?.data?.payload?.data?.result ?? [],
? queryResponse?.data?.payload?.data?.result
: [],
}, },
], ],
}); });
@ -119,11 +106,12 @@ function ChartPreview({
{(queryResponse?.isError || queryResponse?.error) && ( {(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart"> <FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '} <InfoCircleOutlined />{' '}
{(queryResponse?.error as QueryResponseError).message || {queryResponse.error.message || t('preview_chart_unexpected_error')}
t('preview_chart_unexpected_error')}
</FailedMessageContainer> </FailedMessageContainer>
)} )}
{queryResponse.isLoading && <Spinner size="large" tip="Loading..." />} {queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="70vh" />
)}
{chartDataSet && !queryResponse.isError && ( {chartDataSet && !queryResponse.isError && (
<GridGraphComponent <GridGraphComponent
title={name} title={name}

View File

@ -4,8 +4,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertTypes } from 'types/api/alerts/alertTypes';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
import ChQuerySection from './ChQuerySection'; import ChQuerySection from './ChQuerySection';
import PromqlSection from './PromqlSection'; import PromqlSection from './PromqlSection';
@ -20,8 +23,14 @@ function QuerySection({
// init namespace for translations // init namespace for translations
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const handleQueryCategoryChange = (queryType: string): void => { const handleQueryCategoryChange = (queryType: string): void => {
featureResponse.refetch().then(() => {
setQueryCategory(queryType as EQueryType); setQueryCategory(queryType as EQueryType);
});
}; };
const renderPromqlUI = (): JSX.Element => <PromqlSection />; const renderPromqlUI = (): JSX.Element => <PromqlSection />;
@ -38,10 +47,6 @@ function QuerySection({
/> />
); );
const handleRunQuery = (): void => {
runQuery();
};
const tabs = [ const tabs = [
{ {
label: t('tab_qb'), label: t('tab_qb'),
@ -76,7 +81,7 @@ function QuerySection({
onChange={handleQueryCategoryChange} onChange={handleQueryCategoryChange}
tabBarExtraContent={ tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> <span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button type="primary" onClick={handleRunQuery}> <Button type="primary" onClick={runQuery}>
Run Query Run Query
</Button> </Button>
</span> </span>
@ -95,7 +100,7 @@ function QuerySection({
onChange={handleQueryCategoryChange} onChange={handleQueryCategoryChange}
tabBarExtraContent={ tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> <span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<Button type="primary" onClick={handleRunQuery}> <Button type="primary" onClick={runQuery}>
Run Query Run Query
</Button> </Button>
</span> </span>
@ -132,7 +137,7 @@ interface QuerySectionProps {
queryCategory: EQueryType; queryCategory: EQueryType;
setQueryCategory: (n: EQueryType) => void; setQueryCategory: (n: EQueryType) => void;
alertType: AlertTypes; alertType: AlertTypes;
runQuery: () => void; runQuery: VoidFunction;
} }
export default QuerySection; export default QuerySection;

View File

@ -140,12 +140,14 @@ function RuleOptions({
{queryCategory === EQueryType.PROM {queryCategory === EQueryType.PROM
? renderPromRuleOptions() ? renderPromRuleOptions()
: renderThresholdRuleOpts()} : renderThresholdRuleOpts()}
<Form.Item name={['condition', 'target']}>
<InputNumber <InputNumber
addonBefore={t('field_threshold')} addonBefore={t('field_threshold')}
value={alertDef?.condition?.target} value={alertDef?.condition?.target}
onChange={onChange} onChange={onChange}
type="number" type="number"
/> />
</Form.Item>
</FormContainer> </FormContainer>
</> </>
); );

View File

@ -48,7 +48,12 @@ function FormAlertRules({
// init namespace for translations // init namespace for translations
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); const {
currentQuery,
stagedQuery,
handleRunQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
// use query client // use query client
const ruleCache = useQueryClient(); const ruleCache = useQueryClient();
@ -65,35 +70,14 @@ function FormAlertRules({
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]); const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
// manualStagedQuery requires manual staging of query useShareBuilderUrl({ defaultValue: sq });
// when user clicks run query button. Useful for clickhouse tab where
// run query button is provided.
const [manualStagedQuery, setManualStagedQuery] = useState<Query>();
// this use effect initiates staged query and
// other queries based on server data.
// useful when fetching of initial values (from api)
// is delayed
const { compositeQuery } = useShareBuilderUrl({ defaultValue: sq });
useEffect(() => { useEffect(() => {
if (compositeQuery && !manualStagedQuery) {
setManualStagedQuery(compositeQuery);
}
setAlertDef(initialValue); setAlertDef(initialValue);
}, [ }, [initialValue]);
initialValue,
initQuery,
redirectWithQueryBuilderData,
currentQuery,
manualStagedQuery,
compositeQuery,
]);
const onRunQuery = (): void => { const onRunQuery = (): void => {
setManualStagedQuery(currentQuery); handleRunQuery();
redirectWithQueryBuilderData(currentQuery);
}; };
const onCancelHandler = useCallback(() => { const onCancelHandler = useCallback(() => {
@ -115,8 +99,6 @@ function FormAlertRules({
} }
const query: Query = { ...currentQuery, queryType: val }; const query: Query = { ...currentQuery, queryType: val };
setManualStagedQuery(query);
redirectWithQueryBuilderData(query); redirectWithQueryBuilderData(query);
}; };
const { notifications } = useNotifications(); const { notifications } = useNotifications();
@ -201,10 +183,6 @@ function FormAlertRules({
const isFormValid = useCallback((): boolean => { const isFormValid = useCallback((): boolean => {
if (!alertDef.alert || alertDef.alert === '') { if (!alertDef.alert || alertDef.alert === '') {
notifications.error({
message: 'Error',
description: t('alertname_required'),
});
return false; return false;
} }
@ -217,14 +195,7 @@ function FormAlertRules({
} }
return validateQBParams(); return validateQBParams();
}, [ }, [validateQBParams, validateChQueryParams, alertDef, validatePromParams]);
t,
validateQBParams,
validateChQueryParams,
alertDef,
validatePromParams,
notifications,
]);
const preparePostData = (): AlertDef => { const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = { const postableAlert: AlertDef = {
@ -328,9 +299,7 @@ function FormAlertRules({
title: t('confirm_save_title'), title: t('confirm_save_title'),
centered: true, centered: true,
content, content,
onOk() { onOk: saveRule,
saveRule();
},
}); });
}, [t, saveRule, currentQuery]); }, [t, saveRule, currentQuery]);
@ -381,7 +350,7 @@ function FormAlertRules({
headline={<PlotTag queryType={currentQuery.queryType} />} headline={<PlotTag queryType={currentQuery.queryType} />}
name="" name=""
threshold={alertDef.condition?.target} threshold={alertDef.condition?.target}
query={manualStagedQuery} query={stagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)} selectedInterval={toChartInterval(alertDef.evalWindow)}
/> />
); );
@ -391,7 +360,7 @@ function FormAlertRules({
headline={<PlotTag queryType={currentQuery.queryType} />} headline={<PlotTag queryType={currentQuery.queryType} />}
name="Chart Preview" name="Chart Preview"
threshold={alertDef.condition?.target} threshold={alertDef.condition?.target}
query={manualStagedQuery} query={stagedQuery}
/> />
); );
@ -400,23 +369,25 @@ function FormAlertRules({
headline={<PlotTag queryType={currentQuery.queryType} />} headline={<PlotTag queryType={currentQuery.queryType} />}
name="Chart Preview" name="Chart Preview"
threshold={alertDef.condition?.target} threshold={alertDef.condition?.target}
query={manualStagedQuery} query={stagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)} selectedInterval={toChartInterval(alertDef.evalWindow)}
/> />
); );
const isNewRule = ruleId === 0; const isNewRule = ruleId === 0;
const isAlertNameMissing = !formInstance.getFieldValue('alert');
const isAlertAvialableToSave = const isAlertAvialableToSave =
isAlertAvialable && isAlertAvialable &&
isNewRule && currentQuery.queryType === EQueryType.QUERY_BUILDER &&
currentQuery.queryType === EQueryType.QUERY_BUILDER; alertType !== AlertTypes.METRICS_BASED_ALERT;
return ( return (
<> <>
{Element} {Element}
<PanelContainer> <PanelContainer>
<StyledLeftContainer flex="5 1 600px"> <StyledLeftContainer flex="5 1 600px" md={18}>
<MainFormContainer <MainFormContainer
initialValues={initialValue} initialValues={initialValue}
layout="vertical" layout="vertical"
@ -448,7 +419,7 @@ function FormAlertRules({
type="primary" type="primary"
onClick={onSaveHandler} onClick={onSaveHandler}
icon={<SaveOutlined />} icon={<SaveOutlined />}
disabled={isAlertAvialableToSave} disabled={isAlertNameMissing || isAlertAvialableToSave}
> >
{isNewRule ? t('button_createrule') : t('button_savechanges')} {isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton> </ActionButton>

View File

@ -84,8 +84,8 @@ function LabelSelect({
handleBlur(); handleBlur();
}, [handleBlur]); }, [handleBlur]);
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => { const handleLabelChange = (event: ChangeEvent<HTMLInputElement>): void => {
setCurrentVal(e.target?.value); setCurrentVal(event.target?.value.replace(':', ''));
}; };
const handleClose = (key: string): void => { const handleClose = (key: string): void => {
@ -133,9 +133,9 @@ function LabelSelect({
<div style={{ display: 'flex', width: '100%' }}> <div style={{ display: 'flex', width: '100%' }}>
<Input <Input
placeholder={renderPlaceholder()} placeholder={renderPlaceholder()}
onChange={handleChange} onChange={handleLabelChange}
onKeyUp={(e): void => { onKeyUp={(e): void => {
if (e.key === 'Enter' || e.code === 'Enter') { if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
send('NEXT'); send('NEXT');
} }
}} }}

View File

@ -7,16 +7,13 @@ import {
timeItems, timeItems,
timePreferance, timePreferance,
} from 'container/NewWidget/RightContainer/timeItems'; } from 'container/NewWidget/RightContainer/timeItems';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData'; import getChartData from 'lib/getChartData';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { TimeContainer } from './styles'; import { TimeContainer } from './styles';
@ -44,18 +41,24 @@ function FullView({
name: getSelectedTime()?.name || '', name: getSelectedTime()?.name || '',
enum: widget?.timePreferance || 'GLOBAL_TIME', enum: widget?.timePreferance || 'GLOBAL_TIME',
}); });
const response = useQuery<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse const queryKey = useMemo(
>(
`FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
() => () =>
GetMetricQueryRange({ `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
[selectedTime, globalSelectedTime, widget],
);
const response = useGetQueryRange(
{
selectedTime: selectedTime.enum, selectedTime: selectedTime.enum,
graphType: widget.panelTypes, graphType: widget.panelTypes,
query: widget.query, query: widget.query,
globalSelectedInterval: globalSelectedTime, globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(), variables: getDashboardVariables(),
}), },
{
queryKey,
},
); );
const chartDataSet = useMemo( const chartDataSet = useMemo(

View File

@ -3,6 +3,7 @@ import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent'; import GridGraphComponent from 'container/GridGraphComponent';
import { UpdateDashboard } from 'container/GridGraphLayout/utils'; import { UpdateDashboard } from 'container/GridGraphLayout/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import usePreviousValue from 'hooks/usePreviousValue'; import usePreviousValue from 'hooks/usePreviousValue';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
@ -20,7 +21,6 @@ import {
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { useQuery } from 'react-query';
import { connect, useSelector } from 'react-redux'; import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
@ -28,7 +28,6 @@ import {
DeleteWidget, DeleteWidget,
DeleteWidgetProps, DeleteWidgetProps,
} from 'store/actions/dashboard/deleteWidget'; } from 'store/actions/dashboard/deleteWidget';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
@ -55,7 +54,7 @@ function GridCardGraph({
const { ref: graphRef, inView: isGraphVisible } = useInView({ const { ref: graphRef, inView: isGraphVisible } = useInView({
threshold: 0, threshold: 0,
triggerOnce: true, triggerOnce: true,
initialInView: true, initialInView: false,
}); });
const { notifications } = useNotifications(); const { notifications } = useNotifications();
@ -81,33 +80,28 @@ function GridCardGraph({
const selectedData = selectedDashboard?.data; const selectedData = selectedDashboard?.data;
const { variables } = selectedData; const { variables } = selectedData;
const queryResponse = useQuery( const queryResponse = useGetQueryRange(
[
`GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget.id}`,
{ {
selectedTime: widget?.timePreferance,
graphType: widget?.panelTypes,
query: widget?.query,
globalSelectedInterval,
variables: getDashboardVariables(),
},
{
queryKey: [
`GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget?.id}`,
widget, widget,
maxTime, maxTime,
minTime, minTime,
globalSelectedInterval, globalSelectedInterval,
variables, variables,
},
], ],
() =>
GetMetricQueryRange({
selectedTime: widget?.timePreferance,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval,
variables: getDashboardVariables(),
}),
{
keepPreviousData: true, keepPreviousData: true,
enabled: isGraphVisible, enabled: isGraphVisible,
refetchOnMount: false, refetchOnMount: false,
onError: (error) => { onError: (error) => {
if (error instanceof Error) {
setErrorMessage(error.message); setErrorMessage(error.message);
}
}, },
}, },
); );
@ -179,7 +173,7 @@ function GridCardGraph({
{ {
data: selectedDashboard.data, data: selectedDashboard.data,
generateWidgetId: uuid, generateWidgetId: uuid,
graphType: widget.panelTypes, graphType: widget?.panelTypes,
selectedDashboard, selectedDashboard,
layout, layout,
widgetData: widget, widgetData: widget,
@ -193,7 +187,7 @@ function GridCardGraph({
setTimeout(() => { setTimeout(() => {
history.push( history.push(
`${history.location.pathname}/new?graphType=${widget.panelTypes}&widgetId=${uuid}`, `${history.location.pathname}/new?graphType=${widget?.panelTypes}&widgetId=${uuid}`,
); );
}, 1500); }, 1500);
}); });
@ -259,10 +253,10 @@ function GridCardGraph({
/> />
</div> </div>
<GridGraphComponent <GridGraphComponent
GRAPH_TYPES={widget.panelTypes} GRAPH_TYPES={widget?.panelTypes}
data={prevChartDataSetRef} data={prevChartDataSetRef}
isStacked={widget.isStacked} isStacked={widget?.isStacked}
opacity={widget.opacity} opacity={widget?.opacity}
title={' '} title={' '}
name={name} name={name}
yAxisUnit={yAxisUnit} yAxisUnit={yAxisUnit}

View File

@ -126,7 +126,7 @@ function WidgetHeader({
{ {
key: keyMethodMapping.clone.key, key: keyMethodMapping.clone.key,
icon: <CopyOutlined />, icon: <CopyOutlined />,
disabled: false, disabled: !editWidget,
label: 'Clone', label: 'Clone',
}, },
{ {

View File

@ -124,8 +124,7 @@ function GridGraph(props: Props): JSX.Element {
} }
} }
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, isAddWidget, layouts, selectedDashboard, widgets]);
}, []);
const { featureResponse } = useSelector<AppState, AppReducer>( const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app, (state) => state.app,

View File

@ -1,15 +1,10 @@
import { NotificationInstance } from 'antd/es/notification/interface'; import { NotificationInstance } from 'antd/es/notification/interface';
import updateDashboardApi from 'api/dashboard/update'; import updateDashboardApi from 'api/dashboard/update';
import { import { initialQueriesMap } from 'constants/queryBuilder';
initialClickHouseData,
initialQueryBuilderFormValues,
initialQueryPromQLData,
} from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import store from 'store'; import store from 'store';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
export const UpdateDashboard = async ( export const UpdateDashboard = async (
{ {
@ -41,23 +36,7 @@ export const UpdateDashboard = async (
nullZeroValues: widgetData?.nullZeroValues || '', nullZeroValues: widgetData?.nullZeroValues || '',
opacity: '', opacity: '',
panelTypes: graphType, panelTypes: graphType,
query: widgetData?.query || { query: widgetData?.query || initialQueriesMap.metrics,
queryType: EQueryType.QUERY_BUILDER,
promql: [initialQueryPromQLData],
clickhouse_sql: [initialClickHouseData],
builder: {
queryFormulas: [],
queryData: [initialQueryBuilderFormValues],
},
},
queryData: {
data: {
queryData: widgetData?.queryData.data.queryData || [],
},
error: false,
errorMessage: '',
loading: false,
},
timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME', timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME',
title: widgetData ? copyTitle : '', title: widgetData ? copyTitle : '',
}, },

View File

@ -91,7 +91,7 @@ function HeaderContainer(): JSX.Element {
const onClickSignozCloud = (): void => { const onClickSignozCloud = (): void => {
window.open( window.open(
'https://signoz.io/pricing/?utm_source=product_navbar&utm_medium=frontend', 'https://signoz.io/oss-to-cloud/?utm_source=product_navbar&utm_medium=frontend&utm_campaign=oss_users',
'_blank', '_blank',
); );
}; };

View File

@ -2,6 +2,7 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd'; import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import saveAlertApi from 'api/alerts/save';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
@ -67,7 +68,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
.catch(handleError); .catch(handleError);
}, [featureResponse, handleError]); }, [featureResponse, handleError]);
const onEditHandler = (record: GettableAlert): void => { const onEditHandler = (record: GettableAlert) => (): void => {
featureResponse featureResponse
.refetch() .refetch()
.then(() => { .then(() => {
@ -84,6 +85,44 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
.catch(handleError); .catch(handleError);
}; };
const onCloneHandler = (
originalAlert: GettableAlert,
) => async (): Promise<void> => {
const copyAlert = {
...originalAlert,
alert: originalAlert.alert.concat(' - Copy'),
};
const apiReq = { data: copyAlert };
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
notificationsApi.success({
message: 'Success',
description: 'Alert cloned successfully',
});
const { data: refetchData, status } = await refetch();
if (status === 'success' && refetchData.payload) {
setData(refetchData.payload || []);
setTimeout(() => {
const clonedAlert = refetchData.payload[refetchData.payload.length - 1];
history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${clonedAlert.id}`);
}, 2000);
}
if (status === 'error') {
notificationsApi.error({
message: t('something_went_wrong'),
});
}
} else {
notificationsApi.error({
message: 'Error',
description: response.error || t('something_went_wrong'),
});
}
};
const columns: ColumnsType<GettableAlert> = [ const columns: ColumnsType<GettableAlert> = [
{ {
title: 'Status', title: 'Status',
@ -107,9 +146,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return 0; return 0;
}, },
render: (value, record): JSX.Element => ( render: (value, record): JSX.Element => (
<Typography.Link onClick={(): void => onEditHandler(record)}> <Typography.Link onClick={onEditHandler(record)}>{value}</Typography.Link>
{value}
</Typography.Link>
), ),
}, },
{ {
@ -165,9 +202,12 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
<> <>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} /> <ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
<ColumnButton onClick={(): void => onEditHandler(record)} type="link"> <ColumnButton onClick={onEditHandler(record)} type="link">
Edit Edit
</ColumnButton> </ColumnButton>
<ColumnButton onClick={onCloneHandler(record)} type="link">
Clone
</ColumnButton>
<DeleteAlert notifications={notificationsApi} setData={setData} id={id} /> <DeleteAlert notifications={notificationsApi} setData={setData} id={id} />
</> </>

View File

@ -71,23 +71,8 @@ function ImportJSON({
setDashboardCreating(true); setDashboardCreating(true);
const dashboardData = JSON.parse(editorValue) as DashboardData; const dashboardData = JSON.parse(editorValue) as DashboardData;
// removing the queryData
const parsedWidgets: DashboardData = {
...dashboardData,
widgets: dashboardData.widgets?.map((e) => ({
...e,
queryData: {
...e.queryData,
data: e.queryData.data,
error: false,
errorMessage: '',
loading: false,
},
})),
};
const response = await createDashboard({ const response = await createDashboard({
...parsedWidgets, ...dashboardData,
uploadedGrafana, uploadedGrafana,
}); });

View File

@ -1 +0,0 @@
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];

View File

@ -1,16 +1,11 @@
import { import { CloudDownloadOutlined, FastBackwardOutlined } from '@ant-design/icons';
CloudDownloadOutlined, import { Button, Divider, Dropdown, MenuProps } from 'antd';
FastBackwardOutlined,
LeftOutlined,
RightOutlined,
} from '@ant-design/icons';
import { Button, Divider, Dropdown, MenuProps, Select } from 'antd';
import { Excel } from 'antd-table-saveas-excel'; import { Excel } from 'antd-table-saveas-excel';
import Controls from 'container/Controls';
import { getGlobalTime } from 'container/LogsSearchFilter/utils'; import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import { getMinMax } from 'container/TopNav/AutoRefresh/config'; import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { defaultSelectStyle } from 'pages/Logs/config';
import * as Papa from 'papaparse'; import * as Papa from 'papaparse';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -26,7 +21,6 @@ import {
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
import { ITEMS_PER_PAGE_OPTIONS } from './config';
import { Container, DownloadLogButton } from './styles'; import { Container, DownloadLogButton } from './styles';
function LogControls(): JSX.Element | null { function LogControls(): JSX.Element | null {
@ -149,15 +143,6 @@ function LogControls(): JSX.Element | null {
const isLoading = isLogsLoading || isLoadingAggregate; const isLoading = isLogsLoading || isLoadingAggregate;
const isNextAndPreviousDisabled = useMemo(
() =>
isLoading ||
logLinesPerPage === 0 ||
logs.length === 0 ||
logs.length < logLinesPerPage,
[isLoading, logLinesPerPage, logs.length],
);
if (liveTail !== 'STOPPED') { if (liveTail !== 'STOPPED') {
return null; return null;
} }
@ -179,37 +164,14 @@ function LogControls(): JSX.Element | null {
<FastBackwardOutlined /> Go to latest <FastBackwardOutlined /> Go to latest
</Button> </Button>
<Divider type="vertical" /> <Divider type="vertical" />
<Button <Controls
loading={isLoading} isLoading={isLoading}
size="small" count={logs.length}
type="link" countPerPage={logLinesPerPage}
disabled={isNextAndPreviousDisabled} handleNavigatePrevious={handleNavigatePrevious}
onClick={handleNavigatePrevious} handleNavigateNext={handleNavigateNext}
> handleCountItemsPerPageChange={handleLogLinesPerPageChange}
<LeftOutlined /> Previous />
</Button>
<Button
loading={isLoading}
size="small"
type="link"
disabled={isNextAndPreviousDisabled}
onClick={handleNavigateNext}
>
Next <RightOutlined />
</Button>
<Select
style={defaultSelectStyle}
loading={isLoading}
value={logLinesPerPage}
onChange={handleLogLinesPerPageChange}
>
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
<Select.Option
key={count}
value={count}
>{`${count} / page`}</Select.Option>
))}
</Select>
</Container> </Container>
); );
} }

View File

@ -32,7 +32,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
const flattenLogData: Record<string, any> | null = useMemo( const flattenLogData: Record<string, string> | null = useMemo(
() => (logData ? flattenObject(logData) : null), () => (logData ? flattenObject(logData) : null),
[logData], [logData],
); );

View File

@ -0,0 +1,11 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const CardStyled = styled(Card)`
position: relative;
margin: 0.5rem 0 3.1rem 0;
.ant-card-body {
height: 20vh;
min-height: 200px;
}
`;

View File

@ -0,0 +1,66 @@
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CardStyled } from './LogsExplorerChart.styled';
export function LogsExplorerChart(): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const panelTypeParam = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const { data, isFetching } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.metrics,
graphType: panelTypeParam,
globalSelectedInterval: selectedTime,
selectedTime: 'GLOBAL_TIME',
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
selectedTime,
stagedQuery,
panelTypeParam,
],
enabled: !!stagedQuery,
},
);
const graphData = useMemo(() => {
if (data?.payload.data && data.payload.data.result.length > 0) {
return getExplorerChartData([data.payload.data.result[0]]);
}
return getExplorerChartData([]);
}, [data]);
return (
<CardStyled>
{isFetching ? (
<Spinner size="default" height="100%" />
) : (
<Graph
name="logsExplorerChart"
data={graphData}
type="bar"
containerHeight="100%"
animate
/>
)}
</CardStyled>
);
}

View File

@ -0,0 +1 @@
export { LogsExplorerChart } from './LogsExplorerChart';

View File

@ -0,0 +1,9 @@
import { Tabs } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const TabsStyled = styled(Tabs)`
& .ant-tabs-nav {
background-color: ${themeColors.lightBlack};
}
`;

View File

@ -0,0 +1,75 @@
import { TabsProps } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { TabsStyled } from './LogsExplorerViews.styled';
export function LogsExplorerViews(): JSX.Element {
const location = useLocation();
const urlQuery = useUrlQuery();
const history = useHistory();
const { currentQuery } = useQueryBuilder();
const panelTypeParams = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const isMultipleQueries = useMemo(
() =>
currentQuery.builder.queryData.length > 1 ||
currentQuery.builder.queryFormulas.length > 0,
[currentQuery],
);
const tabsItems: TabsProps['items'] = useMemo(
() => [
{
label: 'List View',
key: PANEL_TYPES.LIST,
disabled: isMultipleQueries,
},
{ label: 'TimeSeries', key: PANEL_TYPES.TIME_SERIES },
{ label: 'Table', key: PANEL_TYPES.TABLE },
],
[isMultipleQueries],
);
const handleChangeView = useCallback(
(panelType: string) => {
urlQuery.set(PANEL_TYPES_QUERY, JSON.stringify(panelType) as GRAPH_TYPES);
const path = `${location.pathname}?${urlQuery}`;
history.push(path);
},
[history, location, urlQuery],
);
const currentTabKey = useMemo(
() =>
Object.values(PANEL_TYPES).includes(panelTypeParams)
? panelTypeParams
: PANEL_TYPES.LIST,
[panelTypeParams],
);
useEffect(() => {
if (panelTypeParams === 'list' && isMultipleQueries) {
handleChangeView(PANEL_TYPES.TIME_SERIES);
}
}, [panelTypeParams, isMultipleQueries, handleChangeView]);
return (
<div>
<TabsStyled
items={tabsItems}
defaultActiveKey={currentTabKey}
activeKey={currentTabKey}
onChange={handleChangeView}
/>
</div>
);
}

View File

@ -0,0 +1 @@
export { LogsExplorerViews } from './LogsExplorerViews';

View File

@ -6,7 +6,7 @@ import {
QueryOperatorsMultiVal, QueryOperatorsMultiVal,
QueryOperatorsSingleVal, QueryOperatorsSingleVal,
} from 'lib/logql/tokens'; } from 'lib/logql/tokens';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
@ -56,6 +56,8 @@ function QueryField({
onUpdate, onUpdate,
onDelete, onDelete,
}: QueryFieldProps): JSX.Element | null { }: QueryFieldProps): JSX.Element | null {
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
const { const {
fields: { selected }, fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs); } = useSelector<AppState, ILogsReducer>((store) => store.logs);
@ -136,9 +138,12 @@ function QueryField({
<Select <Select
mode="tags" mode="tags"
style={{ width: '100%' }} style={{ width: '100%' }}
open={isDropDownOpen}
onChange={(e): void => handleChange(2, e as never)} onChange={(e): void => handleChange(2, e as never)}
defaultValue={(query[2] && query[2].value) || []} defaultValue={(query[2] && query[2].value) || []}
notFoundContent={null} notFoundContent={null}
onInputKeyDown={(): void => setIsDropDownOpen(true)}
onSelect={(): void => setIsDropDownOpen(false)}
/> />
) : ( ) : (
<Input <Input

View File

@ -10,12 +10,6 @@ export const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => ({
opacity: '0', opacity: '0',
panelTypes: PANEL_TYPES.TIME_SERIES, panelTypes: PANEL_TYPES.TIME_SERIES,
query, query,
queryData: {
data: { queryData: [] },
error: false,
errorMessage: '',
loading: false,
},
timePreferance: 'GLOBAL_TIME', timePreferance: 'GLOBAL_TIME',
title: '', title: '',
}); });

View File

@ -1,6 +1,6 @@
import { import {
initialFormulaBuilderFormValues, initialFormulaBuilderFormValues,
initialQueryBuilderFormValues, initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
@ -18,7 +18,7 @@ export const getQueryBuilderQueries = ({
queryFormulas: [], queryFormulas: [],
queryData: [ queryData: [
{ {
...initialQueryBuilderFormValues, ...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: MetricAggregateOperator.SUM_RATE, aggregateOperator: MetricAggregateOperator.SUM_RATE,
disabled: false, disabled: false,
groupBy, groupBy,
@ -53,7 +53,7 @@ export const getQueryBuilderQuerieswithFormula = ({
], ],
queryData: [ queryData: [
{ {
...initialQueryBuilderFormValues, ...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: MetricAggregateOperator.SUM_RATE, aggregateOperator: MetricAggregateOperator.SUM_RATE,
disabled, disabled,
groupBy, groupBy,
@ -66,7 +66,7 @@ export const getQueryBuilderQuerieswithFormula = ({
}, },
}, },
{ {
...initialQueryBuilderFormValues, ...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: MetricAggregateOperator.SUM_RATE, aggregateOperator: MetricAggregateOperator.SUM_RATE,
disabled, disabled,
groupBy, groupBy,

View File

@ -14,6 +14,7 @@ import { useParams } from 'react-router-dom';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { Card, GraphContainer, GraphTitle, Row } from '../styles'; import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { Button } from './styles'; import { Button } from './styles';
@ -56,6 +57,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
tagFilterItems, tagFilterItems,
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(),
}), }),
[getWidgetQueryBuilder, servicename, tagFilterItems], [getWidgetQueryBuilder, servicename, tagFilterItems],
); );
@ -69,6 +71,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
tagFilterItems, tagFilterItems,
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(),
}), }),
[getWidgetQueryBuilder, servicename, tagFilterItems], [getWidgetQueryBuilder, servicename, tagFilterItems],
); );

View File

@ -15,6 +15,7 @@ import { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { Card, GraphContainer, GraphTitle, Row } from '../styles'; import { Card, GraphContainer, GraphTitle, Row } from '../styles';
import { legend } from './constant'; import { legend } from './constant';
@ -48,6 +49,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
tagFilterItems, tagFilterItems,
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(),
}), }),
[getWidgetQueryBuilder, servicename, tagFilterItems], [getWidgetQueryBuilder, servicename, tagFilterItems],
); );
@ -67,6 +69,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
tagFilterItems, tagFilterItems,
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(),
}), }),
[getWidgetQueryBuilder, servicename, tagFilterItems], [getWidgetQueryBuilder, servicename, tagFilterItems],
); );
@ -82,6 +85,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
tagFilterItems, tagFilterItems,
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(),
}), }),
[getWidgetQueryBuilder, servicename, tagFilterItems], [getWidgetQueryBuilder, servicename, tagFilterItems],
); );
@ -97,6 +101,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
tagFilterItems, tagFilterItems,
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(),
}), }),
[getWidgetQueryBuilder, servicename, tagFilterItems], [getWidgetQueryBuilder, servicename, tagFilterItems],
); );

View File

@ -21,6 +21,7 @@ import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import MetricReducer from 'types/reducer/metrics'; import MetricReducer from 'types/reducer/metrics';
import { v4 as uuid } from 'uuid';
import { import {
errorPercentage, errorPercentage,
@ -91,6 +92,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
topLevelOperations, topLevelOperations,
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(),
}), }),
[getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems], [getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems],
); );
@ -106,6 +108,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
topLevelOperations, topLevelOperations,
}), }),
clickhouse_sql: [], clickhouse_sql: [],
id: uuid(),
}), }),
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder], [servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
); );

View File

@ -11,6 +11,8 @@ import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { getErrorRate } from './utils';
function TopOperationsTable(props: TopOperationsTableProps): JSX.Element { function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>( const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime, (state) => state.globalTime,
@ -89,10 +91,10 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
dataIndex: 'errorCount', dataIndex: 'errorCount',
key: 'errorCount', key: 'errorCount',
width: 50, width: 50,
sorter: (a: TopOperationList, b: TopOperationList): number => sorter: (first: TopOperationList, second: TopOperationList): number =>
a.errorCount - b.errorCount, getErrorRate(first) - getErrorRate(second),
render: (value: number, record: TopOperationList): string => render: (_, record: TopOperationList): string =>
`${((value / record.numCalls) * 100).toFixed(2)} %`, `${getErrorRate(record).toFixed(2)} %`,
}, },
]; ];

View File

@ -0,0 +1,4 @@
import { TopOperationList } from './TopOperationsTable';
export const getErrorRate = (list: TopOperationList): number =>
(list.errorCount / list.numCalls) * 100;

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { initialQueryWithType } from 'constants/queryBuilder'; import { initialQueriesMap } from 'constants/queryBuilder';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
@ -47,7 +47,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
history.push( history.push(
`${history.location.pathname}/new?graphType=${name}&widgetId=${ `${history.location.pathname}/new?graphType=${name}&widgetId=${
emptyLayout.i emptyLayout.i
}&${COMPOSITE_QUERY}=${JSON.stringify(initialQueryWithType)}`, }&${COMPOSITE_QUERY}=${JSON.stringify(initialQueriesMap.metrics)}`,
); );
} catch (error) { } catch (error) {
notifications.error({ notifications.error({

View File

@ -10,6 +10,7 @@ import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboa
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { IDashboardVariable } from 'types/api/dashboard/getAll'; import { IDashboardVariable } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards'; import DashboardReducer from 'types/reducer/dashboards';
import VariableItem from './VariableItem'; import VariableItem from './VariableItem';
@ -29,6 +30,8 @@ function DashboardVariableSelection({
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>(''); const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const onVarChanged = (name: string): void => { const onVarChanged = (name: string): void => {
setLastUpdatedVar(name); setLastUpdatedVar(name);
setUpdate(!update); setUpdate(!update);
@ -36,19 +39,15 @@ function DashboardVariableSelection({
const onValueUpdate = ( const onValueUpdate = (
name: string, name: string,
value: value: IDashboardVariable['selectedValue'],
| string
| string[]
| number
| number[]
| boolean
| boolean[]
| null
| undefined,
): void => { ): void => {
const updatedVariablesData = { ...variables }; const updatedVariablesData = { ...variables };
updatedVariablesData[name].selectedValue = value; updatedVariablesData[name].selectedValue = value;
if (role !== 'VIEWER') {
updateDashboardVariables(updatedVariablesData, notifications); updateDashboardVariables(updatedVariablesData, notifications);
}
onVarChanged(name); onVarChanged(name);
}; };
const onAllSelectedUpdate = ( const onAllSelectedUpdate = (
@ -57,7 +56,10 @@ function DashboardVariableSelection({
): void => { ): void => {
const updatedVariablesData = { ...variables }; const updatedVariablesData = { ...variables };
updatedVariablesData[name].allSelected = value; updatedVariablesData[name].allSelected = value;
if (role !== 'VIEWER') {
updateDashboardVariables(updatedVariablesData, notifications); updateDashboardVariables(updatedVariablesData, notifications);
}
onVarChanged(name); onVarChanged(name);
}; };

View File

@ -6,28 +6,14 @@ import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
import { DashboardData } from 'types/api/dashboard/getAll'; import { DashboardData } from 'types/api/dashboard/getAll';
import { cleardQueryData, downloadObjectAsJson } from './util'; import { downloadObjectAsJson } from './util';
function ShareModal({ function ShareModal({
isJSONModalVisible, isJSONModalVisible,
onToggleHandler, onToggleHandler,
selectedData, selectedData,
}: ShareModalProps): JSX.Element { }: ShareModalProps): JSX.Element {
const getParsedValue = (): string => { const getParsedValue = (): string => JSON.stringify(selectedData, null, 2);
const updatedData: DashboardData = {
...selectedData,
widgets: selectedData.widgets?.map((widget) => ({
...widget,
queryData: {
...widget.queryData,
loading: false,
error: false,
errorMessage: '',
},
})),
};
return JSON.stringify(updatedData, null, 2);
};
const [jsonValue, setJSONValue] = useState<string>(getParsedValue()); const [jsonValue, setJSONValue] = useState<string>(getParsedValue());
const [isViewJSON, setIsViewJSON] = useState<boolean>(false); const [isViewJSON, setIsViewJSON] = useState<boolean>(false);
@ -53,7 +39,6 @@ function ShareModal({
} }
}, [state.error, state.value, t, notifications]); }, [state.error, state.value, t, notifications]);
const selectedDataCleaned = cleardQueryData(selectedData);
const GetFooterComponent = useMemo(() => { const GetFooterComponent = useMemo(() => {
if (!isViewJSON) { if (!isViewJSON) {
return ( return (
@ -69,7 +54,7 @@ function ShareModal({
<Button <Button
type="primary" type="primary"
onClick={(): void => { onClick={(): void => {
downloadObjectAsJson(selectedDataCleaned, selectedData.title); downloadObjectAsJson(selectedData, selectedData.title);
}} }}
> >
{t('download_json')} {t('download_json')}
@ -82,7 +67,7 @@ function ShareModal({
{t('copy_to_clipboard')} {t('copy_to_clipboard')}
</Button> </Button>
); );
}, [isViewJSON, jsonValue, selectedData, selectedDataCleaned, setCopy, t]); }, [isViewJSON, jsonValue, selectedData, setCopy, t]);
return ( return (
<Modal <Modal

View File

@ -1,5 +1,3 @@
import { DashboardData } from 'types/api/dashboard/getAll';
export function downloadObjectAsJson( export function downloadObjectAsJson(
exportObj: unknown, exportObj: unknown,
exportName: string, exportName: string,
@ -14,18 +12,3 @@ export function downloadObjectAsJson(
downloadAnchorNode.click(); downloadAnchorNode.click();
downloadAnchorNode.remove(); downloadAnchorNode.remove();
} }
export function cleardQueryData(param: DashboardData): DashboardData {
return {
...param,
widgets: param.widgets?.map((widget) => ({
...widget,
queryData: {
...widget.queryData,
data: {
queryData: [],
},
},
})),
};
}

View File

@ -1,11 +1,13 @@
import { Button, Tabs, Typography } from 'antd'; import { Button, Tabs, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { WidgetGraphProps } from 'container/NewWidget/types';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useState } from 'react'; import { useCallback } from 'react';
import { connect, useSelector } from 'react-redux'; import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
@ -18,21 +20,31 @@ import AppActions from 'types/actions';
import { Widgets } from 'types/api/dashboard/getAll'; import { Widgets } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards'; import DashboardReducer from 'types/reducer/dashboards';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse'; import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL'; import PromQLQueryContainer from './QueryBuilder/promQL';
function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { function QuerySection({
updateQuery,
selectedGraph,
selectedTime,
}: QueryProps): JSX.Element {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const [isInit, setIsInit] = useState<boolean>(false); const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const { dashboards, isLoadingQueryResult } = useSelector< const getWidgetQueryRange = useGetWidgetQueryRange({
AppState, graphType: selectedGraph,
DashboardReducer selectedTime: selectedTime.enum,
>((state) => state.dashboards); });
const [selectedDashboards] = dashboards; const [selectedDashboards] = dashboards;
const { widgets } = selectedDashboards.data; const { widgets } = selectedDashboards.data;
@ -46,23 +58,11 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
const { query } = selectedWidget; const { query } = selectedWidget;
const { compositeQuery } = useShareBuilderUrl({ defaultValue: query }); useShareBuilderUrl({ defaultValue: query });
useEffect(() => {
if (!isInit && compositeQuery) {
setIsInit(true);
updateQuery({
updatedQuery: compositeQuery,
widgetId: urlQuery.get('widgetId') || '',
yAxisUnit: selectedWidget.yAxisUnit,
});
}
}, [isInit, compositeQuery, selectedWidget, urlQuery, updateQuery]);
const handleStageQuery = useCallback( const handleStageQuery = useCallback(
(updatedQuery: Query): void => { (updatedQuery: Query): void => {
updateQuery({ updateQuery({
updatedQuery,
widgetId: urlQuery.get('widgetId') || '', widgetId: urlQuery.get('widgetId') || '',
yAxisUnit: selectedWidget.yAxisUnit, yAxisUnit: selectedWidget.yAxisUnit,
}); });
@ -76,7 +76,9 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
const handleQueryCategoryChange = (qCategory: string): void => { const handleQueryCategoryChange = (qCategory: string): void => {
const currentQueryType = qCategory as EQueryType; const currentQueryType = qCategory as EQueryType;
featureResponse.refetch().then(() => {
handleStageQuery({ ...currentQuery, queryType: currentQueryType }); handleStageQuery({ ...currentQuery, queryType: currentQueryType });
});
}; };
const handleRunQuery = (): void => { const handleRunQuery = (): void => {
@ -115,7 +117,7 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> <span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" /> <TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
<Button <Button
loading={isLoadingQueryResult} loading={getWidgetQueryRange.isFetching}
type="primary" type="primary"
onClick={handleRunQuery} onClick={handleRunQuery}
> >
@ -142,6 +144,7 @@ const mapDispatchToProps = (
interface QueryProps extends DispatchProps { interface QueryProps extends DispatchProps {
selectedGraph: GRAPH_TYPES; selectedGraph: GRAPH_TYPES;
selectedTime: WidgetGraphProps['selectedTime'];
} }
export default connect(null, mapDispatchToProps)(QuerySection); export default connect(null, mapDispatchToProps)(QuerySection);

View File

@ -1,6 +1,8 @@
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent'; import GridGraphComponent from 'container/GridGraphComponent';
import { NewWidgetProps } from 'container/NewWidget'; import { WidgetGraphProps } from 'container/NewWidget/types';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import getChartData from 'lib/getChartData'; import getChartData from 'lib/getChartData';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@ -12,6 +14,7 @@ import { NotFoundContainer } from './styles';
function WidgetGraph({ function WidgetGraph({
selectedGraph, selectedGraph,
yAxisUnit, yAxisUnit,
selectedTime,
}: WidgetGraphProps): JSX.Element { }: WidgetGraphProps): JSX.Element {
const { dashboards } = useSelector<AppState, DashboardReducer>( const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards, (state) => state.dashboards,
@ -27,20 +30,28 @@ function WidgetGraph({
const selectedWidget = widgets.find((e) => e.id === widgetId); const selectedWidget = widgets.find((e) => e.id === widgetId);
const getWidgetQueryRange = useGetWidgetQueryRange({
graphType: selectedGraph,
selectedTime: selectedTime.enum,
});
if (selectedWidget === undefined) { if (selectedWidget === undefined) {
return <Card>Invalid widget</Card>; return <Card>Invalid widget</Card>;
} }
const { queryData, title, opacity, isStacked } = selectedWidget; const { title, opacity, isStacked } = selectedWidget;
if (queryData.error) { if (getWidgetQueryRange.error) {
return ( return (
<NotFoundContainer> <NotFoundContainer>
<Typography>{queryData.errorMessage}</Typography> <Typography>{getWidgetQueryRange.error.message}</Typography>
</NotFoundContainer> </NotFoundContainer>
); );
} }
if (queryData.data.queryData.length === 0) { if (getWidgetQueryRange.isLoading) {
return <Spinner size="large" tip="Loading..." />;
}
if (getWidgetQueryRange.data?.payload.data.result.length === 0) {
return ( return (
<NotFoundContainer> <NotFoundContainer>
<Typography>No Data</Typography> <Typography>No Data</Typography>
@ -49,7 +60,9 @@ function WidgetGraph({
} }
const chartDataSet = getChartData({ const chartDataSet = getChartData({
queryData: [queryData.data], queryData: [
{ queryData: getWidgetQueryRange.data?.payload.data.result ?? [] },
],
}); });
return ( return (
@ -65,6 +78,4 @@ function WidgetGraph({
); );
} }
type WidgetGraphProps = NewWidgetProps;
export default WidgetGraph; export default WidgetGraph;

View File

@ -1,24 +1,28 @@
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { Card } from 'container/GridGraphLayout/styles'; import { Card } from 'container/GridGraphLayout/styles';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo } from 'react'; import { memo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards'; import DashboardReducer from 'types/reducer/dashboards';
import { NewWidgetProps } from '../../index'; import { WidgetGraphProps } from '../../types';
import PlotTag from './PlotTag'; import PlotTag from './PlotTag';
import { AlertIconContainer, Container, NotFoundContainer } from './styles'; import { AlertIconContainer, Container } from './styles';
import WidgetGraphComponent from './WidgetGraph'; import WidgetGraphComponent from './WidgetGraph';
function WidgetGraph({ function WidgetGraph({
selectedGraph, selectedGraph,
yAxisUnit, yAxisUnit,
selectedTime,
}: WidgetGraphProps): JSX.Element { }: WidgetGraphProps): JSX.Element {
const { dashboards, isQueryFired } = useSelector<AppState, DashboardReducer>( const { currentQuery } = useQueryBuilder();
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards, (state) => state.dashboards,
); );
const [selectedDashboard] = dashboards; const [selectedDashboard] = dashboards;
const { search } = useLocation(); const { search } = useLocation();
@ -31,33 +35,31 @@ function WidgetGraph({
const selectedWidget = widgets.find((e) => e.id === widgetId); const selectedWidget = widgets.find((e) => e.id === widgetId);
const getWidgetQueryRange = useGetWidgetQueryRange({
graphType: selectedGraph,
selectedTime: selectedTime.enum,
});
if (selectedWidget === undefined) { if (selectedWidget === undefined) {
return <Card>Invalid widget</Card>; return <Card>Invalid widget</Card>;
} }
const { queryData } = selectedWidget;
return ( return (
<Container> <Container>
<PlotTag queryType={selectedWidget.query.queryType} /> <PlotTag queryType={currentQuery.queryType} />
{queryData.error && ( {getWidgetQueryRange.error && (
<AlertIconContainer color="red" title={queryData.errorMessage}> <AlertIconContainer color="red" title={getWidgetQueryRange.error.message}>
<InfoCircleOutlined /> <InfoCircleOutlined />
</AlertIconContainer> </AlertIconContainer>
)} )}
{!isQueryFired && ( <WidgetGraphComponent
<NotFoundContainer> selectedTime={selectedTime}
<Typography>No Data</Typography> selectedGraph={selectedGraph}
</NotFoundContainer> yAxisUnit={yAxisUnit}
)} />
{isQueryFired && (
<WidgetGraphComponent selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} />
)}
</Container> </Container>
); );
} }
type WidgetGraphProps = NewWidgetProps;
export default memo(WidgetGraph); export default memo(WidgetGraph);

View File

@ -1,6 +1,6 @@
import { memo } from 'react'; import { memo } from 'react';
import { NewWidgetProps } from '../index'; import { WidgetGraphProps } from '../types';
import QuerySection from './QuerySection'; import QuerySection from './QuerySection';
import { QueryContainer } from './styles'; import { QueryContainer } from './styles';
import WidgetGraph from './WidgetGraph'; import WidgetGraph from './WidgetGraph';
@ -8,12 +8,17 @@ import WidgetGraph from './WidgetGraph';
function LeftContainer({ function LeftContainer({
selectedGraph, selectedGraph,
yAxisUnit, yAxisUnit,
}: NewWidgetProps): JSX.Element { selectedTime,
}: WidgetGraphProps): JSX.Element {
return ( return (
<> <>
<WidgetGraph selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} /> <WidgetGraph
selectedTime={selectedTime}
selectedGraph={selectedGraph}
yAxisUnit={yAxisUnit}
/>
<QueryContainer> <QueryContainer>
<QuerySection selectedGraph={selectedGraph} /> <QuerySection selectedTime={selectedTime} selectedGraph={selectedGraph} />
</QueryContainer> </QueryContainer>
</> </>
); );

View File

@ -1,25 +1,18 @@
import { LockFilled } from '@ant-design/icons'; import { LockFilled } from '@ant-design/icons';
import { Button, Modal, Tooltip, Typography } from 'antd'; import { Button, Modal, Tooltip, Typography } from 'antd';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag'; import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import history from 'lib/history'; import history from 'lib/history';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget'; import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux'; import { connect, useDispatch, useSelector } from 'react-redux';
import { generatePath, useLocation, useParams } from 'react-router-dom'; import { generatePath, useLocation, useParams } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
import {
GetQueryResults,
GetQueryResultsProps,
} from 'store/actions/dashboard/getQueryResults';
import { import {
SaveDashboard, SaveDashboard,
SaveDashboardProps, SaveDashboardProps,
@ -27,10 +20,10 @@ import {
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { FLUSH_DASHBOARD } from 'types/actions/dashboard'; import { FLUSH_DASHBOARD } from 'types/actions/dashboard';
import { Widgets } from 'types/api/dashboard/getAll'; import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards'; import DashboardReducer from 'types/reducer/dashboards';
import { GlobalReducer } from 'types/reducer/globalTime';
import LeftContainer from './LeftContainer'; import LeftContainer from './LeftContainer';
import QueryTypeTag from './LeftContainer/QueryTypeTag'; import QueryTypeTag from './LeftContainer/QueryTypeTag';
@ -43,21 +36,15 @@ import {
PanelContainer, PanelContainer,
RightContainerWrapper, RightContainerWrapper,
} from './styles'; } from './styles';
import { NewWidgetProps } from './types';
function NewWidget({ function NewWidget({ selectedGraph, saveSettingOfPanel }: Props): JSX.Element {
selectedGraph,
saveSettingOfPanel,
getQueryResults,
}: Props): JSX.Element {
const urlQuery = useUrlQuery();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { dashboards } = useSelector<AppState, DashboardReducer>( const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards, (state) => state.dashboards,
); );
const { selectedTime: globalSelectedInterval } = useSelector<
AppState, const { currentQuery } = useQueryBuilder();
GlobalReducer
>((state) => state.globalTime);
const { featureResponse } = useSelector<AppState, AppReducer>( const { featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app, (state) => state.app,
@ -161,27 +148,6 @@ function NewWidget({
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, dispatch]); }, [dashboardId, dispatch]);
const getQueryResult = useCallback(() => {
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
if ((selectedWidget?.id.length !== 0 && compositeQuery) || compositeQuery) {
getQueryResults({
query: JSON.parse(compositeQuery),
selectedTime: selectedTime.enum,
widgetId: selectedWidget?.id || '',
graphType,
globalSelectedInterval,
variables: getDashboardVariables(),
});
}
}, [
selectedTime.enum,
selectedWidget?.id,
getQueryResults,
globalSelectedInterval,
graphType,
urlQuery,
]);
const setGraphHandler = (type: ITEMS): void => { const setGraphHandler = (type: ITEMS): void => {
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
params.set('graphType', type); params.set('graphType', type);
@ -189,10 +155,6 @@ function NewWidget({
setGraphType(type); setGraphType(type);
}; };
useEffect(() => {
getQueryResult();
}, [getQueryResult]);
const onSaveDashboard = useCallback((): void => { const onSaveDashboard = useCallback((): void => {
setSaveModal(true); setSaveModal(true);
}, []); }, []);
@ -201,15 +163,53 @@ function NewWidget({
FeatureKeys.QUERY_BUILDER_PANELS, FeatureKeys.QUERY_BUILDER_PANELS,
); );
const isNewTraceLogsAvailable = useMemo(
() =>
isQueryBuilderActive &&
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
currentQuery.builder.queryData.find(
(query) => query.dataSource !== DataSource.METRICS,
) !== undefined,
[
currentQuery.builder.queryData,
currentQuery.queryType,
isQueryBuilderActive,
],
);
const isSaveDisabled = useMemo(() => {
// new created dashboard
if (selectedWidget?.id === 'empty') {
return isNewTraceLogsAvailable;
}
const isTraceOrLogsQueryBuilder =
currentQuery.builder.queryData.find(
(query) =>
query.dataSource === DataSource.TRACES ||
query.dataSource === DataSource.LOGS,
) !== undefined;
if (isTraceOrLogsQueryBuilder) {
return false;
}
return isNewTraceLogsAvailable;
}, [
currentQuery.builder.queryData,
selectedWidget?.id,
isNewTraceLogsAvailable,
]);
return ( return (
<Container> <Container>
<ButtonContainer> <ButtonContainer>
{isQueryBuilderActive && ( {isSaveDisabled && (
<Tooltip title={MESSAGE.PANEL}> <Tooltip title={MESSAGE.PANEL}>
<Button <Button
icon={<LockFilled />} icon={<LockFilled />}
type="primary" type="primary"
disabled={isQueryBuilderActive} disabled={isSaveDisabled}
onClick={onSaveDashboard} onClick={onSaveDashboard}
> >
Save Save
@ -217,12 +217,8 @@ function NewWidget({
</Tooltip> </Tooltip>
)} )}
{!isQueryBuilderActive && ( {!isSaveDisabled && (
<Button <Button type="primary" disabled={isSaveDisabled} onClick={onSaveDashboard}>
type="primary"
disabled={isQueryBuilderActive}
onClick={onSaveDashboard}
>
Save Save
</Button> </Button>
)} )}
@ -231,7 +227,11 @@ function NewWidget({
<PanelContainer> <PanelContainer>
<LeftContainerWrapper flex={5}> <LeftContainerWrapper flex={5}>
<LeftContainer selectedGraph={graphType} yAxisUnit={yAxisUnit} /> <LeftContainer
selectedTime={selectedTime}
selectedGraph={graphType}
yAxisUnit={yAxisUnit}
/>
</LeftContainerWrapper> </LeftContainerWrapper>
<RightContainerWrapper flex={1}> <RightContainerWrapper flex={1}>
@ -270,34 +270,24 @@ function NewWidget({
width={600} width={600}
> >
<Typography> <Typography>
Your graph built with{' '} Your graph built with <QueryTypeTag queryType={currentQuery.queryType} />{' '}
<QueryTypeTag queryType={selectedWidget?.query.queryType} /> query will be query will be saved. Press OK to confirm.
saved. Press OK to confirm.
</Typography> </Typography>
</Modal> </Modal>
</Container> </Container>
); );
} }
export interface NewWidgetProps {
selectedGraph: GRAPH_TYPES;
yAxisUnit: Widgets['yAxisUnit'];
}
interface DispatchProps { interface DispatchProps {
saveSettingOfPanel: ( saveSettingOfPanel: (
props: SaveDashboardProps, props: SaveDashboardProps,
) => (dispatch: Dispatch<AppActions>) => void; ) => (dispatch: Dispatch<AppActions>) => void;
getQueryResults: (
props: GetQueryResultsProps,
) => (dispatch: Dispatch<AppActions>) => void;
} }
const mapDispatchToProps = ( const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>, dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({ ): DispatchProps => ({
saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch), saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch),
getQueryResults: bindActionCreators(GetQueryResults, dispatch),
}); });
type Props = DispatchProps & NewWidgetProps; type Props = DispatchProps & NewWidgetProps;

View File

@ -0,0 +1,13 @@
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { Widgets } from 'types/api/dashboard/getAll';
import { timePreferance } from './RightContainer/timeItems';
export interface NewWidgetProps {
selectedGraph: GRAPH_TYPES;
yAxisUnit: Widgets['yAxisUnit'];
}
export interface WidgetGraphProps extends NewWidgetProps {
selectedTime: timePreferance;
}

View File

@ -0,0 +1,43 @@
import { SearchOutlined } from '@ant-design/icons';
import { Input } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles';
import { AddColumnSelect, AddColumnWrapper, SearchIconWrapper } from './styles';
function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
const { t } = useTranslation(['trace']);
const isDarkMode = useIsDarkMode();
if (!config) return null;
return (
<AddColumnWrapper direction="vertical">
<FieldTitle>{t('options_menu.addColumn')}</FieldTitle>
<Input.Group compact>
<AddColumnSelect
allowClear
maxTagCount={0}
size="small"
mode="multiple"
placeholder="Search"
options={config.options}
value={config.value}
onChange={config.onChange}
/>
<SearchIconWrapper $isDarkMode={isDarkMode}>
<SearchOutlined />
</SearchIconWrapper>
</Input.Group>
</AddColumnWrapper>
);
}
interface AddColumnFieldProps {
config: OptionsMenuConfig['addColumn'];
}
export default AddColumnField;

View File

@ -0,0 +1,28 @@
import { Card, Select, SelectProps, Space } from 'antd';
import { themeColors } from 'constants/theme';
import { FunctionComponent } from 'react';
import styled from 'styled-components';
export const SearchIconWrapper = styled(Card)<{ $isDarkMode: boolean }>`
width: 15%;
border-color: ${({ $isDarkMode }): string =>
$isDarkMode ? themeColors.borderDarkGrey : themeColors.borderLightGrey};
.ant-card-body {
display: flex;
justify-content: center;
align-items: center;
padding: 0.25rem;
font-size: 0.875rem;
}
`;
export const AddColumnSelect: FunctionComponent<SelectProps> = styled(
Select,
)<SelectProps>`
width: 85%;
`;
export const AddColumnWrapper = styled(Space)`
width: 100%;
`;

View File

@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles';
import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
const { t } = useTranslation(['trace']);
if (!config) return null;
return (
<FormatFieldWrapper direction="vertical">
<FieldTitle>{t('options_menu.format')}</FieldTitle>
<RadioGroup
size="small"
buttonStyle="solid"
value={config.value}
onChange={config.onChange}
>
<RadioButton value="row">{t('options_menu.row')}</RadioButton>
<RadioButton value="default">{t('options_menu.default')}</RadioButton>
<RadioButton value="column">{t('options_menu.column')}</RadioButton>
</RadioGroup>
</FormatFieldWrapper>
);
}
interface FormatFieldProps {
config: OptionsMenuConfig['format'];
}
export default FormatField;

View File

@ -0,0 +1,17 @@
import { Radio, Space } from 'antd';
import styled from 'styled-components';
export const FormatFieldWrapper = styled(Space)`
width: 100%;
margin-bottom: 1.125rem;
`;
export const RadioGroup = styled(Radio.Group)`
display: flex;
text-align: center;
`;
export const RadioButton = styled(Radio.Button)`
font-size: 0.75rem;
flex: 1;
`;

View File

@ -0,0 +1,29 @@
import { useTranslation } from 'react-i18next';
import { OptionsMenuConfig } from '..';
import { FieldTitle } from '../styles';
import { MaxLinesFieldWrapper, MaxLinesInput } from './styles';
function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null {
const { t } = useTranslation(['trace']);
if (!config) return null;
return (
<MaxLinesFieldWrapper>
<FieldTitle>{t('options_menu.maxLines')}</FieldTitle>
<MaxLinesInput
controls
size="small"
value={config.value}
onChange={config.onChange}
/>
</MaxLinesFieldWrapper>
);
}
interface MaxLinesFieldProps {
config: OptionsMenuConfig['maxLines'];
}
export default MaxLinesField;

View File

@ -0,0 +1,12 @@
import { InputNumber } from 'antd';
import styled from 'styled-components';
export const MaxLinesFieldWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
export const MaxLinesInput = styled(InputNumber)`
max-width: 46px;
`;

View File

@ -0,0 +1,57 @@
import { SettingFilled, SettingOutlined } from '@ant-design/icons';
import {
InputNumberProps,
Popover,
RadioProps,
SelectProps,
Space,
} from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import AddColumnField from './AddColumnField';
import FormatField from './FormatField';
import MaxLinesField from './MaxLinesField';
import { OptionsContainer, OptionsContentWrapper } from './styles';
function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
const { t } = useTranslation(['trace']);
const isDarkMode = useIsDarkMode();
const OptionsContent = useMemo(
() => (
<OptionsContentWrapper direction="vertical">
{config?.format && <FormatField config={config.format} />}
{config?.maxLines && <MaxLinesField config={config.maxLines} />}
{config?.addColumn && <AddColumnField config={config.addColumn} />}
</OptionsContentWrapper>
),
[config],
);
const SettingIcon = isDarkMode ? SettingOutlined : SettingFilled;
return (
<OptionsContainer>
<Popover placement="bottom" trigger="click" content={OptionsContent}>
<Space align="center">
{t('options_menu.options')}
<SettingIcon />
</Space>
</Popover>
</OptionsContainer>
);
}
export type OptionsMenuConfig = {
format?: Pick<RadioProps, 'value' | 'onChange'>;
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
addColumn?: Pick<SelectProps, 'options' | 'value' | 'onChange'>;
};
interface OptionsMenuProps {
config: OptionsMenuConfig;
}
export default OptionsMenu;

View File

@ -0,0 +1,19 @@
import { Card, Space, Typography } from 'antd';
import styled from 'styled-components';
export const OptionsContainer = styled(Card)`
.ant-card-body {
display: flex;
padding: 0.25rem 0.938rem;
cursor: pointer;
}
`;
export const OptionsContentWrapper = styled(Space)`
min-width: 11rem;
padding: 0.25rem 0.5rem;
`;
export const FieldTitle = styled(Typography.Text)`
font-size: 0.75rem;
`;

View File

@ -55,12 +55,18 @@ function PendingInvitesContainer(): JSX.Element {
queryKey: ['getPendingInvites', user?.accessJwt], queryKey: ['getPendingInvites', user?.accessJwt],
}); });
const toggleModal = (value: boolean): void => {
setIsInviteTeamMemberModalOpen(value);
};
const [dataSource, setDataSource] = useState<DataProps[]>([]); const [dataSource, setDataSource] = useState<DataProps[]>([]);
const toggleModal = useCallback(
(value: boolean): void => {
setIsInviteTeamMemberModalOpen(value);
if (!value) {
form.resetFields();
}
},
[form],
);
const { hash } = useLocation(); const { hash } = useLocation();
const getParsedInviteData = useCallback( const getParsedInviteData = useCallback(
@ -79,7 +85,7 @@ function PendingInvitesContainer(): JSX.Element {
if (hash === INVITE_MEMBERS_HASH) { if (hash === INVITE_MEMBERS_HASH) {
toggleModal(true); toggleModal(true);
} }
}, [hash]); }, [hash, toggleModal]);
useEffect(() => { useEffect(() => {
if ( if (
@ -225,7 +231,13 @@ function PendingInvitesContainer(): JSX.Element {
}); });
} }
}, },
[getParsedInviteData, getPendingInvitesResponse, notifications, t], [
getParsedInviteData,
getPendingInvitesResponse,
notifications,
t,
toggleModal,
],
); );
return ( return (

View File

@ -1,4 +1,5 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { ReactNode } from 'react';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
export type QueryBuilderConfig = export type QueryBuilderConfig =
@ -11,4 +12,5 @@ export type QueryBuilderConfig =
export type QueryBuilderProps = { export type QueryBuilderProps = {
config?: QueryBuilderConfig; config?: QueryBuilderConfig;
panelType: ITEMS; panelType: ITEMS;
actions?: ReactNode;
}; };

View File

@ -0,0 +1,6 @@
import { Col } from 'antd';
import styled from 'styled-components';
export const ActionsWrapperStyled = styled(Col)`
padding-right: 1rem;
`;

View File

@ -11,15 +11,16 @@ import { Formula, Query } from './components';
// ** Types // ** Types
import { QueryBuilderProps } from './QueryBuilder.interfaces'; import { QueryBuilderProps } from './QueryBuilder.interfaces';
// ** Styles // ** Styles
import { ActionsWrapperStyled } from './QueryBuilder.styled';
export const QueryBuilder = memo(function QueryBuilder({ export const QueryBuilder = memo(function QueryBuilder({
config, config,
panelType, panelType,
actions,
}: QueryBuilderProps): JSX.Element { }: QueryBuilderProps): JSX.Element {
const { const {
currentQuery, currentQuery,
setupInitialDataSource, setupInitialDataSource,
resetQueryBuilderInfo,
addNewBuilderQuery, addNewBuilderQuery,
addNewFormula, addNewFormula,
handleSetPanelType, handleSetPanelType,
@ -35,13 +36,6 @@ export const QueryBuilder = memo(function QueryBuilder({
handleSetPanelType(panelType); handleSetPanelType(panelType);
}, [handleSetPanelType, panelType]); }, [handleSetPanelType, panelType]);
useEffect(
() => (): void => {
resetQueryBuilderInfo();
},
[resetQueryBuilderInfo],
);
const isDisabledQueryButton = useMemo( const isDisabledQueryButton = useMemo(
() => currentQuery.builder.queryData.length >= MAX_QUERIES, () => currentQuery.builder.queryData.length >= MAX_QUERIES,
[currentQuery], [currentQuery],
@ -60,7 +54,7 @@ export const QueryBuilder = memo(function QueryBuilder({
); );
return ( return (
<Row gutter={[0, 20]} justify="start"> <Row style={{ width: '100%' }} gutter={[0, 20]} justify="start">
<Col span={24}> <Col span={24}>
<Row gutter={[0, 50]}> <Row gutter={[0, 50]}>
{currentQuery.builder.queryData.map((query, index) => ( {currentQuery.builder.queryData.map((query, index) => (
@ -81,6 +75,7 @@ export const QueryBuilder = memo(function QueryBuilder({
</Row> </Row>
</Col> </Col>
<ActionsWrapperStyled span={24}>
<Row gutter={[20, 0]}> <Row gutter={[20, 0]}>
<Col> <Col>
<Button <Button
@ -102,7 +97,9 @@ export const QueryBuilder = memo(function QueryBuilder({
Formula Formula
</Button> </Button>
</Col> </Col>
{actions}
</Row> </Row>
</ActionsWrapperStyled>
</Row> </Row>
); );
}); });

View File

@ -1,4 +1,4 @@
import { Col, Row } from 'antd'; import { Col, Row, Typography } from 'antd';
import { Fragment, memo, ReactNode, useState } from 'react'; import { Fragment, memo, ReactNode, useState } from 'react';
// ** Types // ** Types
@ -46,7 +46,9 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
<Col span={24}> <Col span={24}>
<StyledInner onClick={handleToggleOpenFilters}> <StyledInner onClick={handleToggleOpenFilters}>
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />} {isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>} {!isOpenedFilters && (
<Typography>Add conditions for {filtersTexts}</Typography>
)}
</StyledInner> </StyledInner>
</Col> </Col>
{isOpenedFilters && <Col span={24}>{children}</Col>} {isOpenedFilters && <Col span={24}>{children}</Col>}

View File

@ -1,3 +1,4 @@
import { Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { memo } from 'react'; import { memo } from 'react';
@ -11,5 +12,9 @@ export const FilterLabel = memo(function FilterLabel({
}: FilterLabelProps): JSX.Element { }: FilterLabelProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
return <StyledLabel isDarkMode={isDarkMode}>{label}</StyledLabel>; return (
<StyledLabel isDarkMode={isDarkMode}>
<Typography>{label}</Typography>
</StyledLabel>
);
}); });

View File

@ -3,19 +3,17 @@ import userEvent from '@testing-library/user-event';
// Constants // Constants
import { import {
HAVING_OPERATORS, HAVING_OPERATORS,
initialQueryBuilderFormValues, initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData'; import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData';
// ** Types // ** Types
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
// ** Components // ** Components
import { HavingFilter } from '../HavingFilter'; import { HavingFilter } from '../HavingFilter';
const valueWithAttributeAndOperator: IBuilderQuery = { const valueWithAttributeAndOperator: IBuilderQuery = {
...initialQueryBuilderFormValues, ...initialQueryBuilderFormValuesMap.logs,
dataSource: DataSource.LOGS,
aggregateOperator: 'SUM', aggregateOperator: 'SUM',
aggregateAttribute: { aggregateAttribute: {
isColumn: false, isColumn: false,
@ -29,7 +27,10 @@ describe('Having filter behaviour', () => {
test('Having filter render is rendered', () => { test('Having filter render is rendered', () => {
const mockFn = jest.fn(); const mockFn = jest.fn();
const { unmount } = render( const { unmount } = render(
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />, <HavingFilter
query={initialQueryBuilderFormValuesMap.metrics}
onChange={mockFn}
/>,
); );
const selectId = 'havingSelect'; const selectId = 'havingSelect';
@ -44,7 +45,10 @@ describe('Having filter behaviour', () => {
test('Having render is disabled initially', () => { test('Having render is disabled initially', () => {
const mockFn = jest.fn(); const mockFn = jest.fn();
const { unmount } = render( const { unmount } = render(
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />, <HavingFilter
query={initialQueryBuilderFormValuesMap.metrics}
onChange={mockFn}
/>,
); );
const input = screen.getByRole('combobox'); const input = screen.getByRole('combobox');

View File

@ -30,6 +30,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.SETTINGS]: [QueryParams.resourceAttributes], [ROUTES.SETTINGS]: [QueryParams.resourceAttributes],
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes], [ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
[ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes], [ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes],
[ROUTES.TRACES_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.TRACE]: [QueryParams.resourceAttributes], [ROUTES.TRACE]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes], [ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes],
[ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes], [ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],

View File

@ -1,8 +1,8 @@
export const getQueryString = ( export const getQueryString = (
avialableParams: string[], availableParams: string[],
params: URLSearchParams, params: URLSearchParams,
): string[] => ): string[] =>
avialableParams.map((param) => { availableParams.map((param) => {
if (params.has(param)) { if (params.has(param)) {
return `${param}=${params.get(param)}`; return `${param}=${params.get(param)}`;
} }

View File

@ -1,5 +1,5 @@
import { CheckCircleTwoTone, WarningOutlined } from '@ant-design/icons'; import { CheckCircleTwoTone, WarningOutlined } from '@ant-design/icons';
import { Menu, Space, Typography } from 'antd'; import { Menu, MenuProps } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get'; import getLocalStorageKey from 'api/browser/localstorage/get';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app'; import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
@ -27,7 +27,6 @@ import {
Sider, Sider,
SlackButton, SlackButton,
SlackMenuItemContainer, SlackMenuItemContainer,
Tags,
VersionContainer, VersionContainer,
} from './styles'; } from './styles';
@ -42,6 +41,7 @@ function SideNav(): JSX.Element {
>((state) => state.app); >((state) => state.app);
const { pathname, search } = useLocation(); const { pathname, search } = useLocation();
const { t } = useTranslation(''); const { t } = useTranslation('');
const onCollapse = useCallback(() => { const onCollapse = useCallback(() => {
@ -55,9 +55,9 @@ function SideNav(): JSX.Element {
const onClickHandler = useCallback( const onClickHandler = useCallback(
(to: string) => { (to: string) => {
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
const avialableParams = routeConfig[to]; const availableParams = routeConfig[to];
const queryString = getQueryString(avialableParams, params); const queryString = getQueryString(availableParams || [], params);
if (pathname !== to) { if (pathname !== to) {
history.push(`${to}?${queryString.join('&')}`); history.push(`${to}?${queryString.join('&')}`);
@ -66,6 +66,10 @@ function SideNav(): JSX.Element {
[pathname, search], [pathname, search],
); );
const onClickMenuHandler: MenuProps['onClick'] = (e) => {
onClickHandler(e.key);
};
const onClickSlackHandler = (): void => { const onClickSlackHandler = (): void => {
window.open('https://signoz.io/slack', '_blank'); window.open('https://signoz.io/slack', '_blank');
}; };
@ -104,30 +108,17 @@ function SideNav(): JSX.Element {
}, },
]; ];
const currentMenu = useMemo( const currentMenu = useMemo(() => {
() => menus.find((menu) => pathname.startsWith(menu.to)), const routeKeys = Object.keys(ROUTES) as (keyof typeof ROUTES)[];
[pathname], const currentRouteKey = routeKeys.find((key) => {
); const route = ROUTES[key];
return pathname === route;
});
const items = [ if (!currentRouteKey) return null;
...menus.map(({ to, Icon, name, tags, children }) => ({
key: to, return ROUTES[currentRouteKey];
icon: <Icon />, }, [pathname]);
onClick: (): void => onClickHandler(to),
label: (
<Space>
<div>{name}</div>
{tags &&
tags.map((e) => (
<Tags key={e}>
<Typography.Text>{e}</Typography.Text>
</Tags>
))}
</Space>
),
children,
})),
];
const sidebarItems = (props: SidebarItem, index: number): SidebarItem => ({ const sidebarItems = (props: SidebarItem, index: number): SidebarItem => ({
key: `${index}`, key: `${index}`,
@ -141,10 +132,11 @@ function SideNav(): JSX.Element {
<Menu <Menu
theme="dark" theme="dark"
defaultSelectedKeys={[ROUTES.APPLICATION]} defaultSelectedKeys={[ROUTES.APPLICATION]}
selectedKeys={currentMenu ? [currentMenu?.to] : []} selectedKeys={currentMenu ? [currentMenu] : []}
mode="vertical" mode="vertical"
style={styles} style={styles}
items={items} items={menus}
onClick={onClickMenuHandler}
/> />
{sidebar.map((props, index) => ( {sidebar.map((props, index) => (
<SlackMenuItemContainer <SlackMenuItemContainer
@ -155,7 +147,7 @@ function SideNav(): JSX.Element {
<Menu <Menu
theme="dark" theme="dark"
defaultSelectedKeys={[ROUTES.APPLICATION]} defaultSelectedKeys={[ROUTES.APPLICATION]}
selectedKeys={currentMenu ? [currentMenu?.to] : []} selectedKeys={currentMenu ? [currentMenu] : []}
mode="inline" mode="inline"
style={styles} style={styles}
items={[sidebarItems(props, index)]} items={[sidebarItems(props, index)]}

View File

@ -1,84 +0,0 @@
import {
AlertOutlined,
AlignLeftOutlined,
ApiOutlined,
BarChartOutlined,
BugOutlined,
DashboardFilled,
DeploymentUnitOutlined,
LineChartOutlined,
MenuOutlined,
SettingOutlined,
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import ROUTES from 'constants/routes';
const menus: SidebarMenu[] = [
{
Icon: BarChartOutlined,
to: ROUTES.APPLICATION,
name: 'Services',
},
{
Icon: MenuOutlined,
to: ROUTES.TRACE,
name: 'Traces',
},
{
Icon: AlignLeftOutlined,
to: ROUTES.LOGS,
name: 'Logs',
// tags: ['Beta'],
// children: [
// {
// key: ROUTES.LOGS,
// label: 'Search',
// },
// ],
},
{
Icon: DashboardFilled,
to: ROUTES.ALL_DASHBOARD,
name: 'Dashboards',
},
{
Icon: AlertOutlined,
to: ROUTES.LIST_ALL_ALERT,
name: 'Alerts',
},
{
Icon: BugOutlined,
to: ROUTES.ALL_ERROR,
name: 'Exceptions',
},
{
to: ROUTES.SERVICE_MAP,
name: 'Service Map',
Icon: DeploymentUnitOutlined,
},
{
Icon: LineChartOutlined,
to: ROUTES.USAGE_EXPLORER,
name: 'Usage Explorer',
},
{
Icon: SettingOutlined,
to: ROUTES.SETTINGS,
name: 'Settings',
},
{
Icon: ApiOutlined,
to: ROUTES.INSTRUMENTATION,
name: 'Get Started',
},
];
interface SidebarMenu {
to: string;
name: string;
Icon: typeof ApiOutlined;
tags?: string[];
children?: Required<MenuProps>['items'][number][];
}
export default menus;

View File

@ -0,0 +1,113 @@
import {
AlertOutlined,
AlignLeftOutlined,
ApiOutlined,
BarChartOutlined,
BugOutlined,
DashboardFilled,
DeploymentUnitOutlined,
LineChartOutlined,
MenuOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { MenuProps, Space, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { Tags } from './styles';
type MenuItem = Required<MenuProps>['items'][number];
export const createLabelWithTags = (
label: string,
tags: string[],
): JSX.Element => (
<Space>
<div>{label}</div>
{tags.map((tag) => (
<Tags key={tag}>
<Typography.Text>{tag}</Typography.Text>
</Tags>
))}
</Space>
);
const menus: SidebarMenu[] = [
{
key: ROUTES.APPLICATION,
label: 'Services',
icon: <BarChartOutlined />,
},
{
key: ROUTES.TRACE,
label: 'Traces',
icon: <MenuOutlined />,
// children: [
// {
// key: ROUTES.TRACE,
// label: 'Traces',
// },
// TODO: uncomment when will be ready explorer
// {
// key: ROUTES.TRACES_EXPLORER,
// label: "Explorer",
// },
// ],
},
{
key: ROUTES.LOGS,
label: 'Logs',
icon: <AlignLeftOutlined />,
// children: [
// {
// key: ROUTES.LOGS,
// label: 'Search',
// },
// TODO: uncomment when will be ready explorer
// {
// key: ROUTES.LOGS_EXPLORER,
// label: 'Views',
// },
// ],
},
{
key: ROUTES.ALL_DASHBOARD,
label: 'Dashboards',
icon: <DashboardFilled />,
},
{
key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts',
icon: <AlertOutlined />,
},
{
key: ROUTES.ALL_ERROR,
label: 'Exceptions',
icon: <BugOutlined />,
},
{
key: ROUTES.SERVICE_MAP,
label: 'Service Map',
icon: <DeploymentUnitOutlined />,
},
{
key: ROUTES.USAGE_EXPLORER,
label: 'Usage Explorer',
icon: <LineChartOutlined />,
},
{
key: ROUTES.SETTINGS,
label: 'Settings',
icon: <SettingOutlined />,
},
{
key: ROUTES.INSTRUMENTATION,
label: 'Get Started',
icon: <ApiOutlined />,
},
];
type SidebarMenu = MenuItem & {
tags?: string[];
};
export default menus;

View File

@ -5,6 +5,7 @@ import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
const breadcrumbNameMap = { const breadcrumbNameMap = {
[ROUTES.APPLICATION]: 'Services', [ROUTES.APPLICATION]: 'Services',
[ROUTES.TRACE]: 'Traces', [ROUTES.TRACE]: 'Traces',
[ROUTES.TRACES_EXPLORER]: 'Traces Explorer',
[ROUTES.SERVICE_MAP]: 'Service Map', [ROUTES.SERVICE_MAP]: 'Service Map',
[ROUTES.USAGE_EXPLORER]: 'Usage Explorer', [ROUTES.USAGE_EXPLORER]: 'Usage Explorer',
[ROUTES.INSTRUMENTATION]: 'Get Started', [ROUTES.INSTRUMENTATION]: 'Get Started',
@ -19,6 +20,7 @@ const breadcrumbNameMap = {
[ROUTES.LIST_ALL_ALERT]: 'Alerts', [ROUTES.LIST_ALL_ALERT]: 'Alerts',
[ROUTES.ALL_DASHBOARD]: 'Dashboard', [ROUTES.ALL_DASHBOARD]: 'Dashboard',
[ROUTES.LOGS]: 'Logs', [ROUTES.LOGS]: 'Logs',
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
}; };
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element { function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -0,0 +1,25 @@
import Controls from 'container/Controls';
import { memo } from 'react';
import { Container } from './styles';
function TraceExplorerControls(): JSX.Element | null {
const handleCountItemsPerPageChange = (): void => {};
const handleNavigatePrevious = (): void => {};
const handleNavigateNext = (): void => {};
return (
<Container>
<Controls
isLoading={false}
count={0}
countPerPage={0}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
/>
</Container>
);
}
export default memo(TraceExplorerControls);

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