mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-24 02:14:26 +08:00
commit
3c63d66591
12
.github/workflows/build.yaml
vendored
12
.github/workflows/build.yaml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- 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@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Create .env file
|
||||
run: |
|
||||
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env
|
||||
@ -54,12 +54,12 @@ jobs:
|
||||
build-query-service:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: |
|
||||
@ -72,12 +72,12 @@ jobs:
|
||||
build-ee-query-service:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Build EE query-service image
|
||||
shell: bash
|
||||
run: |
|
||||
|
2
.github/workflows/codeql.yaml
vendored
2
.github/workflows/codeql.yaml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
lint-commits:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: wagoid/commitlint-github-action@v5
|
||||
|
@ -12,11 +12,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Codebase
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: signoz/gh-bot
|
||||
- name: Use Node v16
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Setup Cache & Install Dependencies
|
||||
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
with:
|
||||
fail-on-severity: high
|
||||
|
2
.github/workflows/e2e-k3s.yaml
vendored
2
.github/workflows/e2e-k3s.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
DOCKER_TAG: pull-${{ github.event.number }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build query-service image
|
||||
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@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16.x"
|
||||
- name: Install dependencies
|
||||
|
26
.github/workflows/push.yaml
vendored
26
.github/workflows/push.yaml
vendored
@ -14,7 +14,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
@ -42,6 +46,11 @@ jobs:
|
||||
else
|
||||
echo "DOCKER_TAG=${{ steps.branch-name.outputs.current_branch }}-oss" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Install cross-compilation tools
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools
|
||||
- name: Build and push docker image
|
||||
run: make build-push-query-service
|
||||
|
||||
@ -49,7 +58,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
@ -77,6 +90,11 @@ jobs:
|
||||
else
|
||||
echo "DOCKER_TAG=${{ steps.branch-name.outputs.current_branch }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Install cross-compilation tools
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools
|
||||
- name: Build and push docker image
|
||||
run: make build-push-ee-query-service
|
||||
|
||||
@ -84,7 +102,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: yarn install
|
||||
@ -128,7 +146,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Create .env file
|
||||
run: |
|
||||
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env
|
||||
|
2
.github/workflows/sonar.yml
vendored
2
.github/workflows/sonar.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Sonar analysis
|
||||
|
1
.github/workflows/staging-deployment.yaml
vendored
1
.github/workflows/staging-deployment.yaml
vendored
@ -26,6 +26,7 @@ jobs:
|
||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
export OTELCOL_TAG="main"
|
||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||
docker system prune --force
|
||||
docker pull signoz/signoz-otel-collector:main
|
||||
cd ~/signoz
|
||||
|
1
.github/workflows/testing-deployment.yaml
vendored
1
.github/workflows/testing-deployment.yaml
vendored
@ -26,6 +26,7 @@ jobs:
|
||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
export DEV_BUILD="1"
|
||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||
docker system prune --force
|
||||
cd ~/signoz
|
||||
git status
|
||||
|
82
Makefile
82
Makefile
@ -8,6 +8,7 @@ BUILD_HASH ?= $(shell git rev-parse --short HEAD)
|
||||
BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
BUILD_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||
DEV_LICENSE_SIGNOZ_IO ?= https://staging-license.signoz.io/api/v1
|
||||
DEV_BUILD ?= "" # set to any non-empty value to enable dev build
|
||||
|
||||
# Internal variables or constants.
|
||||
FRONTEND_DIRECTORY ?= frontend
|
||||
@ -15,15 +16,15 @@ QUERY_SERVICE_DIRECTORY ?= pkg/query-service
|
||||
EE_QUERY_SERVICE_DIRECTORY ?= ee/query-service
|
||||
STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup
|
||||
SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup
|
||||
LOCAL_GOOS ?= $(shell go env GOOS)
|
||||
LOCAL_GOARCH ?= $(shell go env GOARCH)
|
||||
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
GOARCH ?= $(shell go env GOARCH)
|
||||
GOPATH ?= $(shell go env GOPATH)
|
||||
|
||||
REPONAME ?= signoz
|
||||
DOCKER_TAG ?= $(subst v,,$(BUILD_VERSION))
|
||||
|
||||
FRONTEND_DOCKER_IMAGE ?= frontend
|
||||
QUERY_SERVICE_DOCKER_IMAGE ?= query-service
|
||||
DEV_BUILD ?= ""
|
||||
|
||||
# Build-time Go variables
|
||||
PACKAGE?=go.signoz.io/signoz
|
||||
@ -37,10 +38,22 @@ LD_FLAGS=-X ${buildHash}=${BUILD_HASH} -X ${buildTime}=${BUILD_TIME} -X ${buildV
|
||||
DEV_LD_FLAGS=-X ${licenseSignozIo}=${DEV_LICENSE_SIGNOZ_IO}
|
||||
|
||||
all: build-push-frontend build-push-query-service
|
||||
|
||||
# Steps to build static files of frontend
|
||||
build-frontend-static:
|
||||
@echo "------------------"
|
||||
@echo "--> Building frontend static files"
|
||||
@echo "------------------"
|
||||
@cd $(FRONTEND_DIRECTORY) && \
|
||||
rm -rf build && \
|
||||
CI=1 yarn install && \
|
||||
yarn build && \
|
||||
ls -l build
|
||||
|
||||
# Steps to build and push docker image of frontend
|
||||
.PHONY: build-frontend-amd64 build-push-frontend
|
||||
# Step to build docker image of frontend in amd64 (used in build pipeline)
|
||||
build-frontend-amd64:
|
||||
build-frontend-amd64: build-frontend-static
|
||||
@echo "------------------"
|
||||
@echo "--> Building frontend docker image for amd64"
|
||||
@echo "------------------"
|
||||
@ -49,7 +62,7 @@ build-frontend-amd64:
|
||||
--build-arg TARGETPLATFORM="linux/amd64" .
|
||||
|
||||
# Step to build and push docker image of frontend(used in push pipeline)
|
||||
build-push-frontend:
|
||||
build-push-frontend: build-frontend-static
|
||||
@echo "------------------"
|
||||
@echo "--> Building and pushing frontend docker image"
|
||||
@echo "------------------"
|
||||
@ -57,24 +70,52 @@ build-push-frontend:
|
||||
docker buildx build --file Dockerfile --progress plain --push --platform linux/arm64,linux/amd64 \
|
||||
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
|
||||
|
||||
# Steps to build static binary of query service
|
||||
.PHONY: build-query-service-static
|
||||
build-query-service-static:
|
||||
@echo "------------------"
|
||||
@echo "--> Building query-service static binary"
|
||||
@echo "------------------"
|
||||
@if [ $(DEV_BUILD) != "" ]; then \
|
||||
cd $(QUERY_SERVICE_DIRECTORY) && \
|
||||
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
|
||||
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
|
||||
else \
|
||||
cd $(QUERY_SERVICE_DIRECTORY) && \
|
||||
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
|
||||
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS}"; \
|
||||
fi
|
||||
|
||||
.PHONY: build-query-service-static-amd64
|
||||
build-query-service-static-amd64:
|
||||
make GOARCH=amd64 build-query-service-static
|
||||
|
||||
.PHONY: build-query-service-static-arm64
|
||||
build-query-service-static-arm64:
|
||||
make CC=aarch64-linux-gnu-gcc GOARCH=arm64 build-query-service-static
|
||||
|
||||
# Steps to build static binary of query service for all platforms
|
||||
.PHONY: build-query-service-static-all
|
||||
build-query-service-static-all: build-query-service-static-amd64 build-query-service-static-arm64
|
||||
|
||||
# Steps to build and push docker image of query service
|
||||
.PHONY: build-query-service-amd64 build-push-query-service
|
||||
.PHONY: build-query-service-amd64 build-push-query-service
|
||||
# Step to build docker image of query service in amd64 (used in build pipeline)
|
||||
build-query-service-amd64:
|
||||
build-query-service-amd64: build-query-service-static-amd64
|
||||
@echo "------------------"
|
||||
@echo "--> Building query-service docker image for amd64"
|
||||
@echo "------------------"
|
||||
@docker build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile \
|
||||
-t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
|
||||
--build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="$(LD_FLAGS)" .
|
||||
--tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
|
||||
--build-arg TARGETPLATFORM="linux/amd64" .
|
||||
|
||||
# Step to build and push docker image of query in amd64 and arm64 (used in push pipeline)
|
||||
build-push-query-service:
|
||||
build-push-query-service: build-query-service-static-all
|
||||
@echo "------------------"
|
||||
@echo "--> Building and pushing query-service docker image"
|
||||
@echo "------------------"
|
||||
@docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plain \
|
||||
--push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS="$(LD_FLAGS)" \
|
||||
--push --platform linux/arm64,linux/amd64 \
|
||||
--tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) .
|
||||
|
||||
# Step to build EE docker image of query service in amd64 (used in build pipeline)
|
||||
@ -82,24 +123,14 @@ build-ee-query-service-amd64:
|
||||
@echo "------------------"
|
||||
@echo "--> Building query-service docker image for amd64"
|
||||
@echo "------------------"
|
||||
@if [ $(DEV_BUILD) != "" ]; then \
|
||||
docker build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \
|
||||
-t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
|
||||
--build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="${LD_FLAGS} ${DEV_LD_FLAGS}" .; \
|
||||
else \
|
||||
docker build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \
|
||||
-t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
|
||||
--build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="$(LD_FLAGS)" .; \
|
||||
fi
|
||||
make QUERY_SERVICE_DIRECTORY=${EE_QUERY_SERVICE_DIRECTORY} build-query-service-amd64
|
||||
|
||||
# Step to build and push EE docker image of query in amd64 and arm64 (used in push pipeline)
|
||||
build-push-ee-query-service:
|
||||
@echo "------------------"
|
||||
@echo "--> Building and pushing query-service docker image"
|
||||
@echo "------------------"
|
||||
@docker buildx build --file $(EE_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) .
|
||||
make QUERY_SERVICE_DIRECTORY=${EE_QUERY_SERVICE_DIRECTORY} build-push-query-service
|
||||
|
||||
dev-setup:
|
||||
mkdir -p /var/lib/signoz
|
||||
@ -110,7 +141,7 @@ dev-setup:
|
||||
@echo "------------------"
|
||||
|
||||
run-local:
|
||||
@LOCAL_GOOS=$(LOCAL_GOOS) LOCAL_GOARCH=$(LOCAL_GOARCH) docker-compose -f \
|
||||
@docker-compose -f \
|
||||
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \
|
||||
up --build -d
|
||||
|
||||
@ -153,3 +184,4 @@ test:
|
||||
go test ./pkg/query-service/formatter/...
|
||||
go test ./pkg/query-service/tests/integration/...
|
||||
go test ./pkg/query-service/rules/...
|
||||
go test ./pkg/query-service/collectorsimulator/...
|
||||
|
203
README.zh-cn.md
203
README.zh-cn.md
@ -1,170 +1,225 @@
|
||||
<p align="center">
|
||||
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
|
||||
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
|
||||
|
||||
<p align="center">监视你的应用,并可排查已部署应用中的问题,这是一个开源的可替代DataDog、NewRelic的方案</p>
|
||||
<p align="center">监控你的应用,并且可排查已部署应用的问题,这是一个可替代 DataDog、NewRelic 的开源方案</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/frontend?label=Downloads"> </a>
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker Downloads"> </a>
|
||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
|
||||
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
|
||||
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
|
||||
</p>
|
||||
|
||||
##
|
||||
<h3 align="center">
|
||||
<a href="https://signoz.io/docs"><b>文档</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/develop/README.zh-cn.md"><b>中文ReadMe</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/develop/README.de-de.md"><b>德文ReadMe</b></a> •
|
||||
<a href="https://github.com/SigNoz/signoz/blob/develop/README.pt-br.md"><b>葡萄牙语ReadMe</b></a> •
|
||||
<a href="https://signoz.io/slack"><b>Slack 社区</b></a> •
|
||||
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
|
||||
</h3>
|
||||
|
||||
SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNoz使用分布式追踪来增加软件技术栈的可见性。
|
||||
##
|
||||
|
||||
👉 你能看到一些性能指标,服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。
|
||||
SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可以使用 SigNoz 实现如下能力:
|
||||
|
||||
👉 通过准确的追踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。
|
||||
👉 在同一块面板上,可视化 Metrics, Traces 和 Logs 内容。
|
||||
|
||||
👉 聚合trace数据来获得业务相关指标。
|
||||
👉 你可以关注服务的 p99 延迟和错误率, 包括外部 API 调用和个别的端点。
|
||||
|
||||

|
||||
<br />
|
||||

|
||||
<br />
|
||||

|
||||
👉 你可以找到问题的根因,通过提取相关问题的 traces 日志、单独查看请求 traces 的火焰图详情。
|
||||
|
||||
👉 执行 trace 数据聚合,以获取业务相关的 metrics
|
||||
|
||||
👉 对日志过滤和查询,通过日志的属性建立看板和告警
|
||||
|
||||
👉 通过 Python,java,Ruby 和 Javascript 自动记录异常
|
||||
|
||||
👉 轻松的自定义查询和设置告警
|
||||
|
||||
### 应用 Metrics 展示
|
||||
|
||||

|
||||
|
||||
### 分布式追踪
|
||||
|
||||
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
|
||||
|
||||
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
|
||||
|
||||
### 日志管理
|
||||
|
||||
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
|
||||
|
||||
### 基础设施监控
|
||||
|
||||
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
|
||||
|
||||
### 异常监控
|
||||
|
||||

|
||||
|
||||
### 告警
|
||||
|
||||
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
|
||||
## 加入我们 Slack 社区
|
||||
|
||||
## 加入我们的Slack社区
|
||||
|
||||
来[Slack](https://signoz.io/slack) 跟我们打声招呼👋
|
||||
来 [Slack](https://signoz.io/slack) 和我们打招呼吧 👋
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Features.svg" width="50px" />
|
||||
## 特性:
|
||||
|
||||
## 功能:
|
||||
- 为 metrics, traces and logs 制定统一的 UI。 无需切换 Prometheus 到 Jaeger 去查找问题,也无需使用想 Elastic 这样的日志工具分开你的 metrics 和 traces
|
||||
|
||||
- 应用概览指标(metrics),如RPS, p50/p90/p99延迟率分位值,错误率等。
|
||||
- 应用中最慢的终端(endpoint)
|
||||
- 查看特定请求的trace数据来分析下游服务问题、慢数据库查询问题 及调用第三方服务如支付网关的问题
|
||||
- 通过服务名称、操作、延迟、错误、标签来过滤traces。
|
||||
- 聚合trace数据(events/spans)来得到业务相关指标。比如,你可以通过过滤条件`customer_type: gold` or `deployment_version: v2` or `external_call: paypal` 来获取指定业务的错误率和p99延迟
|
||||
- 为metrics和trace提供统一的UI。排查问题不需要在Prometheus和Jaeger之间切换。
|
||||
- 默认统计应用的 metrics 数据,像 RPS (每秒请求数), 50th/90th/99th 的分位数延迟数据,还有相关的错误率
|
||||
|
||||
- 找到应用中最慢的端点
|
||||
|
||||
- 查看准确的请求跟踪数据,找到下游服务的问题了,比如 DB 慢查询,或者调用第三方的支付网关等
|
||||
|
||||
- 通过 服务名、操作方式、延迟、错误、标签/注释 过滤 traces 数据
|
||||
|
||||
- 通过聚合 trace 数据而获得业务相关的 metrics。 比如你可以通过 `customer_type: gold` 或者 `deployment_version: v2` 或者 `external_call: paypal` 获取错误率和 P99 延迟数据
|
||||
|
||||
- 原生支持 OpenTelemetry 日志,高级日志查询,自动收集 k8s 相关日志
|
||||
|
||||
- 快如闪电的日志分析 ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
|
||||
|
||||
- 可视化点到点的基础设施性能,提取有所有类型机器的 metrics 数据
|
||||
|
||||
- 轻易自定义告警查询
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/WhatsCool.svg" width="50px" />
|
||||
## 为什么使用 SigNoz?
|
||||
|
||||
## 为何选择SigNoz?
|
||||
作为开发者, 我们发现 SaaS 厂商对一些大家想要的小功能都是闭源的,这种行为真的让人有点恼火。 闭源厂商还会在月底给你一张没有明细的巨额账单。
|
||||
|
||||
作为开发人员,我们发现依赖闭源的SaaS厂商提供的每个小功能有些麻烦,闭源厂商通常会给你一份巨额月付账单,但不提供足够的透明度,你不知道你为哪些功能付费。
|
||||
我们想做一个自托管并且可开源的工具,像 DataDog 和 NewRelic 那样, 为那些担心数据隐私和安全的公司提供第三方服务。
|
||||
|
||||
我们想做一个自服务的开源版本的工具,类似于DataDog和NewRelic,用于那些对客户数据流入第三方有隐私和安全担忧的厂商。
|
||||
作为开源的项目,你完全可以自己掌控你的配置、样本和更新。你同样可以基于 SigNoz 拓展特定的业务模块。
|
||||
|
||||
开源也让你对配置、采样和正常运行时间有完整的控制,你可以在SigNoz基础上构建模块来满足特定的商业需求。
|
||||
### 支持的编程语言:
|
||||
|
||||
### 语言支持
|
||||
|
||||
我们支持[OpenTelemetry](https://opentelemetry.io)库,你可以使用它来装备应用。也就是说SigNoz支持任何支持OpenTelemetry库的框架和语言。 主要支持语言包括:
|
||||
我们支持 [OpenTelemetry](https://opentelemetry.io)。作为一个观测你应用的库文件。所以任何 OpenTelemetry 支持的框架和语言,对于 SigNoz 也同样支持。 一些主要支持的语言如下:
|
||||
|
||||
- Java
|
||||
- Python
|
||||
- NodeJS
|
||||
- Go
|
||||
- PHP
|
||||
- .NET
|
||||
- Ruby
|
||||
- Elixir
|
||||
- Rust
|
||||
|
||||
你可以在这个文档里找到完整的语言列表 - https://opentelemetry.io/docs/
|
||||
你可以在这里找到全部支持的语言列表 - https://opentelemetry.io/docs/
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Philosophy.svg" width="50px" />
|
||||
## 让我们开始吧
|
||||
|
||||
## 入门
|
||||
### 使用 Docker 部署
|
||||
|
||||
请一步步跟随 [这里](https://signoz.io/docs/install/docker/) 通过 docker 来安装。
|
||||
|
||||
### 使用Docker部署
|
||||
|
||||
请按照[这里](https://signoz.io/docs/install/docker/)列出的步骤使用Docker来安装
|
||||
|
||||
如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/install/troubleshooting/)会对你有帮助。
|
||||
这个 [排障说明书](https://signoz.io/docs/install/troubleshooting/) 可以帮助你解决碰到的问题。
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### 使用 Helm 在 Kubernetes 部署
|
||||
|
||||
### 使用Helm在Kubernetes上部署
|
||||
|
||||
请跟着[这里](https://signoz.io/docs/deployment/helm_chart)的步骤使用helm charts安装
|
||||
请一步步跟随 [这里](https://signoz.io/docs/deployment/helm_chart) 通过 helm 来安装
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/UseSigNoz.svg" width="50px" />
|
||||
|
||||
## 与其他方案的比较
|
||||
## 比较相似的工具
|
||||
|
||||
### SigNoz vs Prometheus
|
||||
|
||||
如果你只是需要监控指标(metrics),那Prometheus是不错的,但如果你要无缝的在metrics和traces之间切换,那目前把Prometheus & Jaeger串起来的体验并不好。
|
||||
Prometheus 是一个针对 metrics 监控的强大工具。但是如果你想无缝的切换 metrics 和 traces 查询,你当前大概率需要在 Prometheus 和 Jaeger 之间切换。
|
||||
|
||||
我们的目标是为metrics和traces提供统一的UI - 类似于Datadog这样的Saas厂提供的方案。并且能够对trace进行过滤和聚合,这是目前Jaeger缺失的功能。
|
||||
我们的目标是提供一个客户观测 metrics 和 traces 整合的 UI。就像 SaaS 供应商 DataDog,它提供很多 jaeger 缺失的功能,比如针对 traces 过滤功能和聚合功能。
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Jaeger
|
||||
|
||||
Jaeger只做分布式追踪(distributed tracing),SigNoz则支持metrics,traces,logs ,即可视化的三大支柱。
|
||||
Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metrics, traces 和 logs 所有的观测。
|
||||
|
||||
并且SigNoz有一些Jaeger没有的高级功能:
|
||||
而且, SigNoz 相较于 Jaeger 拥有更对的高级功能:
|
||||
|
||||
- Jaegar UI无法在traces或过滤的traces上展示metrics。
|
||||
- Jaeger不能对过滤的traces做聚合操作。例如,拥有tag为customer_type='premium'的所有请求的p99延迟。而这个功能在SigNoz这儿是很容易实现。
|
||||
- Jaegar UI 不能提供任何基于 traces 的 metrics 查询和过滤。
|
||||
|
||||
- Jaeger 不能针对过滤的 traces 做聚合。 比如, p99 延迟的请求有个标签是 customer_type='premium'。 而这些在 SigNoz 可以轻松做到。
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Elastic
|
||||
|
||||
- SigNoz 的日志管理是基于 ClickHouse 实现的,可以使日志的聚合更加高效,因为它是基于 OLAP 的数据仓储。
|
||||
|
||||
- 与 Elastic 相比,可以节省 50% 的资源成本
|
||||
|
||||
我们已经公布了 Elastic 和 SigNoz 的性能对比。 请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Loki
|
||||
|
||||
- SigNoz 支持大容量高基数的聚合,但是 loki 是不支持的。
|
||||
|
||||
- SigNoz 支持索引的高基数查询,并且对索引没有数量限制,而 Loki 会在添加部分索引后到达最大上限。
|
||||
|
||||
- 相较于 SigNoz,Loki 在搜索大量数据下既困难又缓慢。
|
||||
|
||||
我们已经发布了基准测试对比 Loki 和 SigNoz 性能。请点击 [这里](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributors.svg" width="50px" />
|
||||
|
||||
## 贡献
|
||||
|
||||
我们 ❤️ 你的贡献,无论大小。 请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 再开始给 SigNoz 做贡献。
|
||||
|
||||
我们 ❤️ 任何贡献无论大小。 请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 然后开始给Signoz做贡献。
|
||||
如果你不知道如何开始? 只需要在 [slack 社区](https://signoz.io/slack) 通过 `#contributing` 频道联系我们。
|
||||
|
||||
还不清楚怎么开始? 只需在[slack社区](https://signoz.io/slack)的`#contributing`频道里ping我们。
|
||||
### 项目维护人员
|
||||
|
||||
### Project maintainers
|
||||
|
||||
#### Backend
|
||||
#### 后端
|
||||
|
||||
- [Ankit Nayan](https://github.com/ankitnayan)
|
||||
- [Nityananda Gohain](https://github.com/nityanandagohain)
|
||||
- [Srikanth Chekuri](https://github.com/srikanthccv)
|
||||
- [Vishal Sharma](https://github.com/makeavish)
|
||||
|
||||
#### Frontend
|
||||
#### 前端
|
||||
|
||||
- [Palash Gupta](https://github.com/palashgdev)
|
||||
|
||||
#### DevOps
|
||||
#### 运维开发
|
||||
|
||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/DevelopingLocally.svg" width="50px" />
|
||||
|
||||
## 文档
|
||||
|
||||
文档在这里:https://signoz.io/docs/. 如果你觉得有任何不清楚或者有文档缺失,请在Github里发一个问题,并使用标签 `documentation` 或者在社区stack频道里告诉我们。
|
||||
你可以通过 https://signoz.io/docs/ 找到相关文档。如果你需要阐述问题或者发现一些确实的事件, 通过标签为 `documentation` 提交 Github 问题。或者通过 slack 社区频道。
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
|
||||
|
||||
## 社区
|
||||
|
||||
加入[slack community](https://signoz.io/slack),了解更多关于分布式跟踪、可观察性(observability),以及SigNoz。同时与其他用户和贡献者一起交流。
|
||||
加入 [slack 社区](https://signoz.io/slack) 去了解更多关于分布式追踪、可观测性系统 。或者与 SigNoz 其他用户和贡献者交流。
|
||||
|
||||
如果你有任何想法、问题或者反馈,请在[Github Discussions](https://github.com/SigNoz/signoz/discussions)分享给我们。
|
||||
如果你有任何想法、问题、或者任何反馈, 请通过 [Github Discussions](https://github.com/SigNoz/signoz/discussions) 分享。
|
||||
|
||||
最后,感谢我们这些优秀的贡献者们。
|
||||
不管怎么样,感谢这个项目的所有贡献者!
|
||||
|
||||
<a href="https://github.com/signoz/signoz/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
@ -144,7 +144,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.30.0
|
||||
image: signoz/query-service:0.31.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@ -184,7 +184,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.30.0
|
||||
image: signoz/frontend:0.31.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@ -197,7 +197,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.79.7
|
||||
image: signoz/signoz-otel-collector:0.79.8
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@ -230,7 +230,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.79.7
|
||||
image: signoz/signoz-otel-collector:0.79.8
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
|
@ -48,7 +48,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: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.79.7
|
||||
image: signoz/signoz-otel-collector:0.79.8
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@ -78,7 +78,7 @@ services:
|
||||
|
||||
otel-collector-metrics:
|
||||
container_name: signoz-otel-collector-metrics
|
||||
image: signoz/signoz-otel-collector:0.79.7
|
||||
image: signoz/signoz-otel-collector:0.79.8
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
|
@ -8,7 +8,7 @@ services:
|
||||
dockerfile: "./Dockerfile"
|
||||
args:
|
||||
LDFLAGS: ""
|
||||
TARGETPLATFORM: "${LOCAL_GOOS}/${LOCAL_GOARCH}"
|
||||
TARGETPLATFORM: "${GOOS}/${GOARCH}"
|
||||
container_name: signoz-query-service
|
||||
environment:
|
||||
- ClickHouseUrl=tcp://clickhouse:9000
|
||||
@ -52,8 +52,8 @@ services:
|
||||
context: "../../../frontend"
|
||||
dockerfile: "./Dockerfile"
|
||||
args:
|
||||
TARGETOS: "${LOCAL_GOOS}"
|
||||
TARGETPLATFORM: "${LOCAL_GOARCH}"
|
||||
TARGETOS: "${GOOS}"
|
||||
TARGETPLATFORM: "${GOARCH}"
|
||||
container_name: signoz-frontend
|
||||
environment:
|
||||
- FRONTEND_API_ENDPOINT=http://query-service:8080
|
||||
|
@ -162,7 +162,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.30.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.31.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@ -201,7 +201,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.30.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.31.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -213,7 +213,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.7}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.8}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
@ -244,7 +244,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.7}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.8}
|
||||
container_name: signoz-otel-collector-metrics
|
||||
command:
|
||||
[
|
||||
|
@ -1,40 +1,20 @@
|
||||
FROM golang:1.21-bookworm AS builder
|
||||
|
||||
# LD_FLAGS is passed as argument from Makefile. It will be empty, if no argument passed
|
||||
ARG LD_FLAGS
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOPATH=/go
|
||||
|
||||
RUN export GOOS=$(echo ${TARGETPLATFORM} | cut -d / -f1) && \
|
||||
export GOARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2)
|
||||
|
||||
# Prepare and enter src directory
|
||||
WORKDIR /go/src/github.com/signoz/signoz
|
||||
|
||||
# Add the sources and proceed with build
|
||||
ADD . .
|
||||
RUN cd ee/query-service \
|
||||
&& go build -tags timetzdata -a -o ./bin/query-service \
|
||||
-ldflags "-linkmode external -extldflags '-static' -s -w $LD_FLAGS" \
|
||||
&& chmod +x ./bin/query-service
|
||||
|
||||
|
||||
# use a minimal alpine image
|
||||
FROM alpine:3.16.7
|
||||
FROM alpine:3.17
|
||||
|
||||
# Add Maintainer Info
|
||||
LABEL maintainer="signoz"
|
||||
|
||||
# define arguments that can be passed during build time
|
||||
ARG TARGETOS TARGETARCH
|
||||
|
||||
# add ca-certificates in case you need them
|
||||
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
|
||||
|
||||
# set working directory
|
||||
WORKDIR /root
|
||||
|
||||
# copy the binary from builder
|
||||
COPY --from=builder /go/src/github.com/signoz/signoz/ee/query-service/bin/query-service .
|
||||
# copy the query-service binary
|
||||
COPY ee/query-service/bin/query-service-${TARGETOS}-${TARGETARCH} /root/query-service
|
||||
|
||||
# copy prometheus YAML config
|
||||
COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml
|
||||
@ -45,7 +25,6 @@ RUN chmod 755 /root /root/query-service
|
||||
# run the binary
|
||||
ENTRYPOINT ["./query-service"]
|
||||
|
||||
CMD ["-config", "../config/prometheus.yml"]
|
||||
# CMD ["./query-service -config /root/config/prometheus.yml"]
|
||||
CMD ["-config", "/root/config/prometheus.yml"]
|
||||
|
||||
EXPOSE 8080
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/dao"
|
||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||
"go.signoz.io/signoz/ee/query-service/license"
|
||||
"go.signoz.io/signoz/ee/query-service/usage"
|
||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||
@ -27,6 +28,7 @@ type APIHandlerOptions struct {
|
||||
DialTimeout time.Duration
|
||||
AppDao dao.ModelDao
|
||||
RulesManager *rules.Manager
|
||||
UsageManager *usage.Manager
|
||||
FeatureFlags baseint.FeatureLookup
|
||||
LicenseManager *license.Manager
|
||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||
@ -82,6 +84,10 @@ func (ah *APIHandler) LM() *license.Manager {
|
||||
return ah.opts.LicenseManager
|
||||
}
|
||||
|
||||
func (ah *APIHandler) UM() *usage.Manager {
|
||||
return ah.opts.UsageManager
|
||||
}
|
||||
|
||||
func (ah *APIHandler) AppDao() dao.ModelDao {
|
||||
return ah.opts.AppDao
|
||||
}
|
||||
@ -150,6 +156,13 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.getPATs)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/pat/{id}", am.OpenAccess(ah.deletePAT)).Methods(http.MethodDelete)
|
||||
|
||||
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v2/licenses",
|
||||
am.ViewAccess(ah.listLicensesV2)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
ah.APIHandler.RegisterRoutes(router, am)
|
||||
|
||||
}
|
||||
|
@ -4,10 +4,44 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/constants"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type tierBreakdown struct {
|
||||
UnitPrice float64 `json:"unitPrice"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
TierStart int64 `json:"tierStart"`
|
||||
TierEnd int64 `json:"tierEnd"`
|
||||
TierCost float64 `json:"tierCost"`
|
||||
}
|
||||
|
||||
type usageResponse struct {
|
||||
Type string `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
Tiers []tierBreakdown `json:"tiers"`
|
||||
}
|
||||
|
||||
type details struct {
|
||||
Total float64 `json:"total"`
|
||||
Breakdown []usageResponse `json:"breakdown"`
|
||||
BaseFee float64 `json:"baseFee"`
|
||||
}
|
||||
|
||||
type billingDetails struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
BillingPeriodStart int64 `json:"billingPeriodStart"`
|
||||
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
|
||||
Details details `json:"details"`
|
||||
Discount float64 `json:"discount"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
|
||||
licenses, apiError := ah.LM().GetLicenses(context.Background())
|
||||
if apiError != nil {
|
||||
@ -38,3 +72,150 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ah.Respond(w, license)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type checkoutResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
RedirectURL string `json:"redirectURL"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
hClient := &http.Client{}
|
||||
req, err := http.NewRequest("POST", constants.LicenseSignozIo+"/checkout", r.Body)
|
||||
if err != nil {
|
||||
RespondError(w, model.InternalError(err), nil)
|
||||
return
|
||||
}
|
||||
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
|
||||
licenseResp, err := hClient.Do(req)
|
||||
if err != nil {
|
||||
RespondError(w, model.InternalError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// decode response body
|
||||
var resp checkoutResponse
|
||||
if err := json.NewDecoder(licenseResp.Body).Decode(&resp); err != nil {
|
||||
RespondError(w, model.InternalError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, resp.Data)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
|
||||
licenseKey := r.URL.Query().Get("licenseKey")
|
||||
|
||||
if licenseKey == "" {
|
||||
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
||||
return
|
||||
}
|
||||
|
||||
billingURL := fmt.Sprintf("%s/usage?licenseKey=%s", constants.LicenseSignozIo, licenseKey)
|
||||
|
||||
hClient := &http.Client{}
|
||||
req, err := http.NewRequest("GET", billingURL, nil)
|
||||
if err != nil {
|
||||
RespondError(w, model.InternalError(err), nil)
|
||||
return
|
||||
}
|
||||
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
|
||||
billingResp, err := hClient.Do(req)
|
||||
if err != nil {
|
||||
RespondError(w, model.InternalError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// decode response body
|
||||
var billingResponse billingDetails
|
||||
if err := json.NewDecoder(billingResp.Body).Decode(&billingResponse); err != nil {
|
||||
RespondError(w, model.InternalError(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(srikanthccv):Fetch the current day usage and add it to the response
|
||||
ah.Respond(w, billingResponse.Data)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
licenses, apiError := ah.LM().GetLicenses(context.Background())
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
}
|
||||
|
||||
resp := model.Licenses{
|
||||
TrialStart: -1,
|
||||
TrialEnd: -1,
|
||||
OnTrial: false,
|
||||
WorkSpaceBlock: false,
|
||||
Licenses: licenses,
|
||||
}
|
||||
|
||||
var currentActiveLicenseKey string
|
||||
|
||||
for _, license := range licenses {
|
||||
if license.IsCurrent {
|
||||
currentActiveLicenseKey = license.Key
|
||||
}
|
||||
}
|
||||
|
||||
// For the case when no license is applied i.e community edition
|
||||
// There will be no trial details or license details
|
||||
if currentActiveLicenseKey == "" {
|
||||
ah.Respond(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch trial details
|
||||
hClient := &http.Client{}
|
||||
url := fmt.Sprintf("%s/trial?licenseKey=%s", constants.LicenseSignozIo, currentActiveLicenseKey)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
zap.S().Error("Error while creating request for trial details", err)
|
||||
// If there is an error in fetching trial details, we will still return the license details
|
||||
// to avoid blocking the UI
|
||||
ah.Respond(w, resp)
|
||||
return
|
||||
}
|
||||
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
|
||||
trialResp, err := hClient.Do(req)
|
||||
if err != nil {
|
||||
zap.S().Error("Error while fetching trial details", err)
|
||||
// If there is an error in fetching trial details, we will still return the license details
|
||||
// to avoid incorrectly blocking the UI
|
||||
ah.Respond(w, resp)
|
||||
return
|
||||
}
|
||||
defer trialResp.Body.Close()
|
||||
|
||||
trialRespBody, err := io.ReadAll(trialResp.Body)
|
||||
|
||||
if err != nil || trialResp.StatusCode != http.StatusOK {
|
||||
zap.S().Error("Error while fetching trial details", err)
|
||||
// If there is an error in fetching trial details, we will still return the license details
|
||||
// to avoid incorrectly blocking the UI
|
||||
ah.Respond(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// decode response body
|
||||
var trialRespData model.SubscriptionServerResp
|
||||
|
||||
if err := json.Unmarshal(trialRespBody, &trialRespData); err != nil {
|
||||
zap.S().Error("Error while decoding trial details", err)
|
||||
// If there is an error in fetching trial details, we will still return the license details
|
||||
// to avoid incorrectly blocking the UI
|
||||
ah.Respond(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.TrialStart = trialRespData.Data.TrialStart
|
||||
resp.TrialEnd = trialRespData.Data.TrialEnd
|
||||
resp.OnTrial = trialRespData.Data.OnTrial
|
||||
resp.WorkSpaceBlock = trialRespData.Data.WorkSpaceBlock
|
||||
|
||||
ah.Respond(w, resp)
|
||||
}
|
||||
|
@ -217,6 +217,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
DialTimeout: serverOptions.DialTimeout,
|
||||
AppDao: modelDao,
|
||||
RulesManager: rm,
|
||||
UsageManager: usageManager,
|
||||
FeatureFlags: lm,
|
||||
LicenseManager: lm,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
|
@ -9,6 +9,7 @@ const (
|
||||
)
|
||||
|
||||
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
||||
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
||||
|
||||
var SpanLimitStr = GetOrDefaultEnv("SPAN_LIMIT", "5000")
|
||||
|
||||
|
@ -89,3 +89,16 @@ func (l *License) ParseFeatures() {
|
||||
l.FeatureSet = BasicPlan
|
||||
}
|
||||
}
|
||||
|
||||
type Licenses struct {
|
||||
TrialStart int64 `json:"trialStart"`
|
||||
TrialEnd int64 `json:"trialEnd"`
|
||||
OnTrial bool `json:"onTrial"`
|
||||
WorkSpaceBlock bool `json:"workSpaceBlock"`
|
||||
Licenses []License `json:"licenses"`
|
||||
}
|
||||
|
||||
type SubscriptionServerResp struct {
|
||||
Status string `json:"status"`
|
||||
Data Licenses `json:"data"`
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
node_modules
|
||||
.vscode
|
||||
build
|
||||
.git
|
||||
|
@ -1,38 +1,17 @@
|
||||
# Builder stage
|
||||
FROM node:16.15.0 as builder
|
||||
FROM nginx:1.25.2-alpine
|
||||
|
||||
# Add Maintainer Info
|
||||
LABEL maintainer="signoz"
|
||||
|
||||
ARG TARGETOS=linux
|
||||
ARG TARGETARCH
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /frontend
|
||||
|
||||
# Copy the package.json and .yarnrc files prior to install dependencies
|
||||
COPY package.json ./
|
||||
# Copy lock file
|
||||
COPY yarn.lock ./
|
||||
COPY .yarnrc ./
|
||||
|
||||
# Install the dependencies and make the folder
|
||||
RUN CI=1 yarn install
|
||||
|
||||
COPY . .
|
||||
|
||||
# Build the project and copy the files
|
||||
RUN yarn build
|
||||
|
||||
|
||||
FROM nginx:1.25.2-alpine
|
||||
|
||||
COPY conf/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Remove default nginx index page
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
|
||||
# Copy from the stahg 1
|
||||
COPY --from=builder /frontend/build /usr/share/nginx/html
|
||||
# Copy custom nginx config and static files
|
||||
COPY conf/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY build /usr/share/nginx/html
|
||||
|
||||
EXPOSE 3301
|
||||
|
||||
|
@ -13,5 +13,12 @@
|
||||
"import_dashboard_by_pasting": "Import dashboard by pasting JSON or importing JSON file",
|
||||
"error_loading_json": "Error loading JSON file",
|
||||
"empty_json_not_allowed": "Empty JSON is not allowed",
|
||||
"new_dashboard_title": "Sample Title"
|
||||
"new_dashboard_title": "Sample Title",
|
||||
"layout_saved_successfully": "Layout saved successfully",
|
||||
"add_panel": "Add Panel",
|
||||
"save_layout": "Save Layout",
|
||||
"variable_updated_successfully": "Variable updated successfully",
|
||||
"error_while_updating_variable": "Error while updating variable",
|
||||
"dashboard_has_been_updated": "Dashboard has been updated",
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?"
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
{
|
||||
"general": "General",
|
||||
"alert_channels": "Alert Channels",
|
||||
"organization_settings": "Organization Settings",
|
||||
"my_settings": "My Settings",
|
||||
"overview_metrics": "Overview Metrics",
|
||||
"dbcall_metrics": "Database Calls",
|
||||
"external_metrics": "External Calls",
|
||||
"pipeline": "Pipeline",
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics"
|
||||
}
|
||||
{
|
||||
"general": "General",
|
||||
"alert_channels": "Alert Channels",
|
||||
"organization_settings": "Organization Settings",
|
||||
"ingestion_settings": "Ingestion Settings",
|
||||
"my_settings": "My Settings",
|
||||
"overview_metrics": "Overview Metrics",
|
||||
"dbcall_metrics": "Database Calls",
|
||||
"external_metrics": "External Calls",
|
||||
"pipeline": "Pipeline",
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics"
|
||||
}
|
||||
|
@ -1,37 +1,38 @@
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"GET_STARTED": "SigNoz | Get Started",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
"SETTINGS": "SigNoz | Settings",
|
||||
"USAGE_EXPLORER": "SigNoz | Usage Explorer",
|
||||
"APPLICATION": "SigNoz | Home",
|
||||
"ALL_DASHBOARD": "SigNoz | All Dashboards",
|
||||
"DASHBOARD": "SigNoz | Dashboard",
|
||||
"DASHBOARD_WIDGET": "SigNoz | Dashboard Widget",
|
||||
"EDIT_ALERTS": "SigNoz | Edit Alerts",
|
||||
"LIST_ALL_ALERT": "SigNoz | All Alerts",
|
||||
"ALERTS_NEW": "SigNoz | New Alert",
|
||||
"ALL_CHANNELS": "SigNoz | All Channels",
|
||||
"CHANNELS_NEW": "SigNoz | New Channel",
|
||||
"CHANNELS_EDIT": "SigNoz | Edit Channel",
|
||||
"ALL_ERROR": "SigNoz | All Errors",
|
||||
"ERROR_DETAIL": "SigNoz | Error Detail",
|
||||
"VERSION": "SigNoz | Version",
|
||||
"MY_SETTINGS": "SigNoz | My Settings",
|
||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
}
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"GET_STARTED": "SigNoz | Get Started",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
"SETTINGS": "SigNoz | Settings",
|
||||
"USAGE_EXPLORER": "SigNoz | Usage Explorer",
|
||||
"APPLICATION": "SigNoz | Home",
|
||||
"ALL_DASHBOARD": "SigNoz | All Dashboards",
|
||||
"DASHBOARD": "SigNoz | Dashboard",
|
||||
"DASHBOARD_WIDGET": "SigNoz | Dashboard Widget",
|
||||
"EDIT_ALERTS": "SigNoz | Edit Alerts",
|
||||
"LIST_ALL_ALERT": "SigNoz | All Alerts",
|
||||
"ALERTS_NEW": "SigNoz | New Alert",
|
||||
"ALL_CHANNELS": "SigNoz | All Channels",
|
||||
"CHANNELS_NEW": "SigNoz | New Channel",
|
||||
"CHANNELS_EDIT": "SigNoz | Edit Channel",
|
||||
"ALL_ERROR": "SigNoz | All Errors",
|
||||
"ERROR_DETAIL": "SigNoz | Error Detail",
|
||||
"VERSION": "SigNoz | Version",
|
||||
"MY_SETTINGS": "SigNoz | My Settings",
|
||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
|
||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
}
|
||||
|
@ -13,5 +13,12 @@
|
||||
"import_dashboard_by_pasting": "Import dashboard by pasting JSON or importing JSON file",
|
||||
"error_loading_json": "Error loading JSON file",
|
||||
"empty_json_not_allowed": "Empty JSON is not allowed",
|
||||
"new_dashboard_title": "Sample Title"
|
||||
"new_dashboard_title": "Sample Title",
|
||||
"layout_saved_successfully": "Layout saved successfully",
|
||||
"add_panel": "Add Panel",
|
||||
"save_layout": "Save Layout",
|
||||
"variable_updated_successfully": "Variable updated successfully",
|
||||
"error_while_updating_variable": "Error while updating variable",
|
||||
"dashboard_has_been_updated": "Dashboard has been updated",
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?"
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
{
|
||||
"general": "General",
|
||||
"alert_channels": "Alert Channels",
|
||||
"organization_settings": "Organization Settings",
|
||||
"my_settings": "My Settings",
|
||||
"overview_metrics": "Overview Metrics",
|
||||
"dbcall_metrics": "Database Calls",
|
||||
"external_metrics": "External Calls",
|
||||
"pipeline": "Pipeline",
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics"
|
||||
}
|
||||
{
|
||||
"general": "General",
|
||||
"alert_channels": "Alert Channels",
|
||||
"organization_settings": "Organization Settings",
|
||||
"ingestion_settings": "Ingestion Settings",
|
||||
"my_settings": "My Settings",
|
||||
"overview_metrics": "Overview Metrics",
|
||||
"dbcall_metrics": "Database Calls",
|
||||
"external_metrics": "External Calls",
|
||||
"pipeline": "Pipeline",
|
||||
"pipelines": "Pipelines",
|
||||
"archives": "Archives",
|
||||
"logs_to_metrics": "Logs To Metrics"
|
||||
}
|
||||
|
@ -1,37 +1,38 @@
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"GET_STARTED": "SigNoz | Get Started",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
"SETTINGS": "SigNoz | Settings",
|
||||
"USAGE_EXPLORER": "SigNoz | Usage Explorer",
|
||||
"APPLICATION": "SigNoz | Home",
|
||||
"ALL_DASHBOARD": "SigNoz | All Dashboards",
|
||||
"DASHBOARD": "SigNoz | Dashboard",
|
||||
"DASHBOARD_WIDGET": "SigNoz | Dashboard Widget",
|
||||
"EDIT_ALERTS": "SigNoz | Edit Alerts",
|
||||
"LIST_ALL_ALERT": "SigNoz | All Alerts",
|
||||
"ALERTS_NEW": "SigNoz | New Alert",
|
||||
"ALL_CHANNELS": "SigNoz | All Channels",
|
||||
"CHANNELS_NEW": "SigNoz | New Channel",
|
||||
"CHANNELS_EDIT": "SigNoz | Edit Channel",
|
||||
"ALL_ERROR": "SigNoz | All Errors",
|
||||
"ERROR_DETAIL": "SigNoz | Error Detail",
|
||||
"VERSION": "SigNoz | Version",
|
||||
"MY_SETTINGS": "SigNoz | My Settings",
|
||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
}
|
||||
{
|
||||
"SIGN_UP": "SigNoz | Sign Up",
|
||||
"LOGIN": "SigNoz | Login",
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"GET_STARTED": "SigNoz | Get Started",
|
||||
"TRACE": "SigNoz | Trace",
|
||||
"TRACE_DETAIL": "SigNoz | Trace Detail",
|
||||
"TRACES_EXPLORER": "SigNoz | Traces Explorer",
|
||||
"SETTINGS": "SigNoz | Settings",
|
||||
"USAGE_EXPLORER": "SigNoz | Usage Explorer",
|
||||
"APPLICATION": "SigNoz | Home",
|
||||
"ALL_DASHBOARD": "SigNoz | All Dashboards",
|
||||
"DASHBOARD": "SigNoz | Dashboard",
|
||||
"DASHBOARD_WIDGET": "SigNoz | Dashboard Widget",
|
||||
"EDIT_ALERTS": "SigNoz | Edit Alerts",
|
||||
"LIST_ALL_ALERT": "SigNoz | All Alerts",
|
||||
"ALERTS_NEW": "SigNoz | New Alert",
|
||||
"ALL_CHANNELS": "SigNoz | All Channels",
|
||||
"CHANNELS_NEW": "SigNoz | New Channel",
|
||||
"CHANNELS_EDIT": "SigNoz | Edit Channel",
|
||||
"ALL_ERROR": "SigNoz | All Errors",
|
||||
"ERROR_DETAIL": "SigNoz | Error Detail",
|
||||
"VERSION": "SigNoz | Version",
|
||||
"MY_SETTINGS": "SigNoz | My Settings",
|
||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
|
||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
|
||||
import { NotificationProvider } from 'hooks/useNotifications';
|
||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||
import history from 'lib/history';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@ -110,22 +111,24 @@ function App(): JSX.Element {
|
||||
<PrivateRoute>
|
||||
<ResourceProvider>
|
||||
<QueryBuilderProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
<DashboardProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</DashboardProvider>
|
||||
</QueryBuilderProvider>
|
||||
</ResourceProvider>
|
||||
</PrivateRoute>
|
||||
|
@ -102,6 +102,10 @@ export const OrganizationSettings = Loadable(
|
||||
() => import(/* webpackChunkName: "All Settings" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const IngestionSettings = Loadable(
|
||||
() => import(/* webpackChunkName: "Ingestion Settings" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const MySettings = Loadable(
|
||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
EditAlertChannelsAlerts,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
IngestionSettings,
|
||||
LicensePage,
|
||||
ListAllALertsPage,
|
||||
LiveLogs,
|
||||
@ -214,6 +215,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'ORG_SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.INGESTION_SETTINGS,
|
||||
exact: true,
|
||||
component: IngestionSettings,
|
||||
isPrivate: true,
|
||||
key: 'INGESTION_SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.MY_SETTINGS,
|
||||
exact: true,
|
||||
|
@ -1,24 +1,9 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Props } from 'types/api/dashboard/delete';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/delete';
|
||||
|
||||
const deleteDashboard = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<undefined> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/dashboards/${props.uuid}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
const deleteDashboard = (props: Props): Promise<PayloadProps> =>
|
||||
axios
|
||||
.delete<PayloadProps>(`/dashboards/${props.uuid}`)
|
||||
.then((response) => response.data);
|
||||
|
||||
export default deleteDashboard;
|
||||
|
@ -1,24 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/get';
|
||||
import { ApiResponse } from 'types/api';
|
||||
import { Props } from 'types/api/dashboard/get';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const get = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`/dashboards/${props.uuid}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
const get = (props: Props): Promise<Dashboard> =>
|
||||
axios
|
||||
.get<ApiResponse<Dashboard>>(`/dashboards/${props.uuid}`)
|
||||
.then((res) => res.data.data);
|
||||
|
||||
export default get;
|
||||
|
21
frontend/src/api/pipeline/preview.ts
Normal file
21
frontend/src/api/pipeline/preview.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import axios from 'api';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { PipelineData } from 'types/api/pipeline/def';
|
||||
|
||||
export interface PipelineSimulationRequest {
|
||||
logs: ILog[];
|
||||
pipelines: PipelineData[];
|
||||
}
|
||||
|
||||
export interface PipelineSimulationResponse {
|
||||
logs: ILog[];
|
||||
}
|
||||
|
||||
const simulatePipelineProcessing = async (
|
||||
requestBody: PipelineSimulationRequest,
|
||||
): Promise<PipelineSimulationResponse> =>
|
||||
axios
|
||||
.post('/logs/pipelines/preview', requestBody)
|
||||
.then((res) => res.data.data);
|
||||
|
||||
export default simulatePipelineProcessing;
|
24
frontend/src/api/settings/getIngestionData.ts
Normal file
24
frontend/src/api/settings/getIngestionData.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { IngestionResponseProps } from 'types/api/settings/ingestion';
|
||||
|
||||
const getIngestionData = async (): Promise<
|
||||
SuccessResponse<IngestionResponseProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/settings/ingestion_key`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getIngestionData;
|
@ -1,10 +1,18 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { CodeProps } from 'react-markdown/lib/ast-to-react';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||
|
||||
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
|
||||
|
||||
interface LinkProps {
|
||||
href: string;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
function Pre({ children }: { children: React.ReactNode }): JSX.Element {
|
||||
return (
|
||||
<pre className="code-snippet-container">
|
||||
@ -40,4 +48,54 @@ function Code({
|
||||
);
|
||||
}
|
||||
|
||||
export { Code, Pre };
|
||||
function Link({ href, children }: LinkProps): JSX.Element {
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const interpolateMarkdown = (
|
||||
markdownContent: any,
|
||||
variables: { [s: string]: unknown } | ArrayLike<unknown>,
|
||||
) => {
|
||||
let interpolatedContent = markdownContent;
|
||||
|
||||
const variableEntries = Object.entries(variables);
|
||||
|
||||
// Loop through variables and replace placeholders with values
|
||||
for (const [key, value] of variableEntries) {
|
||||
const placeholder = `{{${key}}}`;
|
||||
const regex = new RegExp(placeholder, 'g');
|
||||
interpolatedContent = interpolatedContent.replace(regex, value);
|
||||
}
|
||||
|
||||
return interpolatedContent;
|
||||
};
|
||||
|
||||
function MarkdownRenderer({
|
||||
markdownContent,
|
||||
variables,
|
||||
}: {
|
||||
markdownContent: any;
|
||||
variables: any;
|
||||
}): JSX.Element {
|
||||
const interpolatedMarkdown = interpolateMarkdown(markdownContent, variables);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
a: Link,
|
||||
pre: Pre,
|
||||
code: Code,
|
||||
}}
|
||||
>
|
||||
{interpolatedMarkdown}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
export { Code, Link, MarkdownRenderer, Pre };
|
||||
|
@ -0,0 +1,3 @@
|
||||
.overlay--text-wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
89
frontend/src/components/TextToolTip/TextToolTip.tsx
Normal file
89
frontend/src/components/TextToolTip/TextToolTip.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import './TextToolTip.style.scss';
|
||||
|
||||
import { blue, grey } from '@ant-design/colors';
|
||||
import {
|
||||
QuestionCircleFilled,
|
||||
QuestionCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useMemo } from 'react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { style } from './constant';
|
||||
|
||||
function TextToolTip({
|
||||
text,
|
||||
url,
|
||||
useFilledIcon = true,
|
||||
urlText,
|
||||
}: TextToolTipProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onClickHandler = (
|
||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const overlay = useMemo(
|
||||
() => (
|
||||
<div className="overlay--text-wrap">
|
||||
{`${text} `}
|
||||
{url && (
|
||||
<a
|
||||
// Stopping event propagation on click so that parent click listener are not triggered
|
||||
onClick={onClickHandler}
|
||||
href={url}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{urlText || 'here'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
[text, url, urlText],
|
||||
);
|
||||
|
||||
const iconStyle = useMemo(
|
||||
() => ({
|
||||
...style,
|
||||
color: isDarkMode ? themeColors.whiteCream : grey[0],
|
||||
}),
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
const iconOutlinedStyle = useMemo(
|
||||
() => ({
|
||||
...style,
|
||||
color: isDarkMode ? themeColors.navyBlue : blue[6],
|
||||
}),
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip getTooltipContainer={popupContainer} overlay={overlay}>
|
||||
{useFilledIcon ? (
|
||||
<QuestionCircleFilled style={iconStyle} />
|
||||
) : (
|
||||
<QuestionCircleOutlined style={iconOutlinedStyle} />
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
TextToolTip.defaultProps = {
|
||||
url: '',
|
||||
urlText: '',
|
||||
useFilledIcon: true,
|
||||
};
|
||||
interface TextToolTipProps {
|
||||
url?: string;
|
||||
text: string;
|
||||
useFilledIcon?: boolean;
|
||||
urlText?: string;
|
||||
}
|
||||
|
||||
export default TextToolTip;
|
@ -1,87 +1,3 @@
|
||||
import { blue, grey } from '@ant-design/colors';
|
||||
import {
|
||||
QuestionCircleFilled,
|
||||
QuestionCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useMemo } from 'react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { style } from './styles';
|
||||
|
||||
function TextToolTip({
|
||||
text,
|
||||
url,
|
||||
useFilledIcon = true,
|
||||
urlText,
|
||||
}: TextToolTipProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onClickHandler = (
|
||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const overlay = useMemo(
|
||||
() => (
|
||||
<div>
|
||||
{`${text} `}
|
||||
{url && (
|
||||
<a
|
||||
// Stopping event propagation on click so that parent click listener are not triggered
|
||||
onClick={onClickHandler}
|
||||
href={url}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{urlText || 'here'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
[text, url, urlText],
|
||||
);
|
||||
|
||||
const iconStyle = useMemo(
|
||||
() => ({
|
||||
...style,
|
||||
color: isDarkMode ? themeColors.whiteCream : grey[0],
|
||||
}),
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
const iconOutlinedStyle = useMemo(
|
||||
() => ({
|
||||
...style,
|
||||
color: isDarkMode ? themeColors.navyBlue : blue[6],
|
||||
}),
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip getTooltipContainer={popupContainer} overlay={overlay}>
|
||||
{useFilledIcon ? (
|
||||
<QuestionCircleFilled style={iconStyle} />
|
||||
) : (
|
||||
<QuestionCircleOutlined style={iconOutlinedStyle} />
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
TextToolTip.defaultProps = {
|
||||
url: '',
|
||||
urlText: '',
|
||||
useFilledIcon: true,
|
||||
};
|
||||
interface TextToolTipProps {
|
||||
url?: string;
|
||||
text: string;
|
||||
useFilledIcon?: boolean;
|
||||
urlText?: string;
|
||||
}
|
||||
import TextToolTip from './TextToolTip';
|
||||
|
||||
export default TextToolTip;
|
||||
|
@ -3,5 +3,8 @@ export const REACT_QUERY_KEY = {
|
||||
GET_QUERY_RANGE: 'GET_QUERY_RANGE',
|
||||
GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS',
|
||||
GET_TRIGGERED_ALERTS: 'GET_TRIGGERED_ALERTS',
|
||||
DASHBOARD_BY_ID: 'DASHBOARD_BY_ID',
|
||||
GET_FEATURES_FLAGS: 'GET_FEATURES_FLAGS',
|
||||
DELETE_DASHBOARD: 'DELETE_DASHBOARD',
|
||||
LOGS_PIPELINE_PREVIEW: 'LOGS_PIPELINE_PREVIEW',
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ const ROUTES = {
|
||||
VERSION: '/status',
|
||||
MY_SETTINGS: '/my-settings',
|
||||
ORG_SETTINGS: '/settings/org-settings',
|
||||
INGESTION_SETTINGS: '/settings/ingestion-settings',
|
||||
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
||||
UN_AUTHORIZED: '/un-authorized',
|
||||
NOT_FOUND: '/not-found',
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
initialQueryPromQLData,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
AlertDef,
|
||||
@ -77,7 +78,7 @@ export const logAlertDefaults: AlertDef = {
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
details: `${window.location.protocol}//${window.location.host}/logs`,
|
||||
details: `${window.location.protocol}//${window.location.host}${ROUTES.LOGS_EXPLORER}`,
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
|
@ -25,6 +25,7 @@ export interface ChartPreviewProps {
|
||||
headline?: JSX.Element;
|
||||
alertDef?: AlertDef;
|
||||
userQueryKey?: string;
|
||||
allowSelectedIntervalForStepGen?: boolean;
|
||||
}
|
||||
|
||||
function ChartPreview({
|
||||
@ -35,6 +36,7 @@ function ChartPreview({
|
||||
selectedInterval = '5min',
|
||||
headline,
|
||||
userQueryKey,
|
||||
allowSelectedIntervalForStepGen = false,
|
||||
alertDef,
|
||||
}: ChartPreviewProps): JSX.Element | null {
|
||||
const { t } = useTranslation('alerts');
|
||||
@ -89,6 +91,9 @@ function ChartPreview({
|
||||
globalSelectedInterval: selectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
params: {
|
||||
allowSelectedIntervalForStepGen,
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
@ -127,7 +132,7 @@ function ChartPreview({
|
||||
<GridPanelSwitch
|
||||
panelType={graphType}
|
||||
title={name}
|
||||
data={chartDataSet}
|
||||
data={chartDataSet.data}
|
||||
isStacked
|
||||
name={name || 'Chart Preview'}
|
||||
staticLine={staticLine}
|
||||
@ -146,6 +151,7 @@ ChartPreview.defaultProps = {
|
||||
selectedInterval: '5min',
|
||||
headline: undefined,
|
||||
userQueryKey: '',
|
||||
allowSelectedIntervalForStepGen: false,
|
||||
alertDef: undefined,
|
||||
};
|
||||
|
||||
|
@ -44,7 +44,7 @@ import {
|
||||
StyledLeftContainer,
|
||||
} from './styles';
|
||||
import UserGuide from './UserGuide';
|
||||
import { toChartInterval } from './utils';
|
||||
import { getUpdatedStepInterval, toChartInterval } from './utils';
|
||||
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
@ -354,6 +354,16 @@ function FormAlertRules({
|
||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||
);
|
||||
|
||||
const updatedStagedQuery = useMemo((): Query | null => {
|
||||
const newQuery: Query | null = stagedQuery;
|
||||
if (newQuery) {
|
||||
newQuery.builder.queryData[0].stepInterval = getUpdatedStepInterval(
|
||||
alertDef.evalWindow,
|
||||
);
|
||||
}
|
||||
return newQuery;
|
||||
}, [alertDef.evalWindow, stagedQuery]);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={
|
||||
@ -363,9 +373,10 @@ function FormAlertRules({
|
||||
/>
|
||||
}
|
||||
name=""
|
||||
query={stagedQuery}
|
||||
query={updatedStagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
alertDef={alertDef}
|
||||
allowSelectedIntervalForStepGen
|
||||
/>
|
||||
);
|
||||
|
||||
|
14
frontend/src/container/FormAlertRules/utils.test.ts
Normal file
14
frontend/src/container/FormAlertRules/utils.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// Write a test for getUpdatedStepInterval function in src/container/FormAlertRules/utils.ts
|
||||
|
||||
import { getUpdatedStepInterval } from './utils';
|
||||
|
||||
describe('getUpdatedStepInterval', () => {
|
||||
it('should return 60', () => {
|
||||
const result = getUpdatedStepInterval('5m0s');
|
||||
expect(result).toEqual(60);
|
||||
});
|
||||
it('should return 60 for 10m0s', () => {
|
||||
const result = getUpdatedStepInterval('10m0s');
|
||||
expect(result).toEqual(60);
|
||||
});
|
||||
});
|
@ -1,4 +1,6 @@
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import getStep from 'lib/getStep';
|
||||
|
||||
// toChartInterval converts eval window to chart selection time interval
|
||||
export const toChartInterval = (evalWindow: string | undefined): Time => {
|
||||
@ -21,3 +23,15 @@ export const toChartInterval = (evalWindow: string | undefined): Time => {
|
||||
return '5min';
|
||||
}
|
||||
};
|
||||
|
||||
export const getUpdatedStepInterval = (evalWindow?: string): number => {
|
||||
const { start, end } = getStartEndRangeTime({
|
||||
type: 'GLOBAL_TIME',
|
||||
interval: toChartInterval(evalWindow),
|
||||
});
|
||||
return getStep({
|
||||
start,
|
||||
end,
|
||||
inputFormat: 'ns',
|
||||
});
|
||||
};
|
||||
|
@ -0,0 +1,21 @@
|
||||
.graph-manager-container {
|
||||
margin-top: 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
overflow-x: scroll;
|
||||
|
||||
.filter-table-container {
|
||||
flex-basis: 80%;
|
||||
}
|
||||
|
||||
.save-cancel-container {
|
||||
flex-basis: 20%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.save-cancel-button {
|
||||
margin: 0 0.313rem;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
import './GraphManager.styles.scss';
|
||||
|
||||
import { Button, Input } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
|
||||
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
||||
import {
|
||||
getDefaultTableDataSet,
|
||||
saveLegendEntriesToLocalStorage,
|
||||
} from './utils';
|
||||
|
||||
function GraphManager({
|
||||
data,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onToggleModelHandler,
|
||||
setGraphsVisibilityStates,
|
||||
graphsVisibilityStates = [],
|
||||
lineChartRef,
|
||||
parentChartRef,
|
||||
}: GraphManagerProps): JSX.Element {
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
|
||||
getDefaultTableDataSet(data),
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const checkBoxOnChangeHandler = useCallback(
|
||||
(e: CheckboxChangeEvent, index: number): void => {
|
||||
const newStates = [...graphsVisibilityStates];
|
||||
|
||||
newStates[index] = e.target.checked;
|
||||
|
||||
lineChartRef?.current?.toggleGraph(index, e.target.checked);
|
||||
|
||||
setGraphsVisibilityStates([...newStates]);
|
||||
},
|
||||
[graphsVisibilityStates, setGraphsVisibilityStates, lineChartRef],
|
||||
);
|
||||
|
||||
const labelClickedHandler = useCallback(
|
||||
(labelIndex: number): void => {
|
||||
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||
false,
|
||||
);
|
||||
newGraphVisibilityStates[labelIndex] = true;
|
||||
|
||||
newGraphVisibilityStates.forEach((state, index) => {
|
||||
lineChartRef?.current?.toggleGraph(index, state);
|
||||
parentChartRef?.current?.toggleGraph(index, state);
|
||||
});
|
||||
setGraphsVisibilityStates(newGraphVisibilityStates);
|
||||
},
|
||||
[
|
||||
data.datasets.length,
|
||||
setGraphsVisibilityStates,
|
||||
lineChartRef,
|
||||
parentChartRef,
|
||||
],
|
||||
);
|
||||
|
||||
const columns = getGraphManagerTableColumns({
|
||||
data,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState: graphsVisibilityStates || [],
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
});
|
||||
|
||||
const filterHandler = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = event.target.value.toString().toLowerCase();
|
||||
const updatedDataSet = tableDataSet.map((item) => {
|
||||
if (item.label?.toLocaleLowerCase().includes(value)) {
|
||||
return { ...item, show: true };
|
||||
}
|
||||
return { ...item, show: false };
|
||||
});
|
||||
setTableDataSet(updatedDataSet);
|
||||
},
|
||||
[tableDataSet],
|
||||
);
|
||||
|
||||
const saveHandler = useCallback((): void => {
|
||||
saveLegendEntriesToLocalStorage({
|
||||
data,
|
||||
graphVisibilityState: graphsVisibilityStates || [],
|
||||
name,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'The updated graphs & legends are saved',
|
||||
});
|
||||
if (onToggleModelHandler) {
|
||||
onToggleModelHandler();
|
||||
}
|
||||
}, [data, graphsVisibilityStates, name, notifications, onToggleModelHandler]);
|
||||
|
||||
const dataSource = tableDataSet.filter((item) => item.show);
|
||||
|
||||
return (
|
||||
<div className="graph-manager-container">
|
||||
<div className="filter-table-container">
|
||||
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
rowKey="index"
|
||||
pagination={false}
|
||||
scroll={{ y: 240 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="save-cancel-container">
|
||||
<span className="save-cancel-button">
|
||||
<Button type="default" onClick={onToggleModelHandler}>
|
||||
Cancel
|
||||
</Button>
|
||||
</span>
|
||||
<span className="save-cancel-button">
|
||||
<Button onClick={saveHandler} type="primary">
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
GraphManager.defaultProps = {
|
||||
graphVisibilityStateHandler: undefined,
|
||||
};
|
||||
|
||||
export default memo(GraphManager);
|
@ -1,3 +1,4 @@
|
||||
import { grey } from '@ant-design/colors';
|
||||
import { Checkbox, ConfigProvider } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
|
||||
@ -6,7 +7,7 @@ import { CheckBoxProps } from '../types';
|
||||
function CustomCheckBox({
|
||||
data,
|
||||
index,
|
||||
graphVisibilityState,
|
||||
graphVisibilityState = [],
|
||||
checkBoxOnChangeHandler,
|
||||
}: CheckBoxProps): JSX.Element {
|
||||
const { datasets } = data;
|
||||
@ -15,17 +16,21 @@ function CustomCheckBox({
|
||||
checkBoxOnChangeHandler(e, index);
|
||||
};
|
||||
|
||||
const color = datasets[index]?.borderColor?.toString() || grey[0];
|
||||
|
||||
const isChecked = graphVisibilityState[index] || false;
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: datasets[index].borderColor?.toString(),
|
||||
colorBorder: datasets[index].borderColor?.toString(),
|
||||
colorBgContainer: datasets[index].borderColor?.toString(),
|
||||
colorPrimary: color,
|
||||
colorBorder: color,
|
||||
colorBgContainer: color,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Checkbox onChange={onChangeHandler} checked={graphVisibilityState[index]} />
|
||||
<Checkbox onChange={onChangeHandler} checked={isChecked} />
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
@ -5,7 +5,7 @@ import { ChartData } from 'chart.js';
|
||||
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
|
||||
import { DataSetProps } from '../types';
|
||||
import { getGraphManagerTableHeaderTitle } from '../utils';
|
||||
import { getCheckBox } from './GetCheckBox';
|
||||
import CustomCheckBox from './CustomCheckBox';
|
||||
import { getLabel } from './GetLabel';
|
||||
|
||||
export const getGraphManagerTableColumns = ({
|
||||
@ -20,11 +20,14 @@ export const getGraphManagerTableColumns = ({
|
||||
width: 50,
|
||||
dataIndex: ColumnsKeyAndDataIndex.Index,
|
||||
key: ColumnsKeyAndDataIndex.Index,
|
||||
...getCheckBox({
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState,
|
||||
data,
|
||||
}),
|
||||
render: (_: string, __: DataSetProps, index: number): JSX.Element => (
|
||||
<CustomCheckBox
|
||||
data={data}
|
||||
index={index}
|
||||
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
|
||||
graphVisibilityState={graphVisibilityState}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: ColumnsTitle[ColumnsKeyAndDataIndex.Label],
|
@ -12,17 +12,16 @@ import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { toggleGraphsVisibilityInChart } from '../utils';
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||
import GraphManager from './GraphManager';
|
||||
import { GraphContainer, TimeContainer } from './styles';
|
||||
import { FullViewProps } from './types';
|
||||
import { getIsGraphLegendToggleAvailable } from './utils';
|
||||
|
||||
function FullView({
|
||||
widget,
|
||||
@ -34,45 +33,29 @@ function FullView({
|
||||
isDependedDataLoaded = false,
|
||||
graphsVisibilityStates,
|
||||
onToggleModelHandler,
|
||||
setGraphsVisibilityStates,
|
||||
parentChartRef,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const getSelectedTime = useCallback(
|
||||
() =>
|
||||
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
|
||||
[widget],
|
||||
);
|
||||
|
||||
const canModifyChart = useChartMutable({
|
||||
panelType: widget.panelTypes,
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
});
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
|
||||
useEffect(() => {
|
||||
if (graphsVisibilityStates && canModifyChart && lineChartRef.current) {
|
||||
toggleGraphsVisibilityInChart({
|
||||
graphsVisibilityStates,
|
||||
lineChartRef,
|
||||
});
|
||||
}
|
||||
}, [graphsVisibilityStates, canModifyChart]);
|
||||
|
||||
const [selectedTime, setSelectedTime] = useState<timePreferance>({
|
||||
name: getSelectedTime()?.name || '',
|
||||
enum: widget?.timePreferance || 'GLOBAL_TIME',
|
||||
});
|
||||
|
||||
const queryKey = useMemo(
|
||||
() =>
|
||||
`FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
|
||||
[selectedTime, globalSelectedTime, widget],
|
||||
);
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
|
||||
const response = useGetQueryRange(
|
||||
@ -81,14 +64,19 @@ function FullView({
|
||||
graphType: widget.panelTypes,
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(),
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
},
|
||||
{
|
||||
queryKey,
|
||||
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
|
||||
enabled: !isDependedDataLoaded,
|
||||
},
|
||||
);
|
||||
|
||||
const canModifyChart = useChartMutable({
|
||||
panelType: widget.panelTypes,
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
});
|
||||
|
||||
const chartDataSet = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
@ -101,9 +89,14 @@ function FullView({
|
||||
[response],
|
||||
);
|
||||
|
||||
const isGraphLegendToggleAvailable = getIsGraphLegendToggleAvailable(
|
||||
widget.panelTypes,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!response.isFetching && lineChartRef.current) {
|
||||
graphsVisibilityStates?.forEach((e, i) => {
|
||||
lineChartRef?.current?.toggleGraph(i, e);
|
||||
parentChartRef?.current?.toggleGraph(i, e);
|
||||
});
|
||||
}
|
||||
}, [graphsVisibilityStates, parentChartRef, response.isFetching]);
|
||||
|
||||
if (response.isFetching) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
@ -128,10 +121,10 @@ function FullView({
|
||||
</TimeContainer>
|
||||
)}
|
||||
|
||||
<GraphContainer isGraphLegendToggleAvailable={isGraphLegendToggleAvailable}>
|
||||
<GraphContainer isGraphLegendToggleAvailable={canModifyChart}>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
data={chartDataSet.data}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
@ -147,10 +140,14 @@ function FullView({
|
||||
|
||||
{canModifyChart && (
|
||||
<GraphManager
|
||||
data={chartDataSet}
|
||||
data={chartDataSet.data}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
||||
graphsVisibilityStates={graphsVisibilityStates}
|
||||
lineChartRef={lineChartRef}
|
||||
parentChartRef={parentChartRef}
|
||||
/>
|
||||
)}
|
||||
</>
|
@ -31,26 +31,6 @@ export const GraphContainer = styled.div<GraphContainerProps>`
|
||||
isGraphLegendToggleAvailable ? '50%' : '100%'};
|
||||
`;
|
||||
|
||||
export const FilterTableAndSaveContainer = styled.div`
|
||||
margin-top: 1.875rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
`;
|
||||
|
||||
export const FilterTableContainer = styled.div`
|
||||
flex-basis: 80%;
|
||||
`;
|
||||
|
||||
export const SaveContainer = styled.div`
|
||||
flex-basis: 20%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
export const SaveCancelButtonContainer = styled.span`
|
||||
margin: 0 0.313rem;
|
||||
`;
|
||||
|
||||
export const LabelContainer = styled.button`
|
||||
max-width: 18.75rem;
|
||||
cursor: pointer;
|
@ -1,7 +1,8 @@
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ChartData, ChartDataset } from 'chart.js';
|
||||
import { GraphOnClickHandler } from 'components/Graph/types';
|
||||
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { MutableRefObject } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface DataSetProps {
|
||||
@ -40,20 +41,6 @@ export interface LabelProps {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface GraphManagerProps {
|
||||
data: ChartData;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onToggleModelHandler?: () => void;
|
||||
}
|
||||
|
||||
export interface CheckBoxProps {
|
||||
data: ChartData;
|
||||
index: number;
|
||||
graphVisibilityState: boolean[];
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
}
|
||||
|
||||
export interface FullViewProps {
|
||||
widget: Widgets;
|
||||
fullViewOptions?: boolean;
|
||||
@ -64,6 +51,26 @@ export interface FullViewProps {
|
||||
isDependedDataLoaded?: boolean;
|
||||
graphsVisibilityStates?: boolean[];
|
||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||
setGraphsVisibilityStates: (graphsVisibilityStates: boolean[]) => void;
|
||||
parentChartRef: GraphManagerProps['lineChartRef'];
|
||||
}
|
||||
|
||||
export interface GraphManagerProps {
|
||||
data: ChartData;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
onToggleModelHandler?: () => void;
|
||||
setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates'];
|
||||
graphsVisibilityStates: FullViewProps['graphsVisibilityStates'];
|
||||
lineChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
|
||||
parentChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
|
||||
}
|
||||
|
||||
export interface CheckBoxProps {
|
||||
data: ChartData;
|
||||
index: number;
|
||||
graphVisibilityState: boolean[];
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
}
|
||||
|
||||
export interface SaveLegendEntriesToLocalStoreProps {
|
@ -1,6 +1,5 @@
|
||||
import { ChartData, ChartDataset } from 'chart.js';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import {
|
||||
ExtendedChartDataset,
|
||||
@ -110,10 +109,6 @@ export const saveLegendEntriesToLocalStorage = ({
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsGraphLegendToggleAvailable = (
|
||||
panelType: PANEL_TYPES,
|
||||
): boolean => panelType === PANEL_TYPES.TIME_SERIES;
|
||||
|
||||
export const getGraphManagerTableHeaderTitle = (
|
||||
title: string,
|
||||
yAxisUnit?: string,
|
@ -0,0 +1,277 @@
|
||||
import { Typography } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView';
|
||||
import { FullViewContainer, Modal } from './styles';
|
||||
import { WidgetGraphComponentProps } from './types';
|
||||
import { getGraphVisibilityStateOnDataChange } from './utils';
|
||||
|
||||
function WidgetGraphComponent({
|
||||
data,
|
||||
widget,
|
||||
queryResponse,
|
||||
errorMessage,
|
||||
name,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
threshold,
|
||||
headerMenuList,
|
||||
isWarning,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [modal, setModal] = useState<boolean>(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { notifications } = useNotifications();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
|
||||
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
data,
|
||||
isExpandedName: true,
|
||||
name,
|
||||
}),
|
||||
[data, name],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lineChartRef.current) return;
|
||||
|
||||
localStoredVisibilityStates.forEach((state, index) => {
|
||||
lineChartRef.current?.toggleGraph(index, state);
|
||||
});
|
||||
}, [localStoredVisibilityStates]);
|
||||
|
||||
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
|
||||
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
|
||||
boolean[]
|
||||
>(localStoredVisibilityStates);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const onToggleModal = useCallback(
|
||||
(func: Dispatch<SetStateAction<boolean>>) => {
|
||||
func((value) => !value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
const onDeleteHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
|
||||
(e) => e.id !== widget.id,
|
||||
);
|
||||
|
||||
const updatedLayout =
|
||||
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
|
||||
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
layout: updatedLayout,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
featureResponse.refetch();
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onCloneHandler = async (): Promise<void> => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
const uuid = v4();
|
||||
|
||||
const layout = [
|
||||
...(selectedDashboard.data.layout || []),
|
||||
{
|
||||
i: uuid,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
y: 0,
|
||||
},
|
||||
];
|
||||
|
||||
updateDashboardMutation.mutateAsync(
|
||||
{
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
layout,
|
||||
widgets: [
|
||||
...(selectedDashboard.data.widgets || []),
|
||||
{
|
||||
...{
|
||||
...widget,
|
||||
id: uuid,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||
});
|
||||
const queryParams = {
|
||||
graphType: widget?.panelTypes,
|
||||
widgetId: uuid,
|
||||
};
|
||||
history.push(`${pathname}/new?${createQueryParams(queryParams)}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleOnView = (): void => {
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
const handleOnDelete = (): void => {
|
||||
onToggleModal(setDeleteModal);
|
||||
};
|
||||
|
||||
const onDeleteModelHandler = (): void => {
|
||||
onToggleModal(setDeleteModal);
|
||||
};
|
||||
|
||||
const onToggleModelHandler = (): void => {
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
onMouseOver={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseOut={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
onBlur={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
onCancel={onDeleteModelHandler}
|
||||
open={deleteModal}
|
||||
title="Delete"
|
||||
height="10vh"
|
||||
onOk={onDeleteHandler}
|
||||
centered
|
||||
>
|
||||
<Typography>Are you sure you want to delete this widget</Typography>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="View"
|
||||
footer={[]}
|
||||
centered
|
||||
open={modal}
|
||||
onCancel={onToggleModelHandler}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
>
|
||||
<FullViewContainer>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
widget={widget}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
graphsVisibilityStates={graphsVisibilityStates}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
||||
parentChartRef={lineChartRef}
|
||||
/>
|
||||
</FullViewContainer>
|
||||
</Modal>
|
||||
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
onClone={onCloneHandler}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
threshold={threshold}
|
||||
headerMenuList={headerMenuList}
|
||||
isWarning={isWarning}
|
||||
/>
|
||||
</div>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={data}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
ref={lineChartRef}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
WidgetGraphComponent.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
setLayout: undefined,
|
||||
onDragSelect: undefined,
|
||||
onClickHandler: undefined,
|
||||
};
|
||||
|
||||
export default WidgetGraphComponent;
|
134
frontend/src/container/GridCardLayout/GridCard/index.tsx
Normal file
134
frontend/src/container/GridCardLayout/GridCard/index.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import EmptyWidget from '../EmptyWidget';
|
||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||
import { GridCardGraphProps } from './types';
|
||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||
|
||||
function GridCardGraph({
|
||||
widget,
|
||||
name,
|
||||
onClickHandler,
|
||||
headerMenuList = [MenuItemKeys.View],
|
||||
isQueryEnabled,
|
||||
threshold,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const onDragSelect = (start: number, end: number): void => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
};
|
||||
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
initialInView: false,
|
||||
});
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
|
||||
const isEmptyWidget =
|
||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
selectedTime: widget?.timePreferance,
|
||||
graphType: widget?.panelTypes,
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
maxTime,
|
||||
minTime,
|
||||
globalSelectedInterval,
|
||||
selectedDashboard?.data?.variables,
|
||||
widget?.query,
|
||||
widget?.panelTypes,
|
||||
widget.timePreferance,
|
||||
],
|
||||
keepPreviousData: true,
|
||||
enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
setErrorMessage(error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
createDataset: undefined,
|
||||
isWarningLimit: true,
|
||||
}),
|
||||
[queryResponse],
|
||||
);
|
||||
|
||||
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
|
||||
|
||||
if (queryResponse.isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
<WidgetGraphComponent
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={chartData.data}
|
||||
isWarning={chartData.isWarning}
|
||||
name={name}
|
||||
onDragSelect={onDragSelect}
|
||||
threshold={threshold}
|
||||
headerMenuList={headerMenuList}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
|
||||
{isEmptyLayout && <EmptyWidget />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
GridCardGraph.defaultProps = {
|
||||
onDragSelect: undefined,
|
||||
onClickHandler: undefined,
|
||||
isQueryEnabled: true,
|
||||
threshold: undefined,
|
||||
headerMenuList: [MenuItemKeys.View],
|
||||
};
|
||||
|
||||
export default memo(GridCardGraph);
|
@ -1,15 +1,11 @@
|
||||
import { ChartData } from 'chart.js';
|
||||
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { MutableRefObject, ReactNode } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { DeleteWidgetProps } from 'store/actions/dashboard/deleteWidget';
|
||||
import AppActions from 'types/actions';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { LayoutProps } from '..';
|
||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||
import { LegendEntryProps } from './FullView/types';
|
||||
|
||||
@ -18,15 +14,7 @@ export interface GraphVisibilityLegendEntryProps {
|
||||
legendEntry: LegendEntryProps[];
|
||||
}
|
||||
|
||||
export interface DispatchProps {
|
||||
deleteWidget: ({
|
||||
widgetId,
|
||||
}: DeleteWidgetProps) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
export interface WidgetGraphComponentProps extends DispatchProps {
|
||||
enableModel: boolean;
|
||||
enableWidgetHeader: boolean;
|
||||
export interface WidgetGraphComponentProps {
|
||||
widget: Widgets;
|
||||
queryResponse: UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||
@ -34,21 +22,16 @@ export interface WidgetGraphComponentProps extends DispatchProps {
|
||||
errorMessage: string | undefined;
|
||||
data: ChartData;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
layout?: Layout[];
|
||||
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
threshold?: ReactNode;
|
||||
headerMenuList: MenuItemKeys[];
|
||||
isWarning: boolean;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
widget: Widgets;
|
||||
name: string;
|
||||
yAxisUnit: string | undefined;
|
||||
layout?: Layout[];
|
||||
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
threshold?: ReactNode;
|
141
frontend/src/container/GridCardLayout/GridCardLayout.tsx
Normal file
141
frontend/src/container/GridCardLayout/GridCardLayout.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { headerMenuList } from './config';
|
||||
import GridCard from './GridCard';
|
||||
import {
|
||||
Button,
|
||||
ButtonContainer,
|
||||
Card,
|
||||
CardContainer,
|
||||
ReactGridLayout,
|
||||
} from './styles';
|
||||
import { GraphLayoutProps } from './types';
|
||||
|
||||
function GraphLayout({
|
||||
onAddPanelHandler,
|
||||
widgets,
|
||||
}: GraphLayoutProps): JSX.Element {
|
||||
const {
|
||||
selectedDashboard,
|
||||
layouts,
|
||||
setLayouts,
|
||||
setSelectedDashboard,
|
||||
} = useDashboard();
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
const { featureResponse, role } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
||||
['save_layout', 'add_panel'],
|
||||
role,
|
||||
);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
layout: layouts.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.payload) {
|
||||
if (updatedDashboard.payload.data.layout)
|
||||
setLayouts(updatedDashboard.payload.data.layout);
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
notifications.success({
|
||||
message: t('dashboard:layout_saved_successfully'),
|
||||
});
|
||||
|
||||
featureResponse.refetch();
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonContainer>
|
||||
{saveLayoutPermission && (
|
||||
<Button
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveFilled />}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
{t('dashboard:save_layout')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{addPanelPermission && (
|
||||
<Button onClick={onAddPanelHandler} icon={<PlusOutlined />}>
|
||||
{t('dashboard:add_panel')}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
|
||||
<ReactGridLayout
|
||||
cols={12}
|
||||
rowHeight={100}
|
||||
autoSize
|
||||
width={100}
|
||||
isDraggable={addPanelPermission}
|
||||
isDroppable={addPanelPermission}
|
||||
isResizable={addPanelPermission}
|
||||
allowOverlap={false}
|
||||
onLayoutChange={setLayouts}
|
||||
draggableHandle=".drag-handle"
|
||||
layout={layouts}
|
||||
>
|
||||
{layouts.map((layout) => {
|
||||
const { i: id } = layout;
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||
|
||||
return (
|
||||
<CardContainer isDarkMode={isDarkMode} key={id} data-grid={layout}>
|
||||
<Card $panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}>
|
||||
<GridCard
|
||||
widget={currentWidget || ({ id } as Widgets)}
|
||||
name={currentWidget?.id || ''}
|
||||
headerMenuList={headerMenuList}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
);
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GraphLayout;
|
@ -1,9 +1,14 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { limit } from 'lib/getChartData';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
const positionCss: CSSProperties['position'] = 'absolute';
|
||||
|
||||
export const spinnerStyles = { position: positionCss, right: '0.5rem' };
|
||||
export const spinnerStyles = {
|
||||
position: positionCss,
|
||||
top: '0',
|
||||
right: '0',
|
||||
};
|
||||
export const tooltipStyles = {
|
||||
fontSize: '1rem',
|
||||
top: '0.313rem',
|
||||
@ -21,3 +26,5 @@ export const overlayStyles: CSSProperties = {
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
export const WARNING_MESSAGE = `Too many timeseries in the result. UI has restricted to showing the top ${limit}. Please check the query if this is needed and contact support@signoz.io if you need to show >${limit} timeseries in the panel`;
|
@ -5,10 +5,12 @@ import {
|
||||
EditFilled,
|
||||
ExclamationCircleOutlined,
|
||||
FullscreenOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
@ -27,6 +29,7 @@ import {
|
||||
overlayStyles,
|
||||
spinnerStyles,
|
||||
tooltipStyles,
|
||||
WARNING_MESSAGE,
|
||||
} from './config';
|
||||
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
|
||||
import {
|
||||
@ -52,6 +55,7 @@ interface IWidgetHeaderProps {
|
||||
errorMessage: string | undefined;
|
||||
threshold?: ReactNode;
|
||||
headerMenuList?: MenuItemKeys[];
|
||||
isWarning: boolean;
|
||||
}
|
||||
|
||||
function WidgetHeader({
|
||||
@ -65,7 +69,8 @@ function WidgetHeader({
|
||||
errorMessage,
|
||||
threshold,
|
||||
headerMenuList,
|
||||
}: IWidgetHeaderProps): JSX.Element {
|
||||
isWarning,
|
||||
}: IWidgetHeaderProps): JSX.Element | null {
|
||||
const [localHover, setLocalHover] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
@ -126,7 +131,7 @@ function WidgetHeader({
|
||||
icon: <FullscreenOutlined />,
|
||||
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.View],
|
||||
isVisible: headerMenuList?.includes(MenuItemKeys.View) || false,
|
||||
disabled: queryResponse.isLoading,
|
||||
disabled: queryResponse.isFetching,
|
||||
},
|
||||
{
|
||||
key: MenuItemKeys.Edit,
|
||||
@ -158,7 +163,7 @@ function WidgetHeader({
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
[queryResponse.isLoading, headerMenuList, editWidget, deleteWidget],
|
||||
[headerMenuList, queryResponse.isFetching, editWidget, deleteWidget],
|
||||
);
|
||||
|
||||
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
||||
@ -175,6 +180,10 @@ function WidgetHeader({
|
||||
[updatedMenuList, onMenuItemSelectHandler],
|
||||
);
|
||||
|
||||
if (widget.id === PANEL_TYPES.EMPTY_WIDGET) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WidgetHeaderContainer>
|
||||
<Dropdown
|
||||
@ -202,6 +211,7 @@ function WidgetHeader({
|
||||
</HeaderContentContainer>
|
||||
</HeaderContainer>
|
||||
</Dropdown>
|
||||
|
||||
<ThesholdContainer>{threshold}</ThesholdContainer>
|
||||
{queryResponse.isFetching && !queryResponse.isError && (
|
||||
<Spinner height="5vh" style={spinnerStyles} />
|
||||
@ -211,6 +221,12 @@ function WidgetHeader({
|
||||
<ExclamationCircleOutlined style={tooltipStyles} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isWarning && (
|
||||
<Tooltip title={WARNING_MESSAGE} placement={errorTooltipPosition}>
|
||||
<WarningOutlined style={tooltipStyles} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</WidgetHeaderContainer>
|
||||
);
|
||||
}
|
17
frontend/src/container/GridCardLayout/config.ts
Normal file
17
frontend/src/container/GridCardLayout/config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||
|
||||
export const headerMenuList = [
|
||||
MenuItemKeys.View,
|
||||
MenuItemKeys.Clone,
|
||||
MenuItemKeys.Delete,
|
||||
MenuItemKeys.Edit,
|
||||
];
|
||||
|
||||
export const EMPTY_WIDGET_LAYOUT = {
|
||||
i: PANEL_TYPES.EMPTY_WIDGET,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
y: 0,
|
||||
};
|
35
frontend/src/container/GridCardLayout/index.tsx
Normal file
35
frontend/src/container/GridCardLayout/index.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
import { EMPTY_WIDGET_LAYOUT } from './config';
|
||||
import GraphLayoutContainer from './GridCardLayout';
|
||||
|
||||
function GridGraph(): JSX.Element {
|
||||
const {
|
||||
selectedDashboard,
|
||||
setLayouts,
|
||||
handleToggleDashboardSlider,
|
||||
} = useDashboard();
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
const { widgets } = data || {};
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
handleToggleDashboardSlider(true);
|
||||
|
||||
setLayouts((preLayout: Layout[]) => [
|
||||
EMPTY_WIDGET_LAYOUT,
|
||||
...(preLayout || []),
|
||||
]);
|
||||
}, [handleToggleDashboardSlider, setLayouts]);
|
||||
|
||||
return (
|
||||
<GraphLayoutContainer
|
||||
onAddPanelHandler={onEmptyWidgetHandler}
|
||||
widgets={widgets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridGraph;
|
6
frontend/src/container/GridCardLayout/types.ts
Normal file
6
frontend/src/container/GridCardLayout/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface GraphLayoutProps {
|
||||
onAddPanelHandler: VoidFunction;
|
||||
widgets?: Widgets[];
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
import { Button, Input } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { Events } from 'constants/events';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
import { getGraphVisibilityStateOnDataChange } from '../utils';
|
||||
import {
|
||||
FilterTableAndSaveContainer,
|
||||
FilterTableContainer,
|
||||
SaveCancelButtonContainer,
|
||||
SaveContainer,
|
||||
} from './styles';
|
||||
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
||||
import {
|
||||
getDefaultTableDataSet,
|
||||
saveLegendEntriesToLocalStorage,
|
||||
} from './utils';
|
||||
|
||||
function GraphManager({
|
||||
data,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onToggleModelHandler,
|
||||
}: GraphManagerProps): JSX.Element {
|
||||
const {
|
||||
graphVisibilityStates: localstoredVisibilityStates,
|
||||
legendEntry,
|
||||
} = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
data,
|
||||
isExpandedName: false,
|
||||
name,
|
||||
}),
|
||||
[data, name],
|
||||
);
|
||||
|
||||
const [graphVisibilityState, setGraphVisibilityState] = useState<boolean[]>(
|
||||
localstoredVisibilityStates,
|
||||
);
|
||||
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
|
||||
getDefaultTableDataSet(data),
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
// useEffect for updating graph visibility state on data change
|
||||
useEffect(() => {
|
||||
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||
true,
|
||||
);
|
||||
data.datasets.forEach((dataset, i) => {
|
||||
const index = legendEntry.findIndex(
|
||||
(entry) => entry.label === dataset.label,
|
||||
);
|
||||
if (index !== -1) {
|
||||
newGraphVisibilityStates[i] = legendEntry[index].show;
|
||||
}
|
||||
});
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||
name,
|
||||
graphVisibilityStates: newGraphVisibilityStates,
|
||||
});
|
||||
setGraphVisibilityState(newGraphVisibilityStates);
|
||||
}, [data, name, legendEntry]);
|
||||
|
||||
// useEffect for listening to events event graph legend is clicked
|
||||
useEffect(() => {
|
||||
const eventListener = eventEmitter.on(
|
||||
Events.UPDATE_GRAPH_MANAGER_TABLE,
|
||||
(data) => {
|
||||
if (data.name === name) {
|
||||
const newGraphVisibilityStates = graphVisibilityState;
|
||||
newGraphVisibilityStates[data.index] = !newGraphVisibilityStates[
|
||||
data.index
|
||||
];
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||
name,
|
||||
graphVisibilityStates: newGraphVisibilityStates,
|
||||
});
|
||||
setGraphVisibilityState([...newGraphVisibilityStates]);
|
||||
}
|
||||
},
|
||||
);
|
||||
return (): void => {
|
||||
eventListener.off(Events.UPDATE_GRAPH_MANAGER_TABLE);
|
||||
};
|
||||
}, [graphVisibilityState, name]);
|
||||
|
||||
const checkBoxOnChangeHandler = useCallback(
|
||||
(e: CheckboxChangeEvent, index: number): void => {
|
||||
graphVisibilityState[index] = e.target.checked;
|
||||
setGraphVisibilityState([...graphVisibilityState]);
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||
name,
|
||||
graphVisibilityStates: [...graphVisibilityState],
|
||||
});
|
||||
},
|
||||
[graphVisibilityState, name],
|
||||
);
|
||||
|
||||
const labelClickedHandler = useCallback(
|
||||
(labelIndex: number): void => {
|
||||
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||
false,
|
||||
);
|
||||
newGraphVisibilityStates[labelIndex] = true;
|
||||
setGraphVisibilityState([...newGraphVisibilityStates]);
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||
name,
|
||||
graphVisibilityStates: newGraphVisibilityStates,
|
||||
});
|
||||
},
|
||||
[data.datasets.length, name],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getGraphManagerTableColumns({
|
||||
data,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
}),
|
||||
[
|
||||
checkBoxOnChangeHandler,
|
||||
data,
|
||||
graphVisibilityState,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
],
|
||||
);
|
||||
|
||||
const filterHandler = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = event.target.value.toString().toLowerCase();
|
||||
const updatedDataSet = tableDataSet.map((item) => {
|
||||
if (item.label?.toLocaleLowerCase().includes(value)) {
|
||||
return { ...item, show: true };
|
||||
}
|
||||
return { ...item, show: false };
|
||||
});
|
||||
setTableDataSet(updatedDataSet);
|
||||
},
|
||||
[tableDataSet],
|
||||
);
|
||||
|
||||
const saveHandler = useCallback((): void => {
|
||||
saveLegendEntriesToLocalStorage({
|
||||
data,
|
||||
graphVisibilityState,
|
||||
name,
|
||||
});
|
||||
notifications.success({
|
||||
message: 'The updated graphs & legends are saved',
|
||||
});
|
||||
if (onToggleModelHandler) {
|
||||
onToggleModelHandler();
|
||||
}
|
||||
}, [data, graphVisibilityState, name, notifications, onToggleModelHandler]);
|
||||
|
||||
const dataSource = tableDataSet.filter((item) => item.show);
|
||||
|
||||
return (
|
||||
<FilterTableAndSaveContainer>
|
||||
<FilterTableContainer>
|
||||
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
rowKey="index"
|
||||
pagination={false}
|
||||
scroll={{ y: 240 }}
|
||||
/>
|
||||
</FilterTableContainer>
|
||||
<SaveContainer>
|
||||
<SaveCancelButtonContainer>
|
||||
<Button type="default" onClick={onToggleModelHandler}>
|
||||
Cancel
|
||||
</Button>
|
||||
</SaveCancelButtonContainer>
|
||||
<SaveCancelButtonContainer>
|
||||
<Button onClick={saveHandler} type="primary">
|
||||
Save
|
||||
</Button>
|
||||
</SaveCancelButtonContainer>
|
||||
</SaveContainer>
|
||||
</FilterTableAndSaveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
GraphManager.defaultProps = {
|
||||
graphVisibilityStateHandler: undefined,
|
||||
};
|
||||
|
||||
export default memo(
|
||||
GraphManager,
|
||||
(prevProps, nextProps) =>
|
||||
isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name,
|
||||
);
|
@ -1,27 +0,0 @@
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { ChartData } from 'chart.js';
|
||||
|
||||
import { DataSetProps } from '../types';
|
||||
import CustomCheckBox from './CustomCheckBox';
|
||||
|
||||
export const getCheckBox = ({
|
||||
data,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState,
|
||||
}: GetCheckBoxProps): ColumnType<DataSetProps> => ({
|
||||
render: (index: number): JSX.Element => (
|
||||
<CustomCheckBox
|
||||
data={data}
|
||||
index={index}
|
||||
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
|
||||
graphVisibilityState={graphVisibilityState}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
interface GetCheckBoxProps {
|
||||
data: ChartData;
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
graphVisibilityState: boolean[];
|
||||
}
|
@ -1,334 +0,0 @@
|
||||
import { Typography } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { Events } from 'constants/events';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { DeleteWidget } from 'store/actions/dashboard/deleteWidget';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { UpdateDashboard } from '../utils';
|
||||
import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView';
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './FullView/contants';
|
||||
import { FullViewContainer, Modal } from './styles';
|
||||
import { DispatchProps, WidgetGraphComponentProps } from './types';
|
||||
import {
|
||||
getGraphVisibilityStateOnDataChange,
|
||||
toggleGraphsVisibilityInChart,
|
||||
} from './utils';
|
||||
|
||||
function WidgetGraphComponent({
|
||||
enableModel,
|
||||
enableWidgetHeader,
|
||||
data,
|
||||
widget,
|
||||
queryResponse,
|
||||
errorMessage,
|
||||
name,
|
||||
yAxisUnit,
|
||||
layout = [],
|
||||
deleteWidget,
|
||||
setLayout,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
threshold,
|
||||
headerMenuList,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [modal, setModal] = useState<boolean>(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { notifications } = useNotifications();
|
||||
const { t } = useTranslation(['common']);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { graphVisibilityStates: localstoredVisibilityStates } = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
data,
|
||||
isExpandedName: true,
|
||||
name,
|
||||
}),
|
||||
[data, name],
|
||||
);
|
||||
|
||||
const [graphsVisibilityStates, setGraphsVisilityStates] = useState<boolean[]>(
|
||||
localstoredVisibilityStates,
|
||||
);
|
||||
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
const [selectedDashboard] = dashboards;
|
||||
|
||||
const canModifyChart = useChartMutable({
|
||||
panelType: widget.panelTypes,
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
});
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
|
||||
// Updating the visibility state of the graph on data change according to global time range
|
||||
useEffect(() => {
|
||||
if (canModifyChart) {
|
||||
const newGraphVisibilityState = getGraphVisibilityStateOnDataChange({
|
||||
data,
|
||||
isExpandedName: true,
|
||||
name,
|
||||
});
|
||||
setGraphsVisilityStates(newGraphVisibilityState.graphVisibilityStates);
|
||||
}
|
||||
}, [canModifyChart, data, name]);
|
||||
|
||||
useEffect(() => {
|
||||
const eventListener = eventEmitter.on(
|
||||
Events.UPDATE_GRAPH_VISIBILITY_STATE,
|
||||
(data) => {
|
||||
if (data.name === `${name}expanded` && canModifyChart) {
|
||||
setGraphsVisilityStates([...data.graphVisibilityStates]);
|
||||
}
|
||||
},
|
||||
);
|
||||
return (): void => {
|
||||
eventListener.off(Events.UPDATE_GRAPH_VISIBILITY_STATE);
|
||||
};
|
||||
}, [canModifyChart, name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (canModifyChart && lineChartRef.current) {
|
||||
toggleGraphsVisibilityInChart({
|
||||
graphsVisibilityStates,
|
||||
lineChartRef,
|
||||
});
|
||||
}
|
||||
}, [graphsVisibilityStates, canModifyChart]);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const onToggleModal = useCallback(
|
||||
(func: Dispatch<SetStateAction<boolean>>) => {
|
||||
func((value) => !value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onDeleteHandler = useCallback(() => {
|
||||
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
|
||||
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
||||
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
deleteWidget({ widgetId, setLayout });
|
||||
onToggleModal(setDeleteModal);
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: t('common:something_went_wrong'),
|
||||
});
|
||||
});
|
||||
}, [
|
||||
widget,
|
||||
layout,
|
||||
featureResponse,
|
||||
deleteWidget,
|
||||
setLayout,
|
||||
onToggleModal,
|
||||
notifications,
|
||||
t,
|
||||
]);
|
||||
|
||||
const onCloneHandler = async (): Promise<void> => {
|
||||
const uuid = v4();
|
||||
|
||||
const layout = [
|
||||
{
|
||||
i: uuid,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
y: 0,
|
||||
},
|
||||
...(selectedDashboard.data.layout || []),
|
||||
];
|
||||
|
||||
if (widget) {
|
||||
await UpdateDashboard(
|
||||
{
|
||||
data: selectedDashboard.data,
|
||||
generateWidgetId: uuid,
|
||||
graphType: widget?.panelTypes,
|
||||
selectedDashboard,
|
||||
layout,
|
||||
widgetData: widget,
|
||||
isRedirected: false,
|
||||
},
|
||||
notifications,
|
||||
).then(() => {
|
||||
notifications.success({
|
||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||
});
|
||||
|
||||
const queryParams = {
|
||||
graphType: widget?.panelTypes,
|
||||
widgetId: uuid,
|
||||
};
|
||||
history.push(`${pathname}/new?${createQueryParams(queryParams)}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnView = (): void => {
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
const handleOnDelete = (): void => {
|
||||
onToggleModal(setDeleteModal);
|
||||
};
|
||||
|
||||
const onDeleteModelHandler = (): void => {
|
||||
onToggleModal(setDeleteModal);
|
||||
};
|
||||
|
||||
const onToggleModelHandler = (): void => {
|
||||
onToggleModal(setModal);
|
||||
};
|
||||
|
||||
const getModals = (): JSX.Element => (
|
||||
<>
|
||||
<Modal
|
||||
destroyOnClose
|
||||
onCancel={onDeleteModelHandler}
|
||||
open={deleteModal}
|
||||
title="Delete"
|
||||
height="10vh"
|
||||
onOk={onDeleteHandler}
|
||||
centered
|
||||
>
|
||||
<Typography>Are you sure you want to delete this widget</Typography>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="View"
|
||||
footer={[]}
|
||||
centered
|
||||
open={modal}
|
||||
onCancel={onToggleModelHandler}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
>
|
||||
<FullViewContainer>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
widget={widget}
|
||||
yAxisUnit={yAxisUnit}
|
||||
graphsVisibilityStates={graphsVisibilityStates}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
/>
|
||||
</FullViewContainer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
onMouseOver={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onFocus={(): void => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseOut={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
onBlur={(): void => {
|
||||
setHovered(false);
|
||||
}}
|
||||
>
|
||||
{enableModel && getModals()}
|
||||
{!isEmpty(widget) && data && (
|
||||
<>
|
||||
{enableWidgetHeader && (
|
||||
<div className="drag-handle">
|
||||
<WidgetHeader
|
||||
parentHover={hovered}
|
||||
title={widget?.title}
|
||||
widget={widget}
|
||||
onView={handleOnView}
|
||||
onDelete={handleOnDelete}
|
||||
onClone={onCloneHandler}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
threshold={threshold}
|
||||
headerMenuList={headerMenuList}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={data}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
ref={lineChartRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
WidgetGraphComponent.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
layout: undefined,
|
||||
setLayout: undefined,
|
||||
onDragSelect: undefined,
|
||||
onClickHandler: undefined,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps,
|
||||
)(
|
||||
memo(
|
||||
WidgetGraphComponent,
|
||||
(prevProps, nextProps) =>
|
||||
isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name,
|
||||
),
|
||||
);
|
@ -1,187 +0,0 @@
|
||||
import { ChartData } from 'chart.js';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import usePreviousValue from 'hooks/usePreviousValue';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getSelectedDashboardVariable } from 'utils/dashboard/selectedDashboard';
|
||||
|
||||
import EmptyWidget from '../EmptyWidget';
|
||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||
import { GridCardGraphProps } from './types';
|
||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||
|
||||
function GridCardGraph({
|
||||
widget,
|
||||
name,
|
||||
yAxisUnit,
|
||||
layout = [],
|
||||
setLayout,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
headerMenuList = [MenuItemKeys.View],
|
||||
isQueryEnabled,
|
||||
threshold,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
|
||||
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||
threshold: 0,
|
||||
triggerOnce: true,
|
||||
initialInView: false,
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
|
||||
const variables = getSelectedDashboardVariable(dashboards);
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
|
||||
const isEmptyWidget = useMemo(
|
||||
() => widget?.id === 'empty' || isEmpty(widget),
|
||||
[widget],
|
||||
);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
selectedTime: widget?.timePreferance,
|
||||
graphType: widget?.panelTypes,
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(),
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
`GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget?.id}`,
|
||||
maxTime,
|
||||
minTime,
|
||||
globalSelectedInterval,
|
||||
variables,
|
||||
widget?.query,
|
||||
widget?.panelTypes,
|
||||
],
|
||||
keepPreviousData: true,
|
||||
enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled && !isAddWidget,
|
||||
refetchOnMount: false,
|
||||
onError: (error) => {
|
||||
setErrorMessage(error.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[queryResponse],
|
||||
);
|
||||
|
||||
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
|
||||
|
||||
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||
|
||||
if (queryResponse.isRefetching || queryResponse.isLoading) {
|
||||
return <Spinner height="20vh" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if ((queryResponse.isError && !isEmptyLayout) || !isQueryEnabled) {
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
{!isEmpty(widget) && prevChartDataSetRef && (
|
||||
<WidgetGraphComponent
|
||||
enableModel
|
||||
enableWidgetHeader
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={prevChartDataSetRef}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
threshold={threshold}
|
||||
headerMenuList={headerMenuList}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isEmpty(widget) && prevChartDataSetRef?.labels) {
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
<WidgetGraphComponent
|
||||
enableModel
|
||||
enableWidgetHeader
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={prevChartDataSetRef}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
threshold={threshold}
|
||||
headerMenuList={headerMenuList}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
||||
<WidgetGraphComponent
|
||||
enableModel={!isEmptyLayout}
|
||||
enableWidgetHeader={!isEmptyLayout}
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={chartData}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
threshold={threshold}
|
||||
headerMenuList={headerMenuList}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isEmptyLayout && <EmptyWidget />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
GridCardGraph.defaultProps = {
|
||||
onDragSelect: undefined,
|
||||
onClickHandler: undefined,
|
||||
isQueryEnabled: true,
|
||||
threshold: undefined,
|
||||
headerMenuList: [MenuItemKeys.View],
|
||||
};
|
||||
|
||||
export default memo(GridCardGraph);
|
@ -1,113 +0,0 @@
|
||||
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
|
||||
import { LayoutProps, State } from '.';
|
||||
import {
|
||||
Button,
|
||||
ButtonContainer,
|
||||
Card,
|
||||
CardContainer,
|
||||
ReactGridLayout,
|
||||
} from './styles';
|
||||
|
||||
function GraphLayout({
|
||||
layouts,
|
||||
saveLayoutState,
|
||||
onLayoutSaveHandler,
|
||||
addPanelLoading,
|
||||
onAddPanelHandler,
|
||||
onLayoutChangeHandler,
|
||||
widgets,
|
||||
setLayout,
|
||||
}: GraphLayoutProps): JSX.Element {
|
||||
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
||||
['save_layout', 'add_panel'],
|
||||
role,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonContainer>
|
||||
{saveLayoutPermission && (
|
||||
<Button
|
||||
loading={saveLayoutState.loading}
|
||||
onClick={(): Promise<void> => onLayoutSaveHandler(layouts)}
|
||||
icon={<SaveFilled />}
|
||||
danger={saveLayoutState.error}
|
||||
>
|
||||
Save Layout
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{addPanelPermission && (
|
||||
<Button
|
||||
loading={addPanelLoading}
|
||||
disabled={addPanelLoading || isAddWidget}
|
||||
onClick={onAddPanelHandler}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Add Panel
|
||||
</Button>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
|
||||
<ReactGridLayout
|
||||
cols={12}
|
||||
rowHeight={100}
|
||||
autoSize
|
||||
width={100}
|
||||
isDraggable={addPanelPermission}
|
||||
isDroppable={addPanelPermission}
|
||||
isResizable={addPanelPermission}
|
||||
useCSSTransforms
|
||||
allowOverlap={false}
|
||||
onLayoutChange={onLayoutChangeHandler}
|
||||
draggableHandle=".drag-handle"
|
||||
>
|
||||
{layouts.map(({ Component, ...rest }) => {
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
isDarkMode={isDarkMode}
|
||||
key={currentWidget?.id || 'empty'} // don't change this key
|
||||
data-grid={rest}
|
||||
>
|
||||
<Card $panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}>
|
||||
<Component setLayout={setLayout} />
|
||||
</Card>
|
||||
</CardContainer>
|
||||
);
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface GraphLayoutProps {
|
||||
layouts: LayoutProps[];
|
||||
saveLayoutState: State;
|
||||
onLayoutSaveHandler: (layout: Layout[]) => Promise<void>;
|
||||
addPanelLoading: boolean;
|
||||
onAddPanelHandler: VoidFunction;
|
||||
onLayoutChangeHandler: (layout: Layout[]) => Promise<void>;
|
||||
widgets: Widgets[] | undefined;
|
||||
setLayout: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||
}
|
||||
|
||||
export default GraphLayout;
|
@ -1,8 +0,0 @@
|
||||
import { MenuItemKeys } from 'container/GridGraphLayout/WidgetHeader/contants';
|
||||
|
||||
export const headerMenuList = [
|
||||
MenuItemKeys.View,
|
||||
MenuItemKeys.Clone,
|
||||
MenuItemKeys.Delete,
|
||||
MenuItemKeys.Edit,
|
||||
];
|
@ -1,383 +0,0 @@
|
||||
/* eslint-disable react/no-unstable-nested-components */
|
||||
|
||||
import updateDashboardApi from 'api/dashboard/update';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch as ReduxDispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { AppDispatch } from 'store';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import {
|
||||
ToggleAddWidget,
|
||||
ToggleAddWidgetProps,
|
||||
} from 'store/actions/dashboard/toggleAddWidget';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_DASHBOARD } from 'types/actions/dashboard';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
|
||||
import { headerMenuList } from './config';
|
||||
import Graph from './Graph';
|
||||
import GraphLayoutContainer from './GraphLayout';
|
||||
import { UpdateDashboard } from './utils';
|
||||
|
||||
export const getPreLayouts = (
|
||||
widgets: Widgets[] | undefined,
|
||||
layout: Layout[],
|
||||
): LayoutProps[] =>
|
||||
layout.map((e, index) => ({
|
||||
...e,
|
||||
Component: ({ setLayout }: ComponentProps): JSX.Element => {
|
||||
const widget = widgets?.find((widget) => widget.id === e.i);
|
||||
|
||||
return (
|
||||
<Graph
|
||||
name={e.i + index}
|
||||
widget={widget as Widgets}
|
||||
yAxisUnit={widget?.yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
headerMenuList={headerMenuList}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
function GridGraph(props: Props): JSX.Element {
|
||||
const { toggleAddWidget } = props;
|
||||
const [addPanelLoading, setAddPanelLoading] = useState(false);
|
||||
const { t } = useTranslation(['common']);
|
||||
const { dashboards, isAddWidget } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [saveLayoutPermission] = useComponentPermission(['save_layout'], role);
|
||||
const [saveLayoutState, setSaveLayoutState] = useState<State>({
|
||||
loading: false,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
payload: [],
|
||||
});
|
||||
const [selectedDashboard] = dashboards;
|
||||
const { data } = selectedDashboard;
|
||||
const { widgets } = data;
|
||||
const dispatch: AppDispatch = useDispatch<ReduxDispatch<AppActions>>();
|
||||
|
||||
const [layouts, setLayout] = useState<LayoutProps[]>(
|
||||
getPreLayouts(widgets, selectedDashboard.data.layout || []),
|
||||
);
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number) => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
(async (): Promise<void> => {
|
||||
if (!isAddWidget) {
|
||||
const isEmptyLayoutPresent = layouts.find((e) => e.i === 'empty');
|
||||
if (isEmptyLayoutPresent) {
|
||||
// non empty layout
|
||||
const updatedLayout = layouts.filter((e) => e.i !== 'empty');
|
||||
// non widget
|
||||
const updatedWidget = widgets?.filter((e) => e.id !== 'empty');
|
||||
setLayout(updatedLayout);
|
||||
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
layout: updatedLayout,
|
||||
widgets: updatedWidget,
|
||||
},
|
||||
};
|
||||
|
||||
await updateDashboardApi({
|
||||
data: updatedDashboard.data,
|
||||
uuid: updatedDashboard.uuid,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_DASHBOARD,
|
||||
payload: updatedDashboard,
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [dispatch, isAddWidget, layouts, selectedDashboard, widgets]);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const errorMessage = t('common:something_went_wrong');
|
||||
|
||||
const onLayoutSaveHandler = useCallback(
|
||||
async (layout: Layout[]) => {
|
||||
try {
|
||||
setSaveLayoutState((state) => ({
|
||||
...state,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(async () => {
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
widgets: data.widgets,
|
||||
variables: data.variables,
|
||||
layout,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
// Save layout only when users has the has the permission to do so.
|
||||
if (saveLayoutPermission) {
|
||||
const response = await updateDashboardApi(updatedDashboard);
|
||||
if (response.statusCode === 200) {
|
||||
setSaveLayoutState((state) => ({
|
||||
...state,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
}));
|
||||
dispatch({
|
||||
type: UPDATE_DASHBOARD,
|
||||
payload: updatedDashboard,
|
||||
});
|
||||
} else {
|
||||
setSaveLayoutState((state) => ({
|
||||
...state,
|
||||
error: true,
|
||||
errorMessage: response.error || errorMessage,
|
||||
loading: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setSaveLayoutState((state) => ({
|
||||
...state,
|
||||
error: true,
|
||||
errorMessage,
|
||||
loading: false,
|
||||
}));
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
data.description,
|
||||
data.name,
|
||||
data.tags,
|
||||
data.title,
|
||||
data.variables,
|
||||
data.widgets,
|
||||
dispatch,
|
||||
errorMessage,
|
||||
featureResponse,
|
||||
notifications,
|
||||
saveLayoutPermission,
|
||||
selectedDashboard,
|
||||
],
|
||||
);
|
||||
|
||||
const setLayoutFunction = useCallback(
|
||||
(layout: Layout[]) => {
|
||||
setLayout(
|
||||
layout.map((e) => {
|
||||
const currentWidget =
|
||||
widgets?.find((widget) => widget.id === e.i) || ({} as Widgets);
|
||||
|
||||
return {
|
||||
...e,
|
||||
Component: (): JSX.Element => (
|
||||
<Graph
|
||||
name={currentWidget.id}
|
||||
widget={currentWidget}
|
||||
yAxisUnit={currentWidget?.yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
onDragSelect={onDragSelect}
|
||||
headerMenuList={headerMenuList}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
},
|
||||
[widgets, onDragSelect],
|
||||
);
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(async () => {
|
||||
try {
|
||||
const id = 'empty';
|
||||
|
||||
const layout = [
|
||||
{
|
||||
i: id,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
y: 0,
|
||||
},
|
||||
...(data.layout || []),
|
||||
];
|
||||
|
||||
await UpdateDashboard(
|
||||
{
|
||||
data,
|
||||
generateWidgetId: id,
|
||||
graphType: PANEL_TYPES.EMPTY_WIDGET,
|
||||
selectedDashboard,
|
||||
layout,
|
||||
isRedirected: false,
|
||||
},
|
||||
notifications,
|
||||
);
|
||||
|
||||
setLayoutFunction(layout);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: error instanceof Error ? error.toString() : errorMessage,
|
||||
});
|
||||
}
|
||||
}, [data, selectedDashboard, setLayoutFunction, notifications, errorMessage]);
|
||||
|
||||
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
|
||||
setLayoutFunction(layout);
|
||||
|
||||
// await onLayoutSaveHandler(layout);
|
||||
};
|
||||
|
||||
const onAddPanelHandler = useCallback(() => {
|
||||
try {
|
||||
setAddPanelLoading(true);
|
||||
featureResponse
|
||||
.refetch()
|
||||
.then(() => {
|
||||
const isEmptyLayoutPresent =
|
||||
layouts.find((e) => e.i === 'empty') !== undefined;
|
||||
|
||||
if (!isEmptyLayoutPresent) {
|
||||
onEmptyWidgetHandler()
|
||||
.then(() => {
|
||||
setAddPanelLoading(false);
|
||||
toggleAddWidget(true);
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
toggleAddWidget(true);
|
||||
setAddPanelLoading(false);
|
||||
}
|
||||
})
|
||||
.catch(() =>
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
featureResponse,
|
||||
layouts,
|
||||
onEmptyWidgetHandler,
|
||||
toggleAddWidget,
|
||||
notifications,
|
||||
errorMessage,
|
||||
]);
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
toggleAddWidget(false);
|
||||
},
|
||||
[toggleAddWidget],
|
||||
);
|
||||
|
||||
return (
|
||||
<GraphLayoutContainer
|
||||
addPanelLoading={addPanelLoading}
|
||||
layouts={layouts}
|
||||
onAddPanelHandler={onAddPanelHandler}
|
||||
onLayoutChangeHandler={onLayoutChangeHandler}
|
||||
onLayoutSaveHandler={onLayoutSaveHandler}
|
||||
saveLayoutState={saveLayoutState}
|
||||
setLayout={setLayout}
|
||||
widgets={widgets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ComponentProps {
|
||||
setLayout: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||
}
|
||||
|
||||
export interface LayoutProps extends Layout {
|
||||
Component: (props: ComponentProps) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
payload: Layout[];
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
toggleAddWidget: (
|
||||
props: ToggleAddWidgetProps,
|
||||
) => (dispatch: ReduxDispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch),
|
||||
});
|
||||
|
||||
type Props = DispatchProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(GridGraph);
|
@ -1,78 +0,0 @@
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import updateDashboardApi from 'api/dashboard/update';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import store from 'store';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
export const UpdateDashboard = async (
|
||||
{
|
||||
data,
|
||||
graphType,
|
||||
generateWidgetId,
|
||||
layout,
|
||||
selectedDashboard,
|
||||
isRedirected,
|
||||
widgetData,
|
||||
}: UpdateDashboardProps,
|
||||
notify: NotificationInstance,
|
||||
): Promise<Dashboard | undefined> => {
|
||||
const copyTitle = `${widgetData?.title} - Copy`;
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
variables: data.variables,
|
||||
widgets: [
|
||||
...(data.widgets || []),
|
||||
{
|
||||
description: widgetData?.description || '',
|
||||
id: generateWidgetId,
|
||||
isStacked: false,
|
||||
nullZeroValues: widgetData?.nullZeroValues || '',
|
||||
opacity: '',
|
||||
panelTypes: graphType,
|
||||
query: widgetData?.query || initialQueriesMap.metrics,
|
||||
timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME',
|
||||
title: widgetData ? copyTitle : '',
|
||||
yAxisUnit: widgetData?.yAxisUnit,
|
||||
},
|
||||
],
|
||||
layout,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
const response = await updateDashboardApi(updatedSelectedDashboard);
|
||||
|
||||
if (response.payload) {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_DASHBOARD',
|
||||
payload: response.payload,
|
||||
});
|
||||
}
|
||||
|
||||
if (isRedirected) {
|
||||
if (response.statusCode === 200) {
|
||||
return response.payload;
|
||||
}
|
||||
notify.error({
|
||||
message: response.error || 'Something went wrong',
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
interface UpdateDashboardProps {
|
||||
data: Dashboard['data'];
|
||||
graphType: PANEL_TYPES;
|
||||
generateWidgetId: string;
|
||||
layout: Layout[];
|
||||
selectedDashboard: Dashboard;
|
||||
isRedirected: boolean;
|
||||
widgetData?: Widgets;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
.ingestion-settings-container {
|
||||
color: white;
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import './IngestionSettings.styles.scss';
|
||||
|
||||
import { Table, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import getIngestionData from 'api/settings/getIngestionData';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IngestionDataType } from 'types/api/settings/ingestion';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
export default function IngestionSettings(): JSX.Element {
|
||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const { data: ingestionData } = useQuery({
|
||||
queryFn: getIngestionData,
|
||||
queryKey: ['getIngestionData', user?.userId],
|
||||
});
|
||||
|
||||
const columns: ColumnsType<IngestionDataType> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text): JSX.Element => <Typography.Text> {text} </Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
render: (text): JSX.Element => (
|
||||
<Typography.Text copyable>{text}</Typography.Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const injectionDataPayload =
|
||||
ingestionData &&
|
||||
ingestionData.payload &&
|
||||
Array.isArray(ingestionData.payload) &&
|
||||
ingestionData?.payload[0];
|
||||
|
||||
const data: IngestionDataType[] = [
|
||||
{
|
||||
key: '1',
|
||||
name: 'Ingestion URL',
|
||||
value: injectionDataPayload?.ingestionURL,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
name: 'Ingestion Key',
|
||||
value: injectionDataPayload?.ingestionKey,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
name: 'Ingestion Region',
|
||||
value: injectionDataPayload?.dataRegion,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="ingestion-settings-container">
|
||||
<Typography
|
||||
style={{
|
||||
margin: '16px 0px',
|
||||
}}
|
||||
>
|
||||
You can use the following ingestion credentials to start sending your
|
||||
telemetry data to SigNoz
|
||||
</Typography>
|
||||
|
||||
<Table
|
||||
style={{
|
||||
margin: '16px 0px',
|
||||
}}
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -9,11 +9,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { FLUSH_DASHBOARD } from 'types/actions/dashboard';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { EditorContainer, FooterContainer } from './styles';
|
||||
@ -31,8 +27,6 @@ function ImportJSON({
|
||||
);
|
||||
const [isFeatureAlert, setIsFeatureAlert] = useState<boolean>(false);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const [dashboardCreating, setDashboardCreating] = useState<boolean>(false);
|
||||
|
||||
const [editorValue, setEditorValue] = useState<string>('');
|
||||
@ -77,16 +71,11 @@ function ImportJSON({
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
dispatch({
|
||||
type: FLUSH_DASHBOARD,
|
||||
});
|
||||
setTimeout(() => {
|
||||
history.push(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.payload.uuid,
|
||||
}),
|
||||
);
|
||||
}, 10);
|
||||
history.push(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.payload.uuid,
|
||||
}),
|
||||
);
|
||||
} else if (response.error === 'feature usage exceeded') {
|
||||
setIsFeatureAlert(true);
|
||||
notifications.error({
|
||||
|
@ -1,37 +1,36 @@
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { Modal } from 'antd';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
||||
import { useCallback } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { DeleteDashboard, DeleteDashboardProps } from 'store/actions';
|
||||
import AppActions from 'types/actions';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
import { Data } from '../index';
|
||||
import { TableLinkText } from './styles';
|
||||
|
||||
function DeleteButton({
|
||||
deleteDashboard,
|
||||
id,
|
||||
refetchDashboardList,
|
||||
}: DeleteButtonProps): JSX.Element {
|
||||
function DeleteButton({ id }: Data): JSX.Element {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const deleteDashboardMutation = useDeleteDashboard(id);
|
||||
|
||||
const openConfirmationDialog = useCallback((): void => {
|
||||
modal.confirm({
|
||||
title: 'Do you really want to delete this dashboard?',
|
||||
icon: <ExclamationCircleOutlined style={{ color: '#e42b35' }} />,
|
||||
onOk() {
|
||||
deleteDashboard({
|
||||
uuid: id,
|
||||
refetch: refetchDashboardList,
|
||||
deleteDashboardMutation.mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_ALL_DASHBOARDS]);
|
||||
},
|
||||
});
|
||||
},
|
||||
okText: 'Delete',
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
});
|
||||
}, [modal, deleteDashboard, id, refetchDashboardList]);
|
||||
}, [modal, deleteDashboardMutation, queryClient]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -44,37 +43,12 @@ function DeleteButton({
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
deleteDashboard: ({
|
||||
uuid,
|
||||
}: DeleteDashboardProps) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
deleteDashboard: bindActionCreators(DeleteDashboard, dispatch),
|
||||
});
|
||||
|
||||
export type DeleteButtonProps = Data & DispatchProps;
|
||||
|
||||
const WrapperDeleteButton = connect(null, mapDispatchToProps)(DeleteButton);
|
||||
|
||||
// This is to avoid the type collision
|
||||
function Wrapper(props: Data): JSX.Element {
|
||||
const {
|
||||
createdBy,
|
||||
description,
|
||||
id,
|
||||
key,
|
||||
refetchDashboardList,
|
||||
lastUpdatedTime,
|
||||
name,
|
||||
tags,
|
||||
} = props;
|
||||
const { createdBy, description, id, key, lastUpdatedTime, name, tags } = props;
|
||||
|
||||
return (
|
||||
<WrapperDeleteButton
|
||||
<DeleteButton
|
||||
{...{
|
||||
createdBy,
|
||||
description,
|
||||
@ -83,7 +57,6 @@ function Wrapper(props: Data): JSX.Element {
|
||||
lastUpdatedTime,
|
||||
name,
|
||||
tags,
|
||||
refetchDashboardList,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -17,21 +17,11 @@ import SearchFilter from 'container/ListOfDashboard/SearchFilter';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import {
|
||||
Dispatch,
|
||||
Key,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { GET_ALL_DASHBOARD_SUCCESS } from 'types/actions/dashboard';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
@ -40,9 +30,7 @@ import ImportJSON from './ImportJSON';
|
||||
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
|
||||
import Createdby from './TableComponents/CreatedBy';
|
||||
import DateComponent from './TableComponents/Date';
|
||||
import DeleteButton, {
|
||||
DeleteButtonProps,
|
||||
} from './TableComponents/DeleteButton';
|
||||
import DeleteButton from './TableComponents/DeleteButton';
|
||||
import Name from './TableComponents/Name';
|
||||
import Tags from './TableComponents/Tags';
|
||||
|
||||
@ -53,7 +41,6 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
refetch: refetchDashboardList,
|
||||
} = useGetAllDashboard();
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [action, createNewDashboard, newDashboard] = useComponentPermission(
|
||||
@ -134,31 +121,12 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
title: 'Action',
|
||||
dataIndex: '',
|
||||
width: 40,
|
||||
render: ({
|
||||
createdBy,
|
||||
description,
|
||||
id,
|
||||
key,
|
||||
lastUpdatedTime,
|
||||
name,
|
||||
tags,
|
||||
}: DeleteButtonProps) => (
|
||||
<DeleteButton
|
||||
description={description}
|
||||
id={id}
|
||||
key={key}
|
||||
lastUpdatedTime={lastUpdatedTime}
|
||||
name={name}
|
||||
tags={tags}
|
||||
createdBy={createdBy}
|
||||
refetchDashboardList={refetchDashboardList}
|
||||
/>
|
||||
),
|
||||
render: DeleteButton,
|
||||
});
|
||||
}
|
||||
|
||||
return tableColumns;
|
||||
}, [action, refetchDashboardList]);
|
||||
}, [action]);
|
||||
|
||||
const data: Data[] =
|
||||
filteredDashboards?.map((e) => ({
|
||||
@ -186,10 +154,6 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
dispatch({
|
||||
type: GET_ALL_DASHBOARD_SUCCESS,
|
||||
payload: [],
|
||||
});
|
||||
history.push(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.payload.uuid,
|
||||
@ -210,7 +174,7 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||
});
|
||||
}
|
||||
}, [newDashboardState, t, dispatch]);
|
||||
}, [newDashboardState, t]);
|
||||
|
||||
const getText = useCallback(() => {
|
||||
if (!newDashboardState.error && !newDashboardState.loading) {
|
||||
@ -352,7 +316,6 @@ export interface Data {
|
||||
createdBy: string;
|
||||
lastUpdatedTime: string;
|
||||
id: string;
|
||||
refetchDashboardList: UseQueryResult['refetch'];
|
||||
}
|
||||
|
||||
export default ListOfAllDashboard;
|
||||
|
@ -11,11 +11,11 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { prepareQueryRangePayload } from 'lib/dashboard/prepareQueryRangePayload';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { prepareQueryRangePayload } from 'store/actions/dashboard/prepareQueryRangePayload';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
@ -21,7 +21,12 @@ import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import ActionItem, { ActionItemProps } from './ActionItem';
|
||||
import FieldRenderer from './FieldRenderer';
|
||||
import { flattenObject, jsonToDataNodes, recursiveParseJSON } from './utils';
|
||||
import {
|
||||
flattenObject,
|
||||
jsonToDataNodes,
|
||||
recursiveParseJSON,
|
||||
removeEscapeCharacters,
|
||||
} from './utils';
|
||||
|
||||
// Fields which should be restricted from adding it to query
|
||||
const RESTRICTED_FIELDS = ['timestamp'];
|
||||
@ -58,7 +63,7 @@ function TableView({
|
||||
.map((key) => ({
|
||||
key,
|
||||
field: key,
|
||||
value: JSON.stringify(flattenLogData[key]),
|
||||
value: removeEscapeCharacters(JSON.stringify(flattenLogData[key])),
|
||||
}));
|
||||
|
||||
const onTraceHandler = (record: DataType) => (): void => {
|
||||
@ -164,6 +169,8 @@ function TableView({
|
||||
width: 70,
|
||||
ellipsis: false,
|
||||
render: (field, record): JSX.Element => {
|
||||
const textToCopy = field.slice(1, -1);
|
||||
|
||||
if (record.field === 'body') {
|
||||
const parsedBody = recursiveParseJSON(field);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
@ -174,7 +181,7 @@ function TableView({
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyClipboardHOC textToCopy={field}>
|
||||
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||
<span style={{ color: orange[6] }}>{field}</span>
|
||||
</CopyClipboardHOC>
|
||||
);
|
||||
|
13
frontend/src/container/LogDetailedView/config.ts
Normal file
13
frontend/src/container/LogDetailedView/config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const typeToArrayTypeMapper: { [key in DataTypes]: DataTypes } = {
|
||||
[DataTypes.String]: DataTypes.ArrayString,
|
||||
[DataTypes.Float64]: DataTypes.ArrayFloat64,
|
||||
[DataTypes.Int64]: DataTypes.ArrayInt64,
|
||||
[DataTypes.bool]: DataTypes.ArrayBool,
|
||||
[DataTypes.EMPTY]: DataTypes.EMPTY,
|
||||
[DataTypes.ArrayFloat64]: DataTypes.ArrayFloat64,
|
||||
[DataTypes.ArrayInt64]: DataTypes.ArrayInt64,
|
||||
[DataTypes.ArrayString]: DataTypes.ArrayString,
|
||||
[DataTypes.ArrayBool]: DataTypes.ArrayBool,
|
||||
};
|
@ -176,8 +176,8 @@ describe('Get Data Types utils', () => {
|
||||
});
|
||||
|
||||
// Edge cases
|
||||
it('should return Int64 for empty array input', () => {
|
||||
expect(getDataTypes([])).toBe(DataTypes.Int64);
|
||||
it('should return Empty for empty array input', () => {
|
||||
expect(getDataTypes([])).toBe(DataTypes.EMPTY);
|
||||
});
|
||||
|
||||
it('should handle mixed array (return based on first element)', () => {
|
||||
|
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