mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-09-13 23:33:13 +08:00
commit
2ee7817685
6
.github/workflows/build.yaml
vendored
6
.github/workflows/build.yaml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: cd frontend && yarn install
|
||||
- name: Run ESLint
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: |
|
||||
@ -45,7 +45,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Build EE query-service image
|
||||
shell: bash
|
||||
run: |
|
||||
|
8
.github/workflows/codeql.yaml
vendored
8
.github/workflows/codeql.yaml
vendored
@ -39,11 +39,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@ -68,4 +68,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
@ -12,11 +12,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Codebase
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: signoz/gh-bot
|
||||
- name: Use Node v16
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Setup Cache & Install Dependencies
|
||||
|
8
.github/workflows/e2e-k3s.yaml
vendored
8
.github/workflows/e2e-k3s.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
DOCKER_TAG: pull-${{ github.event.number }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build query-service image
|
||||
env:
|
||||
@ -69,12 +69,14 @@ jobs:
|
||||
--restart='OnFailure' -i --rm --command -- curl -X POST -F \
|
||||
'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
|
||||
run: |
|
||||
subdomain="pr-$(git rev-parse --short HEAD)"
|
||||
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
|
||||
env:
|
||||
|
4
.github/workflows/playwright.yaml
vendored
4
.github/workflows/playwright.yaml
vendored
@ -9,8 +9,8 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16.x"
|
||||
- name: Install dependencies
|
||||
|
2
.github/workflows/pr_verify_linked_issue.yml
vendored
2
.github/workflows/pr_verify_linked_issue.yml
vendored
@ -14,6 +14,6 @@ jobs:
|
||||
name: Ensure Pull Request has a linked issue.
|
||||
steps:
|
||||
- name: Verify Linked Issue
|
||||
uses: srikanthccv/verify-linked-issue-action@v0.70
|
||||
uses: srikanthccv/verify-linked-issue-action@v0.71
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
28
.github/workflows/push.yaml
vendored
28
.github/workflows/push.yaml
vendored
@ -14,19 +14,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: benjlevesque/short-sha@v1.2
|
||||
- uses: benjlevesque/short-sha@v2.2
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
@ -49,19 +49,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: benjlevesque/short-sha@v1.2
|
||||
- uses: benjlevesque/short-sha@v2.2
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
@ -84,7 +84,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: yarn install
|
||||
@ -97,15 +97,15 @@ jobs:
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: benjlevesque/short-sha@v1.2
|
||||
- uses: benjlevesque/short-sha@v2.2
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
|
6
.github/workflows/release-drafter.yml
vendored
6
.github/workflows/release-drafter.yml
vendored
@ -12,6 +12,12 @@ on:
|
||||
|
||||
jobs:
|
||||
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
|
||||
steps:
|
||||
# (Optional) GitHub Enterprise requires GHE_HOST variable set
|
||||
|
12
.github/workflows/remove-label.yaml
vendored
12
.github/workflows/remove-label.yaml
vendored
@ -8,9 +8,15 @@ jobs:
|
||||
remove:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remove label
|
||||
uses: buildsville/add-remove-label@v1
|
||||
- name: Remove label ok-to-test from PR
|
||||
uses: buildsville/add-remove-label@v2.0.0
|
||||
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
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
4
.github/workflows/sonar.yml
vendored
4
.github/workflows/sonar.yml
vendored
@ -3,7 +3,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- v*
|
||||
- develop
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
defaults:
|
||||
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Sonar analysis
|
||||
|
15
Makefile
15
Makefile
@ -54,7 +54,7 @@ build-push-frontend:
|
||||
@echo "--> Building and pushing frontend docker image"
|
||||
@echo "------------------"
|
||||
@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) .
|
||||
|
||||
# Steps to build and push docker image of query service
|
||||
@ -73,7 +73,7 @@ build-push-query-service:
|
||||
@echo "------------------"
|
||||
@echo "--> Building and pushing query-service docker image"
|
||||
@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)" \
|
||||
--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 "------------------"
|
||||
@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) .
|
||||
|
||||
dev-setup:
|
||||
@ -136,9 +136,18 @@ clear-swarm-data:
|
||||
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
|
||||
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:
|
||||
go test ./pkg/query-service/app/metrics/...
|
||||
go test ./pkg/query-service/cache/...
|
||||
go test ./pkg/query-service/app/...
|
||||
go test ./pkg/query-service/app/querier/...
|
||||
go test ./pkg/query-service/converter/...
|
||||
go test ./pkg/query-service/formatter/...
|
||||
|
@ -137,7 +137,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.20.2
|
||||
image: signoz/query-service:0.21.0
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -166,7 +166,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.20.2
|
||||
image: signoz/frontend:0.21.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@ -179,7 +179,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
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"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -208,7 +208,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
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"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -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`
|
||||
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"]
|
||||
# user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -67,7 +67,7 @@ services:
|
||||
|
||||
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"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -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`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.20.2}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.21.0}
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
@ -181,7 +181,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.20.2}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.21.0}
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -193,7 +193,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
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"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -219,7 +219,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
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"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -277,7 +277,7 @@ func loggingMiddleware(next http.Handler) http.Handler {
|
||||
path, _ := route.GetPathTemplate()
|
||||
startTime := time.Now()
|
||||
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()
|
||||
startTime := time.Now()
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -3,25 +3,73 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"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/pkg/query-service/auth"
|
||||
"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"
|
||||
"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/zapcore"
|
||||
)
|
||||
|
||||
func initZapLog() *zap.Logger {
|
||||
func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
|
||||
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.TimeKey = "timestamp"
|
||||
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
|
||||
}
|
||||
|
||||
@ -34,12 +82,15 @@ func main() {
|
||||
// the url used to build link in the alert messages in slack and other systems
|
||||
var ruleRepoURL string
|
||||
|
||||
var enableQueryServiceLogOTLPExport bool
|
||||
|
||||
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
||||
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.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
|
||||
flag.Parse()
|
||||
|
||||
loggerMgr := initZapLog()
|
||||
loggerMgr := initZapLog(enableQueryServiceLogOTLPExport)
|
||||
zap.ReplaceGlobals(loggerMgr)
|
||||
defer loggerMgr.Sync() // flushes buffer, if any
|
||||
|
||||
|
56
frontend/CONTRIBUTIONS.md
Normal file
56
frontend/CONTRIBUTIONS.md
Normal 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.
|
11
frontend/public/locales/en-GB/trace.json
Normal file
11
frontend/public/locales/en-GB/trace.json
Normal 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"
|
||||
}
|
||||
}
|
11
frontend/public/locales/en/trace.json
Normal file
11
frontend/public/locales/en/trace.json
Normal 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"
|
||||
}
|
||||
}
|
@ -15,6 +15,11 @@ export const ServiceMapPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
|
||||
);
|
||||
|
||||
export const TracesExplorer = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesExplorer'),
|
||||
);
|
||||
|
||||
export const TraceFilter = Loadable(
|
||||
() => import(/* webpackChunkName: "Trace Filter Page" */ 'pages/Trace'),
|
||||
);
|
||||
@ -101,6 +106,10 @@ export const Logs = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs" */ 'pages/Logs'),
|
||||
);
|
||||
|
||||
export const LogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
|
||||
);
|
||||
|
||||
export const Login = Loadable(
|
||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||
);
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
ListAllALertsPage,
|
||||
Login,
|
||||
Logs,
|
||||
LogsExplorer,
|
||||
MySettings,
|
||||
NewDashboardPage,
|
||||
OrganizationSettings,
|
||||
@ -29,6 +30,7 @@ import {
|
||||
StatusPage,
|
||||
TraceDetail,
|
||||
TraceFilter,
|
||||
TracesExplorer,
|
||||
UnAuthorized,
|
||||
UsageExplorerPage,
|
||||
} from './pageComponents';
|
||||
@ -139,6 +141,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'TRACE',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACES_EXPLORER,
|
||||
exact: true,
|
||||
component: TracesExplorer,
|
||||
isPrivate: true,
|
||||
key: 'TRACES_EXPLORER',
|
||||
},
|
||||
{
|
||||
path: ROUTES.CHANNELS_NEW,
|
||||
exact: true,
|
||||
@ -209,6 +218,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'LOGS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGS_EXPLORER,
|
||||
exact: true,
|
||||
component: LogsExplorer,
|
||||
key: 'LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGIN,
|
||||
exact: true,
|
||||
|
@ -16,7 +16,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
|
||||
return {
|
||||
statusCode,
|
||||
payload: null,
|
||||
error: 'Not Found',
|
||||
error: data.errorType,
|
||||
message: null,
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
// ** Helpers
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
@ -18,6 +19,7 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
BoolOperators,
|
||||
DataSource,
|
||||
LogsAggregatorOperator,
|
||||
MetricAggregateOperator,
|
||||
NumberOperators,
|
||||
PanelTypeKeys,
|
||||
@ -25,6 +27,7 @@ import {
|
||||
QueryBuilderData,
|
||||
ReduceOperators,
|
||||
StringOperators,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@ -100,14 +103,17 @@ export const initialHavingValues: HavingForm = {
|
||||
};
|
||||
|
||||
export const initialAutocompleteData: BaseAutocompleteData = {
|
||||
id: uuid(),
|
||||
id: createIdFromObjectFields(
|
||||
{ dataType: null, key: '', isColumn: null, type: null },
|
||||
baseAutoCompleteIdKeysOrder,
|
||||
),
|
||||
dataType: null,
|
||||
key: '',
|
||||
isColumn: null,
|
||||
type: null,
|
||||
};
|
||||
|
||||
export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
dataSource: DataSource.METRICS,
|
||||
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||
aggregateOperator: MetricAggregateOperator.NOOP,
|
||||
@ -127,6 +133,27 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
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 = {
|
||||
queryName: createNewBuilderItemName({
|
||||
existNames: [],
|
||||
@ -161,17 +188,39 @@ export const initialSingleQueryMap: Record<
|
||||
IClickHouseQuery | IPromQLQuery
|
||||
> = { clickhouse_sql: initialClickHouseData, promql: initialQueryPromQLData };
|
||||
|
||||
export const initialQuery: QueryState = {
|
||||
export const initialQueryState: QueryState = {
|
||||
id: uuid(),
|
||||
builder: initialQueryBuilderData,
|
||||
clickhouse_sql: [initialClickHouseData],
|
||||
promql: [initialQueryPromQLData],
|
||||
};
|
||||
|
||||
export const initialQueryWithType: Query = {
|
||||
...initialQuery,
|
||||
const initialQueryWithType: Query = {
|
||||
...initialQueryState,
|
||||
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[]> = {
|
||||
string: Object.values(StringOperators),
|
||||
number: Object.values(NumberOperators),
|
||||
|
@ -1 +1,2 @@
|
||||
export const COMPOSITE_QUERY = 'compositeQuery';
|
||||
export const PANEL_TYPES_QUERY = 'panelTypes';
|
||||
|
@ -1,3 +1,5 @@
|
||||
export const REACT_QUERY_KEY = {
|
||||
GET_ALL_LICENCES: 'GET_ALL_LICENCES',
|
||||
GET_QUERY_RANGE: 'GET_QUERY_RANGE',
|
||||
GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS',
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ const ROUTES = {
|
||||
SERVICE_MAP: '/service-map',
|
||||
TRACE: '/trace',
|
||||
TRACE_DETAIL: '/trace/:id',
|
||||
TRACES_EXPLORER: '/traces-explorer',
|
||||
SETTINGS: '/settings',
|
||||
INSTRUMENTATION: '/get-started',
|
||||
USAGE_EXPLORER: '/usage-explorer',
|
||||
@ -27,6 +28,7 @@ const ROUTES = {
|
||||
UN_AUTHORIZED: '/un-authorized',
|
||||
NOT_FOUND: '/not-found',
|
||||
LOGS: '/logs',
|
||||
LOGS_EXPLORER: '/logs-explorer',
|
||||
HOME_PAGE: '/',
|
||||
PASSWORD_RESET: '/password-reset',
|
||||
LIST_LICENSES: '/licenses',
|
||||
|
@ -36,8 +36,11 @@ const themeColors = {
|
||||
royalGrey: '#888888',
|
||||
matterhornGrey: '#555555',
|
||||
whiteCream: '#ffffffd5',
|
||||
white: '#ffffff',
|
||||
black: '#000000',
|
||||
lightBlack: '#141414',
|
||||
lightgrey: '#ddd',
|
||||
lightWhite: '#ffffffd9',
|
||||
borderLightGrey: '#d9d9d9',
|
||||
borderDarkGrey: '#424242',
|
||||
};
|
||||
|
7
frontend/src/container/Controls/config.ts
Normal file
7
frontend/src/container/Controls/config.ts
Normal 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',
|
||||
};
|
69
frontend/src/container/Controls/index.tsx
Normal file
69
frontend/src/container/Controls/index.tsx
Normal 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);
|
7
frontend/src/container/Controls/styles.ts
Normal file
7
frontend/src/container/Controls/styles.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
`;
|
@ -1,5 +1,5 @@
|
||||
import {
|
||||
initialQueryBuilderFormValues,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryPromQLData,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
@ -11,11 +11,6 @@ import {
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
DataSource,
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
const defaultAlertDescription =
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})';
|
||||
@ -32,7 +27,7 @@ export const alertDefaults: AlertDef = {
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
A: initialQueryBuilderFormValues,
|
||||
A: initialQueryBuilderFormValuesMap.metrics,
|
||||
},
|
||||
promQueries: { A: initialQueryPromQLData },
|
||||
chQueries: {
|
||||
@ -61,11 +56,7 @@ export const logAlertDefaults: AlertDef = {
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
A: {
|
||||
...initialQueryBuilderFormValues,
|
||||
aggregateOperator: LogsAggregatorOperator.COUNT,
|
||||
dataSource: DataSource.LOGS,
|
||||
},
|
||||
A: initialQueryBuilderFormValuesMap.logs,
|
||||
},
|
||||
promQueries: { A: initialQueryPromQLData },
|
||||
chQueries: {
|
||||
@ -95,11 +86,7 @@ export const traceAlertDefaults: AlertDef = {
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
A: {
|
||||
...initialQueryBuilderFormValues,
|
||||
aggregateOperator: TracesAggregatorOperator.COUNT,
|
||||
dataSource: DataSource.TRACES,
|
||||
},
|
||||
A: initialQueryBuilderFormValuesMap.traces,
|
||||
},
|
||||
promQueries: { A: initialQueryPromQLData },
|
||||
chQueries: {
|
||||
@ -129,11 +116,7 @@ export const exceptionAlertDefaults: AlertDef = {
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
A: {
|
||||
...initialQueryBuilderFormValues,
|
||||
aggregateOperator: TracesAggregatorOperator.COUNT,
|
||||
dataSource: DataSource.TRACES,
|
||||
},
|
||||
A: initialQueryBuilderFormValuesMap.traces,
|
||||
},
|
||||
promQueries: { A: initialQueryPromQLData },
|
||||
chQueries: {
|
||||
|
@ -1,16 +1,11 @@
|
||||
import { Form, Row } from 'antd';
|
||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
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 { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import {
|
||||
alertDefaults,
|
||||
ALERTS_VALUES_MAP,
|
||||
exceptionAlertDefaults,
|
||||
logAlertDefaults,
|
||||
traceAlertDefaults,
|
||||
@ -18,18 +13,12 @@ import {
|
||||
import SelectAlertType from './SelectAlertType';
|
||||
|
||||
function CreateRules(): JSX.Element {
|
||||
const [initValues, setInitValues] = useState<AlertDef>(alertDefaults);
|
||||
const [initValues, setInitValues] = useState<AlertDef | null>(null);
|
||||
const [alertType, setAlertType] = useState<AlertTypes>(
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
|
||||
|
||||
const { redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const onSelectType = (typ: AlertTypes): void => {
|
||||
setAlertType(typ);
|
||||
switch (typ) {
|
||||
@ -45,15 +34,9 @@ function CreateRules(): JSX.Element {
|
||||
default:
|
||||
setInitValues(alertDefaults);
|
||||
}
|
||||
|
||||
const value = ALERTS_VALUES_MAP[typ].condition.compositeQuery;
|
||||
|
||||
const compositeQuery = mapQueryDataFromApi(value);
|
||||
|
||||
redirectWithQueryBuilderData(compositeQuery);
|
||||
};
|
||||
|
||||
if (!compositeQuery) {
|
||||
if (!initValues) {
|
||||
return (
|
||||
<Row wrap={false}>
|
||||
<SelectAlertType onSelect={onSelectType} />
|
||||
|
113
frontend/src/container/ExportPanel/ExportPanel.tsx
Normal file
113
frontend/src/container/ExportPanel/ExportPanel.tsx
Normal 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;
|
9
frontend/src/container/ExportPanel/config.ts
Normal file
9
frontend/src/container/ExportPanel/config.ts
Normal 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',
|
||||
};
|
70
frontend/src/container/ExportPanel/index.tsx
Normal file
70
frontend/src/container/ExportPanel/index.tsx
Normal 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;
|
33
frontend/src/container/ExportPanel/styles.ts
Normal file
33
frontend/src/container/ExportPanel/styles.ts
Normal 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;
|
||||
`;
|
10
frontend/src/container/ExportPanel/utils.ts
Normal file
10
frontend/src/container/ExportPanel/utils.ts
Normal 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,
|
||||
}));
|
@ -1,6 +1,7 @@
|
||||
import { Form, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
|
||||
import ChannelSelect from './ChannelSelect';
|
||||
import LabelSelect from './labels';
|
||||
@ -54,7 +55,15 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
</SeveritySelect>
|
||||
</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
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
@ -97,10 +106,10 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
<FormItemMedium label="Notification Channels">
|
||||
<ChannelSelect
|
||||
currentValue={alertDef.preferredChannels}
|
||||
onSelectChannels={(s: string[]): void => {
|
||||
onSelectChannels={(preferredChannels): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
preferredChannels: s,
|
||||
preferredChannels,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { StaticLineProps } from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridGraphComponent from 'container/GridGraphComponent';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useMemo } from 'react';
|
||||
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 { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
@ -18,7 +17,7 @@ import { ChartContainer, FailedMessageContainer } from './styles';
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
query: Query | undefined;
|
||||
query: Query | null;
|
||||
graphType?: GRAPH_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
selectedInterval?: Time;
|
||||
@ -26,9 +25,6 @@ export interface ChartPreviewProps {
|
||||
threshold?: number | undefined;
|
||||
userQueryKey?: string;
|
||||
}
|
||||
interface QueryResponseError {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
function ChartPreview({
|
||||
name,
|
||||
@ -76,39 +72,30 @@ function ChartPreview({
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
const queryResponse = useQuery({
|
||||
queryKey: [
|
||||
'chartPreview',
|
||||
userQueryKey || JSON.stringify(query),
|
||||
selectedInterval,
|
||||
],
|
||||
queryFn: () =>
|
||||
GetMetricQueryRange({
|
||||
query: query || {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
promql: [],
|
||||
builder: {
|
||||
queryFormulas: [],
|
||||
queryData: [],
|
||||
},
|
||||
clickhouse_sql: [],
|
||||
},
|
||||
globalSelectedInterval: selectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
}),
|
||||
retry: false,
|
||||
enabled: canQuery,
|
||||
});
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
query: query || initialQueriesMap.metrics,
|
||||
globalSelectedInterval: selectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
'chartPreview',
|
||||
userQueryKey || JSON.stringify(query),
|
||||
selectedInterval,
|
||||
],
|
||||
retry: false,
|
||||
enabled: canQuery,
|
||||
},
|
||||
);
|
||||
|
||||
const chartDataSet = queryResponse.isError
|
||||
? null
|
||||
: getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result
|
||||
? queryResponse?.data?.payload?.data?.result
|
||||
: [],
|
||||
queryData: queryResponse?.data?.payload?.data?.result ?? [],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -119,11 +106,12 @@ function ChartPreview({
|
||||
{(queryResponse?.isError || queryResponse?.error) && (
|
||||
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
||||
<InfoCircleOutlined />{' '}
|
||||
{(queryResponse?.error as QueryResponseError).message ||
|
||||
t('preview_chart_unexpected_error')}
|
||||
{queryResponse.error.message || t('preview_chart_unexpected_error')}
|
||||
</FailedMessageContainer>
|
||||
)}
|
||||
{queryResponse.isLoading && <Spinner size="large" tip="Loading..." />}
|
||||
{queryResponse.isLoading && (
|
||||
<Spinner size="large" tip="Loading..." height="70vh" />
|
||||
)}
|
||||
{chartDataSet && !queryResponse.isError && (
|
||||
<GridGraphComponent
|
||||
title={name}
|
||||
|
@ -4,8 +4,11 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import ChQuerySection from './ChQuerySection';
|
||||
import PromqlSection from './PromqlSection';
|
||||
@ -20,8 +23,14 @@ function QuerySection({
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const handleQueryCategoryChange = (queryType: string): void => {
|
||||
setQueryCategory(queryType as EQueryType);
|
||||
featureResponse.refetch().then(() => {
|
||||
setQueryCategory(queryType as EQueryType);
|
||||
});
|
||||
};
|
||||
|
||||
const renderPromqlUI = (): JSX.Element => <PromqlSection />;
|
||||
@ -38,10 +47,6 @@ function QuerySection({
|
||||
/>
|
||||
);
|
||||
|
||||
const handleRunQuery = (): void => {
|
||||
runQuery();
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: t('tab_qb'),
|
||||
@ -76,7 +81,7 @@ function QuerySection({
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Button type="primary" onClick={handleRunQuery}>
|
||||
<Button type="primary" onClick={runQuery}>
|
||||
Run Query
|
||||
</Button>
|
||||
</span>
|
||||
@ -95,7 +100,7 @@ function QuerySection({
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Button type="primary" onClick={handleRunQuery}>
|
||||
<Button type="primary" onClick={runQuery}>
|
||||
Run Query
|
||||
</Button>
|
||||
</span>
|
||||
@ -132,7 +137,7 @@ interface QuerySectionProps {
|
||||
queryCategory: EQueryType;
|
||||
setQueryCategory: (n: EQueryType) => void;
|
||||
alertType: AlertTypes;
|
||||
runQuery: () => void;
|
||||
runQuery: VoidFunction;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
|
@ -140,12 +140,14 @@ function RuleOptions({
|
||||
{queryCategory === EQueryType.PROM
|
||||
? renderPromRuleOptions()
|
||||
: renderThresholdRuleOpts()}
|
||||
<InputNumber
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
/>
|
||||
<Form.Item name={['condition', 'target']}>
|
||||
<InputNumber
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -48,7 +48,12 @@ function FormAlertRules({
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const {
|
||||
currentQuery,
|
||||
stagedQuery,
|
||||
handleRunQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
// use query client
|
||||
const ruleCache = useQueryClient();
|
||||
@ -65,35 +70,14 @@ function FormAlertRules({
|
||||
|
||||
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
||||
|
||||
// manualStagedQuery requires manual staging of query
|
||||
// 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 });
|
||||
useShareBuilderUrl({ defaultValue: sq });
|
||||
|
||||
useEffect(() => {
|
||||
if (compositeQuery && !manualStagedQuery) {
|
||||
setManualStagedQuery(compositeQuery);
|
||||
}
|
||||
setAlertDef(initialValue);
|
||||
}, [
|
||||
initialValue,
|
||||
initQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
currentQuery,
|
||||
manualStagedQuery,
|
||||
compositeQuery,
|
||||
]);
|
||||
}, [initialValue]);
|
||||
|
||||
const onRunQuery = (): void => {
|
||||
setManualStagedQuery(currentQuery);
|
||||
redirectWithQueryBuilderData(currentQuery);
|
||||
handleRunQuery();
|
||||
};
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
@ -115,8 +99,6 @@ function FormAlertRules({
|
||||
}
|
||||
const query: Query = { ...currentQuery, queryType: val };
|
||||
|
||||
setManualStagedQuery(query);
|
||||
|
||||
redirectWithQueryBuilderData(query);
|
||||
};
|
||||
const { notifications } = useNotifications();
|
||||
@ -201,10 +183,6 @@ function FormAlertRules({
|
||||
|
||||
const isFormValid = useCallback((): boolean => {
|
||||
if (!alertDef.alert || alertDef.alert === '') {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('alertname_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -217,14 +195,7 @@ function FormAlertRules({
|
||||
}
|
||||
|
||||
return validateQBParams();
|
||||
}, [
|
||||
t,
|
||||
validateQBParams,
|
||||
validateChQueryParams,
|
||||
alertDef,
|
||||
validatePromParams,
|
||||
notifications,
|
||||
]);
|
||||
}, [validateQBParams, validateChQueryParams, alertDef, validatePromParams]);
|
||||
|
||||
const preparePostData = (): AlertDef => {
|
||||
const postableAlert: AlertDef = {
|
||||
@ -328,9 +299,7 @@ function FormAlertRules({
|
||||
title: t('confirm_save_title'),
|
||||
centered: true,
|
||||
content,
|
||||
onOk() {
|
||||
saveRule();
|
||||
},
|
||||
onOk: saveRule,
|
||||
});
|
||||
}, [t, saveRule, currentQuery]);
|
||||
|
||||
@ -381,7 +350,7 @@ function FormAlertRules({
|
||||
headline={<PlotTag queryType={currentQuery.queryType} />}
|
||||
name=""
|
||||
threshold={alertDef.condition?.target}
|
||||
query={manualStagedQuery}
|
||||
query={stagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
@ -391,7 +360,7 @@ function FormAlertRules({
|
||||
headline={<PlotTag queryType={currentQuery.queryType} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={manualStagedQuery}
|
||||
query={stagedQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -400,23 +369,25 @@ function FormAlertRules({
|
||||
headline={<PlotTag queryType={currentQuery.queryType} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={manualStagedQuery}
|
||||
query={stagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const isAlertNameMissing = !formInstance.getFieldValue('alert');
|
||||
|
||||
const isAlertAvialableToSave =
|
||||
isAlertAvialable &&
|
||||
isNewRule &&
|
||||
currentQuery.queryType === EQueryType.QUERY_BUILDER;
|
||||
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||
alertType !== AlertTypes.METRICS_BASED_ALERT;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
<PanelContainer>
|
||||
<StyledLeftContainer flex="5 1 600px">
|
||||
<StyledLeftContainer flex="5 1 600px" md={18}>
|
||||
<MainFormContainer
|
||||
initialValues={initialValue}
|
||||
layout="vertical"
|
||||
@ -448,7 +419,7 @@ function FormAlertRules({
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
disabled={isAlertAvialableToSave}
|
||||
disabled={isAlertNameMissing || isAlertAvialableToSave}
|
||||
>
|
||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||
</ActionButton>
|
||||
|
@ -84,8 +84,8 @@ function LabelSelect({
|
||||
handleBlur();
|
||||
}, [handleBlur]);
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setCurrentVal(e.target?.value);
|
||||
const handleLabelChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
setCurrentVal(event.target?.value.replace(':', ''));
|
||||
};
|
||||
|
||||
const handleClose = (key: string): void => {
|
||||
@ -133,9 +133,9 @@ function LabelSelect({
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<Input
|
||||
placeholder={renderPlaceholder()}
|
||||
onChange={handleChange}
|
||||
onChange={handleLabelChange}
|
||||
onKeyUp={(e): void => {
|
||||
if (e.key === 'Enter' || e.code === 'Enter') {
|
||||
if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
|
||||
send('NEXT');
|
||||
}
|
||||
}}
|
||||
|
@ -7,16 +7,13 @@ import {
|
||||
timeItems,
|
||||
timePreferance,
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { TimeContainer } from './styles';
|
||||
@ -44,18 +41,24 @@ function FullView({
|
||||
name: getSelectedTime()?.name || '',
|
||||
enum: widget?.timePreferance || 'GLOBAL_TIME',
|
||||
});
|
||||
const response = useQuery<
|
||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||
>(
|
||||
`FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
|
||||
|
||||
const queryKey = useMemo(
|
||||
() =>
|
||||
GetMetricQueryRange({
|
||||
selectedTime: selectedTime.enum,
|
||||
graphType: widget.panelTypes,
|
||||
query: widget.query,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(),
|
||||
}),
|
||||
`FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
|
||||
[selectedTime, globalSelectedTime, widget],
|
||||
);
|
||||
|
||||
const response = useGetQueryRange(
|
||||
{
|
||||
selectedTime: selectedTime.enum,
|
||||
graphType: widget.panelTypes,
|
||||
query: widget.query,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(),
|
||||
},
|
||||
{
|
||||
queryKey,
|
||||
},
|
||||
);
|
||||
|
||||
const chartDataSet = useMemo(
|
||||
|
@ -3,6 +3,7 @@ import { ChartData } from 'chart.js';
|
||||
import Spinner from 'components/Spinner';
|
||||
import GridGraphComponent from 'container/GridGraphComponent';
|
||||
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import usePreviousValue from 'hooks/usePreviousValue';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
@ -20,7 +21,6 @@ import {
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useQuery } from 'react-query';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
@ -28,7 +28,6 @@ import {
|
||||
DeleteWidget,
|
||||
DeleteWidgetProps,
|
||||
} from 'store/actions/dashboard/deleteWidget';
|
||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
@ -55,7 +54,7 @@ function GridCardGraph({
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
initialInView: true,
|
||||
initialInView: false,
|
||||
});
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
@ -81,33 +80,28 @@ function GridCardGraph({
|
||||
const selectedData = selectedDashboard?.data;
|
||||
const { variables } = selectedData;
|
||||
|
||||
const queryResponse = useQuery(
|
||||
[
|
||||
`GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget.id}`,
|
||||
{
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
selectedTime: widget?.timePreferance,
|
||||
graphType: widget?.panelTypes,
|
||||
query: widget?.query,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(),
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
`GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget?.id}`,
|
||||
widget,
|
||||
maxTime,
|
||||
minTime,
|
||||
globalSelectedInterval,
|
||||
variables,
|
||||
},
|
||||
],
|
||||
() =>
|
||||
GetMetricQueryRange({
|
||||
selectedTime: widget?.timePreferance,
|
||||
graphType: widget.panelTypes,
|
||||
query: widget.query,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(),
|
||||
}),
|
||||
{
|
||||
],
|
||||
keepPreviousData: true,
|
||||
enabled: isGraphVisible,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
setErrorMessage(error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -179,7 +173,7 @@ function GridCardGraph({
|
||||
{
|
||||
data: selectedDashboard.data,
|
||||
generateWidgetId: uuid,
|
||||
graphType: widget.panelTypes,
|
||||
graphType: widget?.panelTypes,
|
||||
selectedDashboard,
|
||||
layout,
|
||||
widgetData: widget,
|
||||
@ -193,7 +187,7 @@ function GridCardGraph({
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(
|
||||
`${history.location.pathname}/new?graphType=${widget.panelTypes}&widgetId=${uuid}`,
|
||||
`${history.location.pathname}/new?graphType=${widget?.panelTypes}&widgetId=${uuid}`,
|
||||
);
|
||||
}, 1500);
|
||||
});
|
||||
@ -259,10 +253,10 @@ function GridCardGraph({
|
||||
/>
|
||||
</div>
|
||||
<GridGraphComponent
|
||||
GRAPH_TYPES={widget.panelTypes}
|
||||
GRAPH_TYPES={widget?.panelTypes}
|
||||
data={prevChartDataSetRef}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
isStacked={widget?.isStacked}
|
||||
opacity={widget?.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
|
@ -126,7 +126,7 @@ function WidgetHeader({
|
||||
{
|
||||
key: keyMethodMapping.clone.key,
|
||||
icon: <CopyOutlined />,
|
||||
disabled: false,
|
||||
disabled: !editWidget,
|
||||
label: 'Clone',
|
||||
},
|
||||
{
|
||||
|
@ -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>(
|
||||
(state) => state.app,
|
||||
|
@ -1,15 +1,10 @@
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import updateDashboardApi from 'api/dashboard/update';
|
||||
import {
|
||||
initialClickHouseData,
|
||||
initialQueryBuilderFormValues,
|
||||
initialQueryPromQLData,
|
||||
} from 'constants/queryBuilder';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import store from 'store';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const UpdateDashboard = async (
|
||||
{
|
||||
@ -41,23 +36,7 @@ export const UpdateDashboard = async (
|
||||
nullZeroValues: widgetData?.nullZeroValues || '',
|
||||
opacity: '',
|
||||
panelTypes: graphType,
|
||||
query: widgetData?.query || {
|
||||
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,
|
||||
},
|
||||
query: widgetData?.query || initialQueriesMap.metrics,
|
||||
timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME',
|
||||
title: widgetData ? copyTitle : '',
|
||||
},
|
||||
|
@ -91,7 +91,7 @@ function HeaderContainer(): JSX.Element {
|
||||
|
||||
const onClickSignozCloud = (): void => {
|
||||
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',
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
@ -67,7 +68,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
.catch(handleError);
|
||||
}, [featureResponse, handleError]);
|
||||
|
||||
const onEditHandler = (record: GettableAlert): void => {
|
||||
const onEditHandler = (record: GettableAlert) => (): void => {
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
@ -84,6 +85,44 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
.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> = [
|
||||
{
|
||||
title: 'Status',
|
||||
@ -107,9 +146,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
return 0;
|
||||
},
|
||||
render: (value, record): JSX.Element => (
|
||||
<Typography.Link onClick={(): void => onEditHandler(record)}>
|
||||
{value}
|
||||
</Typography.Link>
|
||||
<Typography.Link onClick={onEditHandler(record)}>{value}</Typography.Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -165,9 +202,12 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
<>
|
||||
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
|
||||
|
||||
<ColumnButton onClick={(): void => onEditHandler(record)} type="link">
|
||||
<ColumnButton onClick={onEditHandler(record)} type="link">
|
||||
Edit
|
||||
</ColumnButton>
|
||||
<ColumnButton onClick={onCloneHandler(record)} type="link">
|
||||
Clone
|
||||
</ColumnButton>
|
||||
|
||||
<DeleteAlert notifications={notificationsApi} setData={setData} id={id} />
|
||||
</>
|
||||
|
@ -71,23 +71,8 @@ function ImportJSON({
|
||||
setDashboardCreating(true);
|
||||
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({
|
||||
...parsedWidgets,
|
||||
...dashboardData,
|
||||
uploadedGrafana,
|
||||
});
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
|
@ -1,16 +1,11 @@
|
||||
import {
|
||||
CloudDownloadOutlined,
|
||||
FastBackwardOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Divider, Dropdown, MenuProps, Select } from 'antd';
|
||||
import { CloudDownloadOutlined, FastBackwardOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Dropdown, MenuProps } from 'antd';
|
||||
import { Excel } from 'antd-table-saveas-excel';
|
||||
import Controls from 'container/Controls';
|
||||
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { defaultSelectStyle } from 'pages/Logs/config';
|
||||
import * as Papa from 'papaparse';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@ -26,7 +21,6 @@ import {
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { ITEMS_PER_PAGE_OPTIONS } from './config';
|
||||
import { Container, DownloadLogButton } from './styles';
|
||||
|
||||
function LogControls(): JSX.Element | null {
|
||||
@ -149,15 +143,6 @@ function LogControls(): JSX.Element | null {
|
||||
|
||||
const isLoading = isLogsLoading || isLoadingAggregate;
|
||||
|
||||
const isNextAndPreviousDisabled = useMemo(
|
||||
() =>
|
||||
isLoading ||
|
||||
logLinesPerPage === 0 ||
|
||||
logs.length === 0 ||
|
||||
logs.length < logLinesPerPage,
|
||||
[isLoading, logLinesPerPage, logs.length],
|
||||
);
|
||||
|
||||
if (liveTail !== 'STOPPED') {
|
||||
return null;
|
||||
}
|
||||
@ -179,37 +164,14 @@ function LogControls(): JSX.Element | null {
|
||||
<FastBackwardOutlined /> Go to latest
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<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={logLinesPerPage}
|
||||
onChange={handleLogLinesPerPageChange}
|
||||
>
|
||||
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
|
||||
<Select.Option
|
||||
key={count}
|
||||
value={count}
|
||||
>{`${count} / page`}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Controls
|
||||
isLoading={isLoading}
|
||||
count={logs.length}
|
||||
countPerPage={logLinesPerPage}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
handleCountItemsPerPageChange={handleLogLinesPerPageChange}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const flattenLogData: Record<string, any> | null = useMemo(
|
||||
const flattenLogData: Record<string, string> | null = useMemo(
|
||||
() => (logData ? flattenObject(logData) : null),
|
||||
[logData],
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
@ -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>
|
||||
);
|
||||
}
|
1
frontend/src/container/LogsExplorerChart/index.ts
Normal file
1
frontend/src/container/LogsExplorerChart/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { LogsExplorerChart } from './LogsExplorerChart';
|
@ -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};
|
||||
}
|
||||
`;
|
@ -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>
|
||||
);
|
||||
}
|
1
frontend/src/container/LogsExplorerViews/index.ts
Normal file
1
frontend/src/container/LogsExplorerViews/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { LogsExplorerViews } from './LogsExplorerViews';
|
@ -6,7 +6,7 @@ import {
|
||||
QueryOperatorsMultiVal,
|
||||
QueryOperatorsSingleVal,
|
||||
} from 'lib/logql/tokens';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
@ -56,6 +56,8 @@ function QueryField({
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: QueryFieldProps): JSX.Element | null {
|
||||
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
|
||||
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
@ -136,9 +138,12 @@ function QueryField({
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
open={isDropDownOpen}
|
||||
onChange={(e): void => handleChange(2, e as never)}
|
||||
defaultValue={(query[2] && query[2].value) || []}
|
||||
notFoundContent={null}
|
||||
onInputKeyDown={(): void => setIsDropDownOpen(true)}
|
||||
onSelect={(): void => setIsDropDownOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
|
@ -10,12 +10,6 @@ export const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => ({
|
||||
opacity: '0',
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
query,
|
||||
queryData: {
|
||||
data: { queryData: [] },
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
},
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
title: '',
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {
|
||||
initialFormulaBuilderFormValues,
|
||||
initialQueryBuilderFormValues,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
} from 'constants/queryBuilder';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@ -18,7 +18,7 @@ export const getQueryBuilderQueries = ({
|
||||
queryFormulas: [],
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
...initialQueryBuilderFormValuesMap.metrics,
|
||||
aggregateOperator: MetricAggregateOperator.SUM_RATE,
|
||||
disabled: false,
|
||||
groupBy,
|
||||
@ -53,7 +53,7 @@ export const getQueryBuilderQuerieswithFormula = ({
|
||||
],
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
...initialQueryBuilderFormValuesMap.metrics,
|
||||
aggregateOperator: MetricAggregateOperator.SUM_RATE,
|
||||
disabled,
|
||||
groupBy,
|
||||
@ -66,7 +66,7 @@ export const getQueryBuilderQuerieswithFormula = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
...initialQueryBuilderFormValuesMap.metrics,
|
||||
aggregateOperator: MetricAggregateOperator.SUM_RATE,
|
||||
disabled,
|
||||
groupBy,
|
||||
|
@ -14,6 +14,7 @@ import { useParams } from 'react-router-dom';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||
import { Button } from './styles';
|
||||
@ -56,6 +57,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
@ -69,6 +71,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
|
@ -15,6 +15,7 @@ import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||
import { legend } from './constant';
|
||||
@ -48,6 +49,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
@ -67,6 +69,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
@ -82,6 +85,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
@ -97,6 +101,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
tagFilterItems,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
);
|
||||
|
@ -21,6 +21,7 @@ import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import MetricReducer from 'types/reducer/metrics';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
errorPercentage,
|
||||
@ -91,6 +92,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
||||
topLevelOperations,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems],
|
||||
);
|
||||
@ -106,6 +108,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
||||
topLevelOperations,
|
||||
}),
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
|
||||
);
|
||||
|
@ -11,6 +11,8 @@ import { useParams } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { getErrorRate } from './utils';
|
||||
|
||||
function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
@ -89,10 +91,10 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
|
||||
dataIndex: 'errorCount',
|
||||
key: 'errorCount',
|
||||
width: 50,
|
||||
sorter: (a: TopOperationList, b: TopOperationList): number =>
|
||||
a.errorCount - b.errorCount,
|
||||
render: (value: number, record: TopOperationList): string =>
|
||||
`${((value / record.numCalls) * 100).toFixed(2)} %`,
|
||||
sorter: (first: TopOperationList, second: TopOperationList): number =>
|
||||
getErrorRate(first) - getErrorRate(second),
|
||||
render: (_, record: TopOperationList): string =>
|
||||
`${getErrorRate(record).toFixed(2)} %`,
|
||||
},
|
||||
];
|
||||
|
||||
|
4
frontend/src/container/MetricsApplication/utils.ts
Normal file
4
frontend/src/container/MetricsApplication/utils.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { TopOperationList } from './TopOperationsTable';
|
||||
|
||||
export const getErrorRate = (list: TopOperationList): number =>
|
||||
(list.errorCount / list.numCalls) * 100;
|
@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { initialQueryWithType } from 'constants/queryBuilder';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@ -47,7 +47,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
|
||||
history.push(
|
||||
`${history.location.pathname}/new?graphType=${name}&widgetId=${
|
||||
emptyLayout.i
|
||||
}&${COMPOSITE_QUERY}=${JSON.stringify(initialQueryWithType)}`,
|
||||
}&${COMPOSITE_QUERY}=${JSON.stringify(initialQueriesMap.metrics)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
|
@ -10,6 +10,7 @@ import { UpdateDashboardVariables } from 'store/actions/dashboard/updatedDashboa
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
|
||||
import VariableItem from './VariableItem';
|
||||
@ -29,6 +30,8 @@ function DashboardVariableSelection({
|
||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const onVarChanged = (name: string): void => {
|
||||
setLastUpdatedVar(name);
|
||||
setUpdate(!update);
|
||||
@ -36,19 +39,15 @@ function DashboardVariableSelection({
|
||||
|
||||
const onValueUpdate = (
|
||||
name: string,
|
||||
value:
|
||||
| string
|
||||
| string[]
|
||||
| number
|
||||
| number[]
|
||||
| boolean
|
||||
| boolean[]
|
||||
| null
|
||||
| undefined,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
): void => {
|
||||
const updatedVariablesData = { ...variables };
|
||||
updatedVariablesData[name].selectedValue = value;
|
||||
updateDashboardVariables(updatedVariablesData, notifications);
|
||||
|
||||
if (role !== 'VIEWER') {
|
||||
updateDashboardVariables(updatedVariablesData, notifications);
|
||||
}
|
||||
|
||||
onVarChanged(name);
|
||||
};
|
||||
const onAllSelectedUpdate = (
|
||||
@ -57,7 +56,10 @@ function DashboardVariableSelection({
|
||||
): void => {
|
||||
const updatedVariablesData = { ...variables };
|
||||
updatedVariablesData[name].allSelected = value;
|
||||
updateDashboardVariables(updatedVariablesData, notifications);
|
||||
|
||||
if (role !== 'VIEWER') {
|
||||
updateDashboardVariables(updatedVariablesData, notifications);
|
||||
}
|
||||
onVarChanged(name);
|
||||
};
|
||||
|
||||
|
@ -6,28 +6,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { cleardQueryData, downloadObjectAsJson } from './util';
|
||||
import { downloadObjectAsJson } from './util';
|
||||
|
||||
function ShareModal({
|
||||
isJSONModalVisible,
|
||||
onToggleHandler,
|
||||
selectedData,
|
||||
}: ShareModalProps): JSX.Element {
|
||||
const getParsedValue = (): string => {
|
||||
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 getParsedValue = (): string => JSON.stringify(selectedData, null, 2);
|
||||
|
||||
const [jsonValue, setJSONValue] = useState<string>(getParsedValue());
|
||||
const [isViewJSON, setIsViewJSON] = useState<boolean>(false);
|
||||
@ -53,7 +39,6 @@ function ShareModal({
|
||||
}
|
||||
}, [state.error, state.value, t, notifications]);
|
||||
|
||||
const selectedDataCleaned = cleardQueryData(selectedData);
|
||||
const GetFooterComponent = useMemo(() => {
|
||||
if (!isViewJSON) {
|
||||
return (
|
||||
@ -69,7 +54,7 @@ function ShareModal({
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(selectedDataCleaned, selectedData.title);
|
||||
downloadObjectAsJson(selectedData, selectedData.title);
|
||||
}}
|
||||
>
|
||||
{t('download_json')}
|
||||
@ -82,7 +67,7 @@ function ShareModal({
|
||||
{t('copy_to_clipboard')}
|
||||
</Button>
|
||||
);
|
||||
}, [isViewJSON, jsonValue, selectedData, selectedDataCleaned, setCopy, t]);
|
||||
}, [isViewJSON, jsonValue, selectedData, setCopy, t]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function downloadObjectAsJson(
|
||||
exportObj: unknown,
|
||||
exportName: string,
|
||||
@ -14,18 +12,3 @@ export function downloadObjectAsJson(
|
||||
downloadAnchorNode.click();
|
||||
downloadAnchorNode.remove();
|
||||
}
|
||||
|
||||
export function cleardQueryData(param: DashboardData): DashboardData {
|
||||
return {
|
||||
...param,
|
||||
widgets: param.widgets?.map((widget) => ({
|
||||
...widget,
|
||||
queryData: {
|
||||
...widget.queryData,
|
||||
data: {
|
||||
queryData: [],
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Button, Tabs, Typography } from 'antd';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { WidgetGraphProps } from 'container/NewWidget/types';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
@ -18,21 +20,31 @@ import AppActions from 'types/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
|
||||
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
||||
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 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<
|
||||
AppState,
|
||||
DashboardReducer
|
||||
>((state) => state.dashboards);
|
||||
const getWidgetQueryRange = useGetWidgetQueryRange({
|
||||
graphType: selectedGraph,
|
||||
selectedTime: selectedTime.enum,
|
||||
});
|
||||
|
||||
const [selectedDashboards] = dashboards;
|
||||
const { widgets } = selectedDashboards.data;
|
||||
@ -46,23 +58,11 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
|
||||
|
||||
const { query } = selectedWidget;
|
||||
|
||||
const { compositeQuery } = useShareBuilderUrl({ defaultValue: query });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInit && compositeQuery) {
|
||||
setIsInit(true);
|
||||
updateQuery({
|
||||
updatedQuery: compositeQuery,
|
||||
widgetId: urlQuery.get('widgetId') || '',
|
||||
yAxisUnit: selectedWidget.yAxisUnit,
|
||||
});
|
||||
}
|
||||
}, [isInit, compositeQuery, selectedWidget, urlQuery, updateQuery]);
|
||||
useShareBuilderUrl({ defaultValue: query });
|
||||
|
||||
const handleStageQuery = useCallback(
|
||||
(updatedQuery: Query): void => {
|
||||
updateQuery({
|
||||
updatedQuery,
|
||||
widgetId: urlQuery.get('widgetId') || '',
|
||||
yAxisUnit: selectedWidget.yAxisUnit,
|
||||
});
|
||||
@ -76,7 +76,9 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
|
||||
const handleQueryCategoryChange = (qCategory: string): void => {
|
||||
const currentQueryType = qCategory as EQueryType;
|
||||
|
||||
handleStageQuery({ ...currentQuery, queryType: currentQueryType });
|
||||
featureResponse.refetch().then(() => {
|
||||
handleStageQuery({ ...currentQuery, queryType: currentQueryType });
|
||||
});
|
||||
};
|
||||
|
||||
const handleRunQuery = (): void => {
|
||||
@ -115,7 +117,7 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element {
|
||||
<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" />
|
||||
<Button
|
||||
loading={isLoadingQueryResult}
|
||||
loading={getWidgetQueryRange.isFetching}
|
||||
type="primary"
|
||||
onClick={handleRunQuery}
|
||||
>
|
||||
@ -142,6 +144,7 @@ const mapDispatchToProps = (
|
||||
|
||||
interface QueryProps extends DispatchProps {
|
||||
selectedGraph: GRAPH_TYPES;
|
||||
selectedTime: WidgetGraphProps['selectedTime'];
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(QuerySection);
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Card, Typography } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
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 { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@ -12,6 +14,7 @@ import { NotFoundContainer } from './styles';
|
||||
function WidgetGraph({
|
||||
selectedGraph,
|
||||
yAxisUnit,
|
||||
selectedTime,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
@ -27,20 +30,28 @@ function WidgetGraph({
|
||||
|
||||
const selectedWidget = widgets.find((e) => e.id === widgetId);
|
||||
|
||||
const getWidgetQueryRange = useGetWidgetQueryRange({
|
||||
graphType: selectedGraph,
|
||||
selectedTime: selectedTime.enum,
|
||||
});
|
||||
|
||||
if (selectedWidget === undefined) {
|
||||
return <Card>Invalid widget</Card>;
|
||||
}
|
||||
|
||||
const { queryData, title, opacity, isStacked } = selectedWidget;
|
||||
const { title, opacity, isStacked } = selectedWidget;
|
||||
|
||||
if (queryData.error) {
|
||||
if (getWidgetQueryRange.error) {
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
<Typography>{queryData.errorMessage}</Typography>
|
||||
<Typography>{getWidgetQueryRange.error.message}</Typography>
|
||||
</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 (
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
@ -49,7 +60,9 @@ function WidgetGraph({
|
||||
}
|
||||
|
||||
const chartDataSet = getChartData({
|
||||
queryData: [queryData.data],
|
||||
queryData: [
|
||||
{ queryData: getWidgetQueryRange.data?.payload.data.result ?? [] },
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
@ -65,6 +78,4 @@ function WidgetGraph({
|
||||
);
|
||||
}
|
||||
|
||||
type WidgetGraphProps = NewWidgetProps;
|
||||
|
||||
export default WidgetGraph;
|
||||
|
@ -1,24 +1,28 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import { Card } from 'container/GridGraphLayout/styles';
|
||||
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
|
||||
import { NewWidgetProps } from '../../index';
|
||||
import { WidgetGraphProps } from '../../types';
|
||||
import PlotTag from './PlotTag';
|
||||
import { AlertIconContainer, Container, NotFoundContainer } from './styles';
|
||||
import { AlertIconContainer, Container } from './styles';
|
||||
import WidgetGraphComponent from './WidgetGraph';
|
||||
|
||||
function WidgetGraph({
|
||||
selectedGraph,
|
||||
yAxisUnit,
|
||||
selectedTime,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { dashboards, isQueryFired } = useSelector<AppState, DashboardReducer>(
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
|
||||
const [selectedDashboard] = dashboards;
|
||||
const { search } = useLocation();
|
||||
|
||||
@ -31,33 +35,31 @@ function WidgetGraph({
|
||||
|
||||
const selectedWidget = widgets.find((e) => e.id === widgetId);
|
||||
|
||||
const getWidgetQueryRange = useGetWidgetQueryRange({
|
||||
graphType: selectedGraph,
|
||||
selectedTime: selectedTime.enum,
|
||||
});
|
||||
|
||||
if (selectedWidget === undefined) {
|
||||
return <Card>Invalid widget</Card>;
|
||||
}
|
||||
|
||||
const { queryData } = selectedWidget;
|
||||
return (
|
||||
<Container>
|
||||
<PlotTag queryType={selectedWidget.query.queryType} />
|
||||
{queryData.error && (
|
||||
<AlertIconContainer color="red" title={queryData.errorMessage}>
|
||||
<PlotTag queryType={currentQuery.queryType} />
|
||||
{getWidgetQueryRange.error && (
|
||||
<AlertIconContainer color="red" title={getWidgetQueryRange.error.message}>
|
||||
<InfoCircleOutlined />
|
||||
</AlertIconContainer>
|
||||
)}
|
||||
|
||||
{!isQueryFired && (
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
</NotFoundContainer>
|
||||
)}
|
||||
|
||||
{isQueryFired && (
|
||||
<WidgetGraphComponent selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} />
|
||||
)}
|
||||
<WidgetGraphComponent
|
||||
selectedTime={selectedTime}
|
||||
selectedGraph={selectedGraph}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
type WidgetGraphProps = NewWidgetProps;
|
||||
|
||||
export default memo(WidgetGraph);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { NewWidgetProps } from '../index';
|
||||
import { WidgetGraphProps } from '../types';
|
||||
import QuerySection from './QuerySection';
|
||||
import { QueryContainer } from './styles';
|
||||
import WidgetGraph from './WidgetGraph';
|
||||
@ -8,12 +8,17 @@ import WidgetGraph from './WidgetGraph';
|
||||
function LeftContainer({
|
||||
selectedGraph,
|
||||
yAxisUnit,
|
||||
}: NewWidgetProps): JSX.Element {
|
||||
selectedTime,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<WidgetGraph selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} />
|
||||
<WidgetGraph
|
||||
selectedTime={selectedTime}
|
||||
selectedGraph={selectedGraph}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
<QueryContainer>
|
||||
<QuerySection selectedGraph={selectedGraph} />
|
||||
<QuerySection selectedTime={selectedTime} selectedGraph={selectedGraph} />
|
||||
</QueryContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -1,25 +1,18 @@
|
||||
import { LockFilled } from '@ant-design/icons';
|
||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import history from 'lib/history';
|
||||
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 { generatePath, useLocation, useParams } from 'react-router-dom';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import {
|
||||
GetQueryResults,
|
||||
GetQueryResultsProps,
|
||||
} from 'store/actions/dashboard/getQueryResults';
|
||||
import {
|
||||
SaveDashboard,
|
||||
SaveDashboardProps,
|
||||
@ -27,10 +20,10 @@ import {
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
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 DashboardReducer from 'types/reducer/dashboards';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import LeftContainer from './LeftContainer';
|
||||
import QueryTypeTag from './LeftContainer/QueryTypeTag';
|
||||
@ -43,21 +36,15 @@ import {
|
||||
PanelContainer,
|
||||
RightContainerWrapper,
|
||||
} from './styles';
|
||||
import { NewWidgetProps } from './types';
|
||||
|
||||
function NewWidget({
|
||||
selectedGraph,
|
||||
saveSettingOfPanel,
|
||||
getQueryResults,
|
||||
}: Props): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
function NewWidget({ selectedGraph, saveSettingOfPanel }: Props): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
@ -161,27 +148,6 @@ function NewWidget({
|
||||
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
||||
}, [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 params = new URLSearchParams(search);
|
||||
params.set('graphType', type);
|
||||
@ -189,10 +155,6 @@ function NewWidget({
|
||||
setGraphType(type);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getQueryResult();
|
||||
}, [getQueryResult]);
|
||||
|
||||
const onSaveDashboard = useCallback((): void => {
|
||||
setSaveModal(true);
|
||||
}, []);
|
||||
@ -201,15 +163,53 @@ function NewWidget({
|
||||
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 (
|
||||
<Container>
|
||||
<ButtonContainer>
|
||||
{isQueryBuilderActive && (
|
||||
{isSaveDisabled && (
|
||||
<Tooltip title={MESSAGE.PANEL}>
|
||||
<Button
|
||||
icon={<LockFilled />}
|
||||
type="primary"
|
||||
disabled={isQueryBuilderActive}
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
>
|
||||
Save
|
||||
@ -217,12 +217,8 @@ function NewWidget({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isQueryBuilderActive && (
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={isQueryBuilderActive}
|
||||
onClick={onSaveDashboard}
|
||||
>
|
||||
{!isSaveDisabled && (
|
||||
<Button type="primary" disabled={isSaveDisabled} onClick={onSaveDashboard}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
@ -231,7 +227,11 @@ function NewWidget({
|
||||
|
||||
<PanelContainer>
|
||||
<LeftContainerWrapper flex={5}>
|
||||
<LeftContainer selectedGraph={graphType} yAxisUnit={yAxisUnit} />
|
||||
<LeftContainer
|
||||
selectedTime={selectedTime}
|
||||
selectedGraph={graphType}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</LeftContainerWrapper>
|
||||
|
||||
<RightContainerWrapper flex={1}>
|
||||
@ -270,34 +270,24 @@ function NewWidget({
|
||||
width={600}
|
||||
>
|
||||
<Typography>
|
||||
Your graph built with{' '}
|
||||
<QueryTypeTag queryType={selectedWidget?.query.queryType} /> query will be
|
||||
saved. Press OK to confirm.
|
||||
Your graph built with <QueryTypeTag queryType={currentQuery.queryType} />{' '}
|
||||
query will be saved. Press OK to confirm.
|
||||
</Typography>
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export interface NewWidgetProps {
|
||||
selectedGraph: GRAPH_TYPES;
|
||||
yAxisUnit: Widgets['yAxisUnit'];
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
saveSettingOfPanel: (
|
||||
props: SaveDashboardProps,
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
getQueryResults: (
|
||||
props: GetQueryResultsProps,
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch),
|
||||
getQueryResults: bindActionCreators(GetQueryResults, dispatch),
|
||||
});
|
||||
|
||||
type Props = DispatchProps & NewWidgetProps;
|
||||
|
13
frontend/src/container/NewWidget/types.ts
Normal file
13
frontend/src/container/NewWidget/types.ts
Normal 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;
|
||||
}
|
43
frontend/src/container/OptionsMenu/AddColumnField/index.tsx
Normal file
43
frontend/src/container/OptionsMenu/AddColumnField/index.tsx
Normal 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;
|
28
frontend/src/container/OptionsMenu/AddColumnField/styles.ts
Normal file
28
frontend/src/container/OptionsMenu/AddColumnField/styles.ts
Normal 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%;
|
||||
`;
|
33
frontend/src/container/OptionsMenu/FormatField/index.tsx
Normal file
33
frontend/src/container/OptionsMenu/FormatField/index.tsx
Normal 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;
|
17
frontend/src/container/OptionsMenu/FormatField/styles.ts
Normal file
17
frontend/src/container/OptionsMenu/FormatField/styles.ts
Normal 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;
|
||||
`;
|
29
frontend/src/container/OptionsMenu/MaxLinesField/index.tsx
Normal file
29
frontend/src/container/OptionsMenu/MaxLinesField/index.tsx
Normal 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;
|
12
frontend/src/container/OptionsMenu/MaxLinesField/styles.ts
Normal file
12
frontend/src/container/OptionsMenu/MaxLinesField/styles.ts
Normal 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;
|
||||
`;
|
57
frontend/src/container/OptionsMenu/index.tsx
Normal file
57
frontend/src/container/OptionsMenu/index.tsx
Normal 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;
|
19
frontend/src/container/OptionsMenu/styles.ts
Normal file
19
frontend/src/container/OptionsMenu/styles.ts
Normal 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;
|
||||
`;
|
@ -55,12 +55,18 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
queryKey: ['getPendingInvites', user?.accessJwt],
|
||||
});
|
||||
|
||||
const toggleModal = (value: boolean): void => {
|
||||
setIsInviteTeamMemberModalOpen(value);
|
||||
};
|
||||
|
||||
const [dataSource, setDataSource] = useState<DataProps[]>([]);
|
||||
|
||||
const toggleModal = useCallback(
|
||||
(value: boolean): void => {
|
||||
setIsInviteTeamMemberModalOpen(value);
|
||||
if (!value) {
|
||||
form.resetFields();
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const { hash } = useLocation();
|
||||
|
||||
const getParsedInviteData = useCallback(
|
||||
@ -79,7 +85,7 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
if (hash === INVITE_MEMBERS_HASH) {
|
||||
toggleModal(true);
|
||||
}
|
||||
}, [hash]);
|
||||
}, [hash, toggleModal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -225,7 +231,13 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
});
|
||||
}
|
||||
},
|
||||
[getParsedInviteData, getPendingInvitesResponse, notifications, t],
|
||||
[
|
||||
getParsedInviteData,
|
||||
getPendingInvitesResponse,
|
||||
notifications,
|
||||
t,
|
||||
toggleModal,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||
import { ReactNode } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type QueryBuilderConfig =
|
||||
@ -11,4 +12,5 @@ export type QueryBuilderConfig =
|
||||
export type QueryBuilderProps = {
|
||||
config?: QueryBuilderConfig;
|
||||
panelType: ITEMS;
|
||||
actions?: ReactNode;
|
||||
};
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { Col } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ActionsWrapperStyled = styled(Col)`
|
||||
padding-right: 1rem;
|
||||
`;
|
@ -11,15 +11,16 @@ import { Formula, Query } from './components';
|
||||
// ** Types
|
||||
import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
||||
// ** Styles
|
||||
import { ActionsWrapperStyled } from './QueryBuilder.styled';
|
||||
|
||||
export const QueryBuilder = memo(function QueryBuilder({
|
||||
config,
|
||||
panelType,
|
||||
actions,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
setupInitialDataSource,
|
||||
resetQueryBuilderInfo,
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
handleSetPanelType,
|
||||
@ -35,13 +36,6 @@ export const QueryBuilder = memo(function QueryBuilder({
|
||||
handleSetPanelType(panelType);
|
||||
}, [handleSetPanelType, panelType]);
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
resetQueryBuilderInfo();
|
||||
},
|
||||
[resetQueryBuilderInfo],
|
||||
);
|
||||
|
||||
const isDisabledQueryButton = useMemo(
|
||||
() => currentQuery.builder.queryData.length >= MAX_QUERIES,
|
||||
[currentQuery],
|
||||
@ -60,7 +54,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
||||
);
|
||||
|
||||
return (
|
||||
<Row gutter={[0, 20]} justify="start">
|
||||
<Row style={{ width: '100%' }} gutter={[0, 20]} justify="start">
|
||||
<Col span={24}>
|
||||
<Row gutter={[0, 50]}>
|
||||
{currentQuery.builder.queryData.map((query, index) => (
|
||||
@ -81,28 +75,31 @@ export const QueryBuilder = memo(function QueryBuilder({
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Row gutter={[20, 0]}>
|
||||
<Col>
|
||||
<Button
|
||||
disabled={isDisabledQueryButton}
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addNewBuilderQuery}
|
||||
>
|
||||
Query
|
||||
</Button>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
disabled={isDisabledFormulaButton}
|
||||
onClick={addNewFormula}
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Formula
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<ActionsWrapperStyled span={24}>
|
||||
<Row gutter={[20, 0]}>
|
||||
<Col>
|
||||
<Button
|
||||
disabled={isDisabledQueryButton}
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addNewBuilderQuery}
|
||||
>
|
||||
Query
|
||||
</Button>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
disabled={isDisabledFormulaButton}
|
||||
onClick={addNewFormula}
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Formula
|
||||
</Button>
|
||||
</Col>
|
||||
{actions}
|
||||
</Row>
|
||||
</ActionsWrapperStyled>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Col, Row } from 'antd';
|
||||
import { Col, Row, Typography } from 'antd';
|
||||
import { Fragment, memo, ReactNode, useState } from 'react';
|
||||
|
||||
// ** Types
|
||||
@ -46,7 +46,9 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
|
||||
<Col span={24}>
|
||||
<StyledInner onClick={handleToggleOpenFilters}>
|
||||
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
|
||||
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>}
|
||||
{!isOpenedFilters && (
|
||||
<Typography>Add conditions for {filtersTexts}</Typography>
|
||||
)}
|
||||
</StyledInner>
|
||||
</Col>
|
||||
{isOpenedFilters && <Col span={24}>{children}</Col>}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Typography } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { memo } from 'react';
|
||||
|
||||
@ -11,5 +12,9 @@ export const FilterLabel = memo(function FilterLabel({
|
||||
}: FilterLabelProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return <StyledLabel isDarkMode={isDarkMode}>{label}</StyledLabel>;
|
||||
return (
|
||||
<StyledLabel isDarkMode={isDarkMode}>
|
||||
<Typography>{label}</Typography>
|
||||
</StyledLabel>
|
||||
);
|
||||
});
|
||||
|
@ -3,19 +3,17 @@ import userEvent from '@testing-library/user-event';
|
||||
// Constants
|
||||
import {
|
||||
HAVING_OPERATORS,
|
||||
initialQueryBuilderFormValues,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
} from 'constants/queryBuilder';
|
||||
import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData';
|
||||
// ** Types
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
// ** Components
|
||||
import { HavingFilter } from '../HavingFilter';
|
||||
|
||||
const valueWithAttributeAndOperator: IBuilderQuery = {
|
||||
...initialQueryBuilderFormValues,
|
||||
dataSource: DataSource.LOGS,
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: 'SUM',
|
||||
aggregateAttribute: {
|
||||
isColumn: false,
|
||||
@ -29,7 +27,10 @@ describe('Having filter behaviour', () => {
|
||||
test('Having filter render is rendered', () => {
|
||||
const mockFn = jest.fn();
|
||||
const { unmount } = render(
|
||||
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />,
|
||||
<HavingFilter
|
||||
query={initialQueryBuilderFormValuesMap.metrics}
|
||||
onChange={mockFn}
|
||||
/>,
|
||||
);
|
||||
|
||||
const selectId = 'havingSelect';
|
||||
@ -44,7 +45,10 @@ describe('Having filter behaviour', () => {
|
||||
test('Having render is disabled initially', () => {
|
||||
const mockFn = jest.fn();
|
||||
const { unmount } = render(
|
||||
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />,
|
||||
<HavingFilter
|
||||
query={initialQueryBuilderFormValuesMap.metrics}
|
||||
onChange={mockFn}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByRole('combobox');
|
||||
|
@ -30,6 +30,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
|
||||
[ROUTES.SETTINGS]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.SIGN_UP]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.SOMETHING_WENT_WRONG]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.TRACES_EXPLORER]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.TRACE]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.TRACE_DETAIL]: [QueryParams.resourceAttributes],
|
||||
[ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],
|
||||
|
@ -1,8 +1,8 @@
|
||||
export const getQueryString = (
|
||||
avialableParams: string[],
|
||||
availableParams: string[],
|
||||
params: URLSearchParams,
|
||||
): string[] =>
|
||||
avialableParams.map((param) => {
|
||||
availableParams.map((param) => {
|
||||
if (params.has(param)) {
|
||||
return `${param}=${params.get(param)}`;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||
import ROUTES from 'constants/routes';
|
||||
@ -27,7 +27,6 @@ import {
|
||||
Sider,
|
||||
SlackButton,
|
||||
SlackMenuItemContainer,
|
||||
Tags,
|
||||
VersionContainer,
|
||||
} from './styles';
|
||||
|
||||
@ -42,6 +41,7 @@ function SideNav(): JSX.Element {
|
||||
>((state) => state.app);
|
||||
|
||||
const { pathname, search } = useLocation();
|
||||
|
||||
const { t } = useTranslation('');
|
||||
|
||||
const onCollapse = useCallback(() => {
|
||||
@ -55,9 +55,9 @@ function SideNav(): JSX.Element {
|
||||
const onClickHandler = useCallback(
|
||||
(to: string) => {
|
||||
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) {
|
||||
history.push(`${to}?${queryString.join('&')}`);
|
||||
@ -66,6 +66,10 @@ function SideNav(): JSX.Element {
|
||||
[pathname, search],
|
||||
);
|
||||
|
||||
const onClickMenuHandler: MenuProps['onClick'] = (e) => {
|
||||
onClickHandler(e.key);
|
||||
};
|
||||
|
||||
const onClickSlackHandler = (): void => {
|
||||
window.open('https://signoz.io/slack', '_blank');
|
||||
};
|
||||
@ -104,30 +108,17 @@ function SideNav(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const currentMenu = useMemo(
|
||||
() => menus.find((menu) => pathname.startsWith(menu.to)),
|
||||
[pathname],
|
||||
);
|
||||
const currentMenu = useMemo(() => {
|
||||
const routeKeys = Object.keys(ROUTES) as (keyof typeof ROUTES)[];
|
||||
const currentRouteKey = routeKeys.find((key) => {
|
||||
const route = ROUTES[key];
|
||||
return pathname === route;
|
||||
});
|
||||
|
||||
const items = [
|
||||
...menus.map(({ to, Icon, name, tags, children }) => ({
|
||||
key: to,
|
||||
icon: <Icon />,
|
||||
onClick: (): void => onClickHandler(to),
|
||||
label: (
|
||||
<Space>
|
||||
<div>{name}</div>
|
||||
{tags &&
|
||||
tags.map((e) => (
|
||||
<Tags key={e}>
|
||||
<Typography.Text>{e}</Typography.Text>
|
||||
</Tags>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
children,
|
||||
})),
|
||||
];
|
||||
if (!currentRouteKey) return null;
|
||||
|
||||
return ROUTES[currentRouteKey];
|
||||
}, [pathname]);
|
||||
|
||||
const sidebarItems = (props: SidebarItem, index: number): SidebarItem => ({
|
||||
key: `${index}`,
|
||||
@ -141,10 +132,11 @@ function SideNav(): JSX.Element {
|
||||
<Menu
|
||||
theme="dark"
|
||||
defaultSelectedKeys={[ROUTES.APPLICATION]}
|
||||
selectedKeys={currentMenu ? [currentMenu?.to] : []}
|
||||
selectedKeys={currentMenu ? [currentMenu] : []}
|
||||
mode="vertical"
|
||||
style={styles}
|
||||
items={items}
|
||||
items={menus}
|
||||
onClick={onClickMenuHandler}
|
||||
/>
|
||||
{sidebar.map((props, index) => (
|
||||
<SlackMenuItemContainer
|
||||
@ -155,7 +147,7 @@ function SideNav(): JSX.Element {
|
||||
<Menu
|
||||
theme="dark"
|
||||
defaultSelectedKeys={[ROUTES.APPLICATION]}
|
||||
selectedKeys={currentMenu ? [currentMenu?.to] : []}
|
||||
selectedKeys={currentMenu ? [currentMenu] : []}
|
||||
mode="inline"
|
||||
style={styles}
|
||||
items={[sidebarItems(props, index)]}
|
||||
|
@ -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;
|
113
frontend/src/container/SideNav/menuItems.tsx
Normal file
113
frontend/src/container/SideNav/menuItems.tsx
Normal 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;
|
@ -5,6 +5,7 @@ import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
const breadcrumbNameMap = {
|
||||
[ROUTES.APPLICATION]: 'Services',
|
||||
[ROUTES.TRACE]: 'Traces',
|
||||
[ROUTES.TRACES_EXPLORER]: 'Traces Explorer',
|
||||
[ROUTES.SERVICE_MAP]: 'Service Map',
|
||||
[ROUTES.USAGE_EXPLORER]: 'Usage Explorer',
|
||||
[ROUTES.INSTRUMENTATION]: 'Get Started',
|
||||
@ -19,6 +20,7 @@ const breadcrumbNameMap = {
|
||||
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
|
||||
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
||||
[ROUTES.LOGS]: 'Logs',
|
||||
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
|
||||
};
|
||||
|
||||
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {
|
||||
|
25
frontend/src/container/TracesExplorer/Controls/index.tsx
Normal file
25
frontend/src/container/TracesExplorer/Controls/index.tsx
Normal 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
Loading…
x
Reference in New Issue
Block a user