Merge pull request #3732 from SigNoz/release/v0.31.0

Release/v0.31.0
This commit is contained in:
Prashant Shahi 2023-10-12 20:40:51 +05:45 committed by GitHub
commit 3c63d66591
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
265 changed files with 5707 additions and 4299 deletions

View File

@ -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: |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
# 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/...

View File

@ -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" />
<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使用分布式追踪来增加软件技术栈的可见性。
SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可以使用 SigNoz 实现如下能力:
👉 你能看到一些性能指标服务、外部api调用、每个终端(endpoint)的p99延迟和错误率。
👉 在同一块面板上,可视化 Metrics, Traces 和 Logs 内容
👉 通过准确的追踪来确定是什么引起了问题,并且可以看到每个独立请求的帧图(framegraph),这样你就能找到根本原因。
👉 你可以关注服务的 p99 延迟和错误率, 包括外部 API 调用和个别的端点
👉 聚合trace数据来获得业务相关指标。
👉 你可以找到问题的根因,通过提取相关问题的 traces 日志、单独查看请求 traces 的火焰图详情
![screenzy-1644432902955](https://user-images.githubusercontent.com/504541/153270713-1b2156e6-ec03-42de-975b-3c02b8ec1836.png)
<br />
![screenzy-1644432986784](https://user-images.githubusercontent.com/504541/153270725-0efb73b3-06ed-4207-bf13-9b7e2e17c4b8.png)
<br />
![screenzy-1647005040573](https://user-images.githubusercontent.com/504541/157875938-a3d57904-ea6d-4278-b929-bd1408d7f94c.png)
👉 执行 trace 数据聚合,以获取业务相关的 metrics
👉 对日志过滤和查询,通过日志的属性建立看板和告警
👉 通过 PythonjavaRuby 和 Javascript 自动记录异常
👉 轻松的自定义查询和设置告警
### 应用 Metrics 展示
![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png)
### 分布式追踪
<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">
### 异常监控
![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.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来安装
一步步跟随 [这里](https://signoz.io/docs/install/docker/) 通过 docker 来安装。
如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/install/troubleshooting/)会对你有帮助
这个 [排障说明书](https://signoz.io/docs/install/troubleshooting/) 可以帮助你解决碰到的问题
<p>&nbsp </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>&nbsp </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>&nbsp </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>&nbsp </p>
### SigNoz vs Loki
- SigNoz 支持大容量高基数的聚合,但是 loki 是不支持的。
- SigNoz 支持索引的高基数查询,并且对索引没有数量限制,而 Loki 会在添加部分索引后到达最大上限。
- 相较于 SigNozLoki 在搜索大量数据下既困难又缓慢。
我们已经发布了基准测试对比 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>

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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:
[

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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")

View File

@ -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"`
}

View File

@ -1,4 +1,3 @@
node_modules
.vscode
build
.git

View File

@ -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

View File

@ -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?"
}

View File

@ -2,6 +2,7 @@
"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",

View File

@ -24,6 +24,7 @@
"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",

View File

@ -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?"
}

View File

@ -2,6 +2,7 @@
"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",

View File

@ -24,6 +24,7 @@
"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",

View File

@ -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,6 +111,7 @@ function App(): JSX.Element {
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
@ -126,6 +128,7 @@ function App(): JSX.Element {
</Switch>
</Suspense>
</AppLayout>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>

View File

@ -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'),
);

View File

@ -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,

View File

@ -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;

View File

@ -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;

View 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;

View 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;

View File

@ -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 };

View File

@ -0,0 +1,3 @@
.overlay--text-wrap {
white-space: pre-wrap;
}

View 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;

View File

@ -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;

View File

@ -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',
};

View File

@ -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',

View File

@ -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,

View File

@ -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,
};

View File

@ -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
/>
);

View 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);
});
});

View File

@ -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',
});
};

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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>
);
}

View File

@ -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],

View File

@ -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}
/>
)}
</>

View File

@ -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;

View File

@ -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 {

View File

@ -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,

View File

@ -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;

View 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);

View File

@ -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;

View 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;

View File

@ -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`;

View File

@ -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>
);
}

View 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,
};

View 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;

View File

@ -0,0 +1,6 @@
import { Widgets } from 'types/api/dashboard/getAll';
export interface GraphLayoutProps {
onAddPanelHandler: VoidFunction;
widgets?: Widgets[];
}

View File

@ -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,
);

View File

@ -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[];
}

View File

@ -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,
),
);

View File

@ -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);

View File

@ -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;

View File

@ -1,8 +0,0 @@
import { MenuItemKeys } from 'container/GridGraphLayout/WidgetHeader/contants';
export const headerMenuList = [
MenuItemKeys.View,
MenuItemKeys.Clone,
MenuItemKeys.Delete,
MenuItemKeys.Edit,
];

View File

@ -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);

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
.ingestion-settings-container {
color: white;
}

View File

@ -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>
);
}

View File

@ -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);
} else if (response.error === 'feature usage exceeded') {
setIsFeatureAlert(true);
notifications.error({

View File

@ -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,
}}
/>
);

View File

@ -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;

View File

@ -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';

View File

@ -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>
);

View 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,
};

View File

@ -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