Merge pull request #1246 from SigNoz/release/v0.8.1

Release/v0.8.1
This commit is contained in:
Ankit Nayan 2022-06-09 21:17:59 +05:30 committed by GitHub
commit 7b315c6766
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
176 changed files with 4894 additions and 5732 deletions

View File

@ -1,40 +1,15 @@
name: build-pipeline
on:
pull_request:
branches:
- develop
- main
- v*
paths:
- "pkg/**"
- "frontend/**"
- release/v*
jobs:
get_filters:
runs-on: ubuntu-latest
# Set job outputs to values from filter step
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
query-service: ${{ steps.filter.outputs.query-service }}
flattener: ${{ steps.filter.outputs.flattener }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'frontend/**'
query-service:
- 'pkg/query-service/**'
flattener:
- 'pkg/processors/flattener/**'
build-frontend:
runs-on: ubuntu-latest
needs:
- get_filters
if: ${{ needs.get_filters.outputs.frontend == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v2
@ -52,9 +27,6 @@ jobs:
build-query-service:
runs-on: ubuntu-latest
needs:
- get_filters
if: ${{ needs.get_filters.outputs.query-service == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v2
@ -62,16 +34,3 @@ jobs:
shell: bash
run: |
make build-query-service-amd64
build-flattener:
runs-on: ubuntu-latest
needs:
- get_filters
if: ${{ needs.get_filters.outputs.flattener == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build flattener docker image
shell: bash
run: |
make build-flattener-amd64

View File

@ -0,0 +1,27 @@
on:
pull_request_target:
types:
- closed
env:
GITHUB_ACCESS_TOKEN: ${{ secrets.CI_BOT_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
jobs:
create_issue_on_merge:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout Codebase
uses: actions/checkout@v2
with:
repository: signoz/gh-bot
- name: Use Node v16
uses: actions/setup-node@v2
with:
node-version: 16
- name: Setup Cache & Install Dependencies
uses: bahmutov/npm-install@v1
with:
install-command: yarn --frozen-lockfile
- name: Comment on PR
run: node create-issue.js

22
.github/workflows/playwright.yaml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "14.x"
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
env:
# This might depend on your test-runner/language binding
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}

5
.gitignore vendored
View File

@ -15,6 +15,7 @@ frontend/build
frontend/.vscode
frontend/.yarnclean
frontend/.temp_cache
frontend/test-results
# misc
.DS_Store
@ -27,10 +28,6 @@ frontend/npm-debug.log*
frontend/yarn-debug.log*
frontend/yarn-error.log*
frontend/src/constants/env.ts
frontend/cypress/**/*.mp4
# env file for cypress
frontend/cypress.env.json
.idea

View File

@ -106,32 +106,70 @@ Need to update [https://github.com/SigNoz/charts](https://github.com/SigNoz/char
- [k3d](https://k3d.io/#installation)
- [minikube](https://minikube.sigs.k8s.io/docs/start/)
- create a k8s cluster and make sure `kubectl` points to the locally created k8s cluster
- run `helm install -n platform --create-namespace my-release charts/signoz` to install SigNoz chart
- run `kubectl -n platform port-forward svc/my-release-frontend 3301:3301` to make SigNoz UI available at [localhost:3301](http://localhost:3301)
- run `make dev-install` to install SigNoz chart with `my-release` release name in `platform` namespace.
- run `kubectl -n platform port-forward svc/my-release-signoz-frontend 3301:3301` to make SigNoz UI available at [localhost:3301](http://localhost:3301)
**To install HotROD sample app:**
```bash
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh \
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
```
**To load data with HotROD sample app:**
```sh
kubectl create ns sample-application
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml
```bash
kubectl -n sample-application run strzal --image=djbingham/curl \
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
```
**To stop the load generation:**
```sh
```bash
kubectl -n sample-application run strzal --image=djbingham/curl \
--restart='OnFailure' -i --tty --rm --command -- curl \
http://locust-master:8089/stop
--restart='OnFailure' -i --tty --rm --command -- curl \
http://locust-master:8089/stop
```
**To delete HotROD sample app:**
```bash
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh \
| HOTROD_NAMESPACE=sample-application bash
```
---
## General Instructions
**Before making any significant changes, please open an issue**. Each issue
should describe the following:
* Requirement - what kind of use case are you trying to solve?
* Proposal - what do you suggest to solve the problem or improve the existing
situation?
* Any open questions to address
Discussing your proposed changes ahead of time will make the contribution
process smooth for everyone. Once the approach is agreed upon, make your changes
and open a pull request(s). Unless your change is small, Please consider submitting different PRs:
* First PR should include the overall structure of the new component:
* Readme, configuration, interfaces or base classes etc...
* This PR is usually trivial to review, so the size limit does not apply to
it.
* Second PR should include the concrete implementation of the component. If the
size of this PR is larger than the recommended size consider splitting it in
multiple PRs.
* If there are multiple sub-component then ideally each one should be implemented as
a separate pull request.
* Last PR should include changes to any user facing documentation. And should include
end to end tests if applicable. The component must be enabled
only after sufficient testing, and there is enough confidence in the
stability and quality of the component.
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [slack](https://signoz.io/slack).
- If you find any bugs, please create an issue

View File

@ -10,7 +10,6 @@ BUILD_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
# Internal variables or constants.
FRONTEND_DIRECTORY ?= frontend
FLATTENER_DIRECTORY ?= pkg/processors/flattener
QUERY_SERVICE_DIRECTORY ?= pkg/query-service
STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup
SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup
@ -20,7 +19,6 @@ DOCKER_TAG ?= latest
FRONTEND_DOCKER_IMAGE ?= frontend
QUERY_SERVICE_DOCKER_IMAGE ?= query-service
FLATTERNER_DOCKER_IMAGE ?= flattener-processor
# Build-time Go variables
PACKAGE?=go.signoz.io/query-service
@ -31,7 +29,7 @@ gitBranch=${PACKAGE}/version.gitBranch
LD_FLAGS="-X ${buildHash}=${BUILD_HASH} -X ${buildTime}=${BUILD_TIME} -X ${buildVersion}=${BUILD_VERSION} -X ${gitBranch}=${BUILD_BRANCH}"
all: build-push-frontend build-push-query-service build-push-flattener
all: build-push-frontend build-push-query-service
# 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)
@ -73,27 +71,6 @@ build-push-query-service:
--push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS=$(LD_FLAGS) \
--tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) .
# Steps to build and push docker image of flattener
.PHONY: build-flattener-amd64 build-push-flattener
# Step to build docker image of flattener in amd64 (used in build pipeline)
build-flattener-amd64:
@echo "------------------"
@echo "--> Building flattener docker image for amd64"
@echo "------------------"
@cd $(FLATTENER_DIRECTORY) && \
docker build -f Dockerfile --no-cache -t $(REPONAME)/$(FLATTERNER_DOCKER_IMAGE):$(DOCKER_TAG) \
--build-arg TARGETPLATFORM="linux/amd64" .
# Step to build and push docker image of flattener in amd64 (used in push pipeline)
build-push-flattener:
@echo "------------------"
@echo "--> Building and pushing flattener docker image"
@echo "------------------"
@cd $(FLATTENER_DIRECTORY) && \
docker buildx build --file Dockerfile --progress plane \
--no-cache --push --platform linux/arm64,linux/amd64 \
--tag $(REPONAME)/$(FLATTERNER_DOCKER_IMAGE):$(DOCKER_TAG) .
dev-setup:
mkdir -p /var/lib/signoz
sqlite3 /var/lib/signoz/signoz.db "VACUUM";

18
SECURITY.md Normal file
View File

@ -0,0 +1,18 @@
# Security Policy
SigNoz is looking forward to working with security researchers across the world to keep SigNoz and our users safe. If you have found an issue in our systems/applications, please reach out to us.
## Supported Versions
We always recommend using the latest version of SigNoz to ensure you get all security updates
## Reporting a Vulnerability
If you believe you have found a security vulnerability within SigNoz, please let us know right away. We'll try and fix the problem as soon as possible.
**Do not report vulnerabilities using public GitHub issues**. Instead, email <security@signoz.io> with a detailed account of the issue. Please submit one issue per email, this helps us triage vulnerabilities.
Once we've received your email we'll keep you updated as we fix the vulnerability.
## Thanks
Thank you for keeping SigNoz and our users safe. 🙇

View File

@ -28,7 +28,7 @@ services:
volumes:
- ./data/alertmanager:/data
command:
- --queryService.url=http://query-service:8080
- --queryService.url=http://query-service:8085
- --storage.path=/data
depends_on:
- query-service
@ -37,10 +37,11 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.8.0
image: signoz/query-service:0.8.1
command: ["-config=/root/config/prometheus.yml"]
ports:
- "8080:8080"
# ports:
# - "6060:6060" # pprof port
# - "8080:8080" # query-service port
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
@ -64,7 +65,7 @@ services:
- clickhouse
frontend:
image: signoz/frontend:0.8.0
image: signoz/frontend:0.8.1
deploy:
restart_policy:
condition: on-failure
@ -77,7 +78,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-0.2
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@ -85,7 +86,7 @@ services:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8889:8889" # Prometheus metrics exposed by the agent
# - "13133" # health_check
# - "13133:13133" # health_check
# - "14268:14268" # Jaeger receiver
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
@ -103,7 +104,7 @@ services:
- clickhouse
otel-collector-metrics:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-0.2
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -52,7 +52,7 @@ extensions:
health_check: {}
zpages: {}
exporters:
clickhouse:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
@ -66,7 +66,7 @@ service:
traces:
receivers: [jaeger, otlp]
processors: [signozspanmetrics/prometheus, batch]
exporters: [clickhouse]
exporters: [clickhousetraces]
metrics:
receivers: [otlp, hostmetrics]
processors: [batch]

View File

@ -12,13 +12,18 @@ server {
gzip_http_version 1.1;
location / {
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0";
add_header Last-Modified $date_gmt;
if ( $uri = '/index.html' ) {
add_header Cache-Control no-store always;
}
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/alertmanager {
proxy_pass http://alertmanager:9093/api/v2;
}
location /api {
proxy_pass http://query-service:8080/api;
}

View File

@ -30,16 +30,19 @@ services:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8080
- --queryService.url=http://query-service:8085
- --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:0.8.0
image: signoz/query-service:0.8.1
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
# - "8080:8080" # query-service port
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
@ -50,7 +53,6 @@ services:
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-arm
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
@ -62,7 +64,7 @@ services:
condition: service_healthy
frontend:
image: signoz/frontend:0.8.0
image: signoz/frontend:0.8.1
container_name: frontend
restart: on-failure
depends_on:
@ -74,7 +76,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-0.2
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@ -82,7 +84,7 @@ services:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8889:8889" # Prometheus metrics exposed by the agent
# - "13133" # health_check
# - "13133:13133" # health_check
# - "14268:14268" # Jaeger receiver
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
@ -95,7 +97,7 @@ services:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-0.2
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -30,15 +30,18 @@ services:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8080
- --queryService.url=http://query-service:8085
- --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:0.8.0
image: signoz/query-service:0.8.1
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
# - "8080:8080" # query-service port
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
@ -60,7 +63,7 @@ services:
condition: service_healthy
frontend:
image: signoz/frontend:0.8.0
image: signoz/frontend:0.8.1
container_name: frontend
restart: on-failure
depends_on:
@ -72,7 +75,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-0.2
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@ -80,7 +83,7 @@ services:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8889:8889" # Prometheus metrics exposed by the agent
# - "13133" # health_check
# - "13133:13133" # health_check
# - "14268:14268" # Jaeger receiver
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
@ -93,7 +96,7 @@ services:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-0.2
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -52,7 +52,7 @@ extensions:
health_check: {}
zpages: {}
exporters:
clickhouse:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
@ -66,7 +66,7 @@ service:
traces:
receivers: [jaeger, otlp]
processors: [signozspanmetrics/prometheus, batch]
exporters: [clickhouse]
exporters: [clickhousetraces]
metrics:
receivers: [otlp, hostmetrics]
processors: [batch]

View File

@ -9,17 +9,18 @@ server {
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_http_version 1.1;
location / {
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0";
add_header Last-Modified $date_gmt;
if ( $uri = '/index.html' ) {
add_header Cache-Control no-store always;
}
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/alertmanager{
location /api/alertmanager {
proxy_pass http://alertmanager:9093/api/v2;
}

View File

@ -1,273 +0,0 @@
version: "2.4"
volumes:
metadata_data: {}
middle_var: {}
historical_var: {}
broker_var: {}
coordinator_var: {}
router_var: {}
# If able to connect to kafka but not able to write to topic otlp_spans look into below link
# https://github.com/wurstmeister/kafka-docker/issues/409#issuecomment-428346707
services:
zookeeper:
image: bitnami/zookeeper:3.6.2-debian-10-r100
ports:
- "2181:2181"
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
# image: wurstmeister/kafka
image: bitnami/kafka:2.7.0-debian-10-r1
ports:
- "9092:9092"
hostname: kafka
environment:
KAFKA_ADVERTISED_HOST_NAME: kafka
KAFKA_ADVERTISED_PORT: 9092
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
ALLOW_PLAINTEXT_LISTENER: 'yes'
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: 'true'
KAFKA_TOPICS: 'otlp_spans:1:1,flattened_spans:1:1'
healthcheck:
# test: ["CMD", "kafka-topics.sh", "--create", "--topic", "otlp_spans", "--zookeeper", "zookeeper:2181"]
test: ["CMD", "kafka-topics.sh", "--list", "--zookeeper", "zookeeper:2181"]
interval: 30s
timeout: 10s
retries: 10
depends_on:
- zookeeper
postgres:
container_name: postgres
image: postgres:latest
volumes:
- metadata_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=FoolishPassword
- POSTGRES_USER=druid
- POSTGRES_DB=druid
coordinator:
image: apache/druid:0.20.0
container_name: coordinator
volumes:
- ./storage:/opt/data
- coordinator_var:/opt/druid/var
depends_on:
- zookeeper
- postgres
ports:
- "8081:8081"
command:
- coordinator
env_file:
- environment_tiny/coordinator
- environment_tiny/common
broker:
image: apache/druid:0.20.0
container_name: broker
volumes:
- broker_var:/opt/druid/var
depends_on:
- zookeeper
- postgres
- coordinator
ports:
- "8082:8082"
command:
- broker
env_file:
- environment_tiny/broker
- environment_tiny/common
historical:
image: apache/druid:0.20.0
container_name: historical
volumes:
- ./storage:/opt/data
- historical_var:/opt/druid/var
depends_on:
- zookeeper
- postgres
- coordinator
ports:
- "8083:8083"
command:
- historical
env_file:
- environment_tiny/historical
- environment_tiny/common
middlemanager:
image: apache/druid:0.20.0
container_name: middlemanager
volumes:
- ./storage:/opt/data
- middle_var:/opt/druid/var
depends_on:
- zookeeper
- postgres
- coordinator
ports:
- "8091:8091"
command:
- middleManager
env_file:
- environment_tiny/middlemanager
- environment_tiny/common
router:
image: apache/druid:0.20.0
container_name: router
volumes:
- router_var:/opt/druid/var
depends_on:
- zookeeper
- postgres
- coordinator
ports:
- "8888:8888"
command:
- router
env_file:
- environment_tiny/router
- environment_tiny/common
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://router:8888/druid/coordinator/v1/datasources/flattened_spans"]
interval: 30s
timeout: 5s
retries: 5
flatten-processor:
image: signoz/flattener-processor:0.4.0
container_name: flattener-processor
depends_on:
- kafka
- otel-collector
ports:
- "8000:8000"
environment:
- KAFKA_BROKER=kafka:9092
- KAFKA_INPUT_TOPIC=otlp_spans
- KAFKA_OUTPUT_TOPIC=flattened_spans
query-service:
image: signoz.docker.scarf.sh/signoz/query-service:0.4.1
container_name: query-service
depends_on:
router:
condition: service_healthy
ports:
- "8080:8080"
volumes:
- ../dashboards:/root/config/dashboards
- ./data/signoz/:/var/lib/signoz/
environment:
- DruidClientUrl=http://router:8888
- DruidDatasource=flattened_spans
- STORAGE=druid
- POSTHOG_API_KEY=H-htDCae7CR3RV57gUzmol6IAKtm5IMCvbcm_fwnL-w
- GODEBUG=netdns=go
frontend:
image: signoz/frontend:0.4.1
container_name: frontend
depends_on:
- query-service
links:
- "query-service"
ports:
- "3301:3301"
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
create-supervisor:
image: theithollow/hollowapp-blog:curl
container_name: create-supervisor
command:
- /bin/sh
- -c
- "curl -X POST -H 'Content-Type: application/json' -d @/app/supervisor-spec.json http://router:8888/druid/indexer/v1/supervisor"
depends_on:
- router
restart: on-failure:6
volumes:
- ./druid-jobs/supervisor-spec.json:/app/supervisor-spec.json
set-retention:
image: theithollow/hollowapp-blog:curl
container_name: set-retention
command:
- /bin/sh
- -c
- "curl -X POST -H 'Content-Type: application/json' -d @/app/retention-spec.json http://router:8888/druid/coordinator/v1/rules/flattened_spans"
depends_on:
- router
restart: on-failure:6
volumes:
- ./druid-jobs/retention-spec.json:/app/retention-spec.json
otel-collector:
image: otel/opentelemetry-collector:0.18.0
command: ["--config=/etc/otel-collector-config.yaml", "--mem-ballast-size-mib=683"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "1777:1777" # pprof extension
- "8887:8888" # Prometheus metrics exposed by the agent
- "14268:14268" # Jaeger receiver
- "55678" # OpenCensus receiver
- "55680:55680" # OTLP HTTP/2.0 legacy port
- "55681:55681" # OTLP HTTP/1.0 receiver
- "4317:4317" # OTLP GRPC receiver
- "55679:55679" # zpages extension
- "13133" # health_check
depends_on:
kafka:
condition: service_healthy
hotrod:
image: jaegertracing/example-hotrod:latest
container_name: hotrod
ports:
- "9000:8080"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
container_name: load-hotrod
hostname: load-hotrod
ports:
- "8089:8089"
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../common/locust-scripts:/locust

View File

@ -1,269 +0,0 @@
version: "2.4"
volumes:
metadata_data: {}
middle_var: {}
historical_var: {}
broker_var: {}
coordinator_var: {}
router_var: {}
# If able to connect to kafka but not able to write to topic otlp_spans look into below link
# https://github.com/wurstmeister/kafka-docker/issues/409#issuecomment-428346707
services:
zookeeper:
image: bitnami/zookeeper:3.6.2-debian-10-r100
ports:
- "2181:2181"
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
# image: wurstmeister/kafka
image: bitnami/kafka:2.7.0-debian-10-r1
ports:
- "9092:9092"
hostname: kafka
environment:
KAFKA_ADVERTISED_HOST_NAME: kafka
KAFKA_ADVERTISED_PORT: 9092
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
ALLOW_PLAINTEXT_LISTENER: 'yes'
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: 'true'
KAFKA_TOPICS: 'otlp_spans:1:1,flattened_spans:1:1'
healthcheck:
# test: ["CMD", "kafka-topics.sh", "--create", "--topic", "otlp_spans", "--zookeeper", "zookeeper:2181"]
test: ["CMD", "kafka-topics.sh", "--list", "--zookeeper", "zookeeper:2181"]
interval: 30s
timeout: 10s
retries: 10
depends_on:
- zookeeper
postgres:
container_name: postgres
image: postgres:latest
volumes:
- metadata_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=FoolishPassword
- POSTGRES_USER=druid
- POSTGRES_DB=druid
coordinator:
image: apache/druid:0.20.0
container_name: coordinator
volumes:
- ./storage:/opt/druid/deepStorage
- coordinator_var:/opt/druid/data
depends_on:
- zookeeper
- postgres
ports:
- "8081:8081"
command:
- coordinator
env_file:
- environment_small/coordinator
broker:
image: apache/druid:0.20.0
container_name: broker
volumes:
- broker_var:/opt/druid/data
depends_on:
- zookeeper
- postgres
- coordinator
ports:
- "8082:8082"
command:
- broker
env_file:
- environment_small/broker
historical:
image: apache/druid:0.20.0
container_name: historical
volumes:
- ./storage:/opt/druid/deepStorage
- historical_var:/opt/druid/data
depends_on:
- zookeeper
- postgres
- coordinator
ports:
- "8083:8083"
command:
- historical
env_file:
- environment_small/historical
middlemanager:
image: apache/druid:0.20.0
container_name: middlemanager
volumes:
- ./storage:/opt/druid/deepStorage
- middle_var:/opt/druid/data
depends_on:
- zookeeper
- postgres
- coordinator
ports:
- "8091:8091"
command:
- middleManager
env_file:
- environment_small/middlemanager
router:
image: apache/druid:0.20.0
container_name: router
volumes:
- router_var:/opt/druid/data
depends_on:
- zookeeper
- postgres
- coordinator
ports:
- "8888:8888"
command:
- router
env_file:
- environment_small/router
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://router:8888/druid/coordinator/v1/datasources/flattened_spans"]
interval: 30s
timeout: 5s
retries: 5
flatten-processor:
image: signoz/flattener-processor:0.4.0
container_name: flattener-processor
depends_on:
- kafka
- otel-collector
ports:
- "8000:8000"
environment:
- KAFKA_BROKER=kafka:9092
- KAFKA_INPUT_TOPIC=otlp_spans
- KAFKA_OUTPUT_TOPIC=flattened_spans
query-service:
image: signoz.docker.scarf.sh/signoz/query-service:0.4.1
container_name: query-service
depends_on:
router:
condition: service_healthy
ports:
- "8080:8080"
volumes:
- ../dashboards:/root/config/dashboards
- ./data/signoz/:/var/lib/signoz/
environment:
- DruidClientUrl=http://router:8888
- DruidDatasource=flattened_spans
- STORAGE=druid
- POSTHOG_API_KEY=H-htDCae7CR3RV57gUzmol6IAKtm5IMCvbcm_fwnL-w
- GODEBUG=netdns=go
frontend:
image: signoz/frontend:0.4.1
container_name: frontend
depends_on:
- query-service
links:
- "query-service"
ports:
- "3301:3301"
volumes:
- ./nginx-config.conf:/etc/nginx/conf.d/default.conf
create-supervisor:
image: theithollow/hollowapp-blog:curl
container_name: create-supervisor
command:
- /bin/sh
- -c
- "curl -X POST -H 'Content-Type: application/json' -d @/app/supervisor-spec.json http://router:8888/druid/indexer/v1/supervisor"
depends_on:
- router
restart: on-failure:6
volumes:
- ./druid-jobs/supervisor-spec.json:/app/supervisor-spec.json
set-retention:
image: theithollow/hollowapp-blog:curl
container_name: set-retention
command:
- /bin/sh
- -c
- "curl -X POST -H 'Content-Type: application/json' -d @/app/retention-spec.json http://router:8888/druid/coordinator/v1/rules/flattened_spans"
depends_on:
- router
restart: on-failure:6
volumes:
- ./druid-jobs/retention-spec.json:/app/retention-spec.json
otel-collector:
image: otel/opentelemetry-collector:0.18.0
command: ["--config=/etc/otel-collector-config.yaml", "--mem-ballast-size-mib=683"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "1777:1777" # pprof extension
- "8887:8888" # Prometheus metrics exposed by the agent
- "14268:14268" # Jaeger receiver
- "55678" # OpenCensus receiver
- "55680:55680" # OTLP HTTP/2.0 leagcy grpc receiver
- "55681:55681" # OTLP HTTP/1.0 receiver
- "4317:4317" # OTLP GRPC receiver
- "55679:55679" # zpages extension
- "13133" # health_check
depends_on:
kafka:
condition: service_healthy
hotrod:
image: jaegertracing/example-hotrod:latest
container_name: hotrod
ports:
- "9000:8080"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
container_name: load-hotrod
hostname: load-hotrod
ports:
- "8089:8089"
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ./locust-scripts:/locust

View File

@ -1 +0,0 @@
[{"period":"P3D","includeFuture":true,"tieredReplicants":{"_default_tier":1},"type":"loadByPeriod"},{"type":"dropForever"}]

View File

@ -1,69 +0,0 @@
{
"type": "kafka",
"dataSchema": {
"dataSource": "flattened_spans",
"parser": {
"type": "string",
"parseSpec": {
"format": "json",
"timestampSpec": {
"column": "StartTimeUnixNano",
"format": "nano"
},
"dimensionsSpec": {
"dimensions": [
"TraceId",
"SpanId",
"ParentSpanId",
"Name",
"ServiceName",
"References",
"Tags",
"ExternalHttpMethod",
"ExternalHttpUrl",
"Component",
"DBSystem",
"DBName",
"DBOperation",
"PeerService",
{
"type": "string",
"name": "TagsKeys",
"multiValueHandling": "ARRAY"
},
{
"type": "string",
"name": "TagsValues",
"multiValueHandling": "ARRAY"
},
{ "name": "DurationNano", "type": "Long" },
{ "name": "Kind", "type": "int" },
{ "name": "StatusCode", "type": "int" }
]
}
}
},
"metricsSpec" : [
{ "type": "quantilesDoublesSketch", "name": "QuantileDuration", "fieldName": "DurationNano" }
],
"granularitySpec": {
"type": "uniform",
"segmentGranularity": "DAY",
"queryGranularity": "NONE",
"rollup": false
}
},
"tuningConfig": {
"type": "kafka",
"reportParseExceptions": true
},
"ioConfig": {
"topic": "flattened_spans",
"replicas": 1,
"taskDuration": "PT20M",
"completionTimeout": "PT30M",
"consumerProperties": {
"bootstrap.servers": "kafka:9092"
}
}
}

View File

@ -1,53 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=512m
DRUID_XMS=512m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=768m
druid_emitter_logging_logLevel=debug
druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms512m", "-Xmx512m", "-XX:MaxDirectMemorySize=768m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_processing_buffer_sizeBytes=100MiB
druid_storage_type=local
druid_storage_storageDirectory=/opt/druid/deepStorage
druid_indexer_logs_type=file
druid_indexer_logs_directory=/opt/druid/data/indexing-logs
druid_processing_numThreads=1
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,52 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=64m
DRUID_XMS=64m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=400m
druid_emitter_logging_logLevel=debug
druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms64m", "-Xmx64m", "-XX:MaxDirectMemorySize=400m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_storage_type=local
druid_storage_storageDirectory=/opt/druid/deepStorage
druid_indexer_logs_type=file
druid_indexer_logs_directory=/opt/druid/data/indexing-logs
druid_processing_numThreads=1
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,53 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=512m
DRUID_XMS=512m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=1280m
druid_emitter_logging_logLevel=debug
druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms512m", "-Xmx512m", "-XX:MaxDirectMemorySize=1280m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_processing_buffer_sizeBytes=200MiB
druid_storage_type=local
druid_storage_storageDirectory=/opt/druid/deepStorage
druid_indexer_logs_type=file
druid_indexer_logs_directory=/opt/druid/data/indexing-logs
druid_processing_numThreads=2
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,53 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=1g
DRUID_XMS=1g
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=2g
druid_emitter_logging_logLevel=debug
druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms1g", "-Xmx1g", "-XX:MaxDirectMemorySize=2g", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_processing_buffer_sizeBytes=200MiB
druid_storage_type=local
druid_storage_storageDirectory=/opt/druid/deepStorage
druid_indexer_logs_type=file
druid_indexer_logs_directory=/opt/druid/data/indexing-logs
druid_processing_numThreads=2
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,52 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=128m
DRUID_XMS=128m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=128m
druid_emitter_logging_logLevel=debug
druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms128m", "-Xmx128m", "-XX:MaxDirectMemorySize=128m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_storage_type=local
druid_storage_storageDirectory=/opt/druid/deepStorage
druid_indexer_logs_type=file
druid_indexer_logs_directory=/opt/druid/data/indexing-logs
druid_processing_numThreads=1
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,52 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=512m
DRUID_XMS=512m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=400m
druid_emitter_logging_logLevel=debug
# druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms512m", "-Xmx512m", "-XX:MaxDirectMemorySize=400m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_processing_buffer_sizeBytes=50MiB
druid_processing_numThreads=1
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,26 +0,0 @@
# For S3 storage
# druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service", "druid-s3-extensions"]
# druid_storage_type=s3
# druid_storage_bucket=<s3-bucket-name>
# druid_storage_baseKey=druid/segments
# AWS_ACCESS_KEY_ID=<s3-access-id>
# AWS_SECRET_ACCESS_KEY=<s3-access-key>
# AWS_REGION=<s3-aws-region>
# druid_indexer_logs_type=s3
# druid_indexer_logs_s3Bucket=<s3-bucket-name>
# druid_indexer_logs_s3Prefix=druid/indexing-logs
# -----------------------------------------------------------
# For local storage
druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_storage_type=local
druid_storage_storageDirectory=/opt/data/segments
druid_indexer_logs_type=file
druid_indexer_logs_directory=/opt/data/indexing-logs

View File

@ -1,49 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=64m
DRUID_XMS=64m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=400m
druid_emitter_logging_logLevel=debug
# druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms64m", "-Xmx64m", "-XX:MaxDirectMemorySize=400m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_processing_numThreads=1
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,49 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=512m
DRUID_XMS=512m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=400m
druid_emitter_logging_logLevel=debug
# druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms512m", "-Xmx512m", "-XX:MaxDirectMemorySize=400m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_processing_buffer_sizeBytes=50MiB
druid_processing_numThreads=1
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,50 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=64m
DRUID_XMS=64m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=400m
druid_emitter_logging_logLevel=debug
# druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms256m", "-Xmx256m", "-XX:MaxDirectMemorySize=400m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_processing_numThreads=1
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,49 +0,0 @@
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Java tuning
DRUID_XMX=64m
DRUID_XMS=64m
DRUID_MAXNEWSIZE=256m
DRUID_NEWSIZE=256m
DRUID_MAXDIRECTMEMORYSIZE=128m
druid_emitter_logging_logLevel=debug
# druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage", "druid-kafka-indexing-service"]
druid_zk_service_host=zookeeper
druid_metadata_storage_host=
druid_metadata_storage_type=postgresql
druid_metadata_storage_connector_connectURI=jdbc:postgresql://postgres:5432/druid
druid_metadata_storage_connector_user=druid
druid_metadata_storage_connector_password=FoolishPassword
druid_coordinator_balancer_strategy=cachingCost
druid_indexer_runner_javaOptsArray=["-server", "-Xms64m", "-Xmx64m", "-XX:MaxDirectMemorySize=128m", "-Duser.timezone=UTC", "-Dfile.encoding=UTF-8", "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]
druid_indexer_fork_property_druid_processing_buffer_sizeBytes=25000000
druid_processing_numThreads=1
druid_processing_numMergeBuffers=2
DRUID_LOG4J=<?xml version="1.0" encoding="UTF-8" ?><Configuration status="WARN"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayout pattern="%d{ISO8601} %p [%t] %c - %m%n"/></Console></Appenders><Loggers><Root level="info"><AppenderRef ref="Console"/></Root><Logger name="org.apache.druid.jetty.RequestLog" additivity="false" level="DEBUG"><AppenderRef ref="Console"/></Logger></Loggers></Configuration>

View File

@ -1,51 +0,0 @@
receivers:
otlp:
protocols:
grpc:
http:
jaeger:
protocols:
grpc:
thrift_http:
processors:
batch:
send_batch_size: 1000
timeout: 10s
memory_limiter:
# Same as --mem-ballast-size-mib CLI argument
ballast_size_mib: 683
# 80% of maximum memory up to 2G
limit_mib: 1500
# 25% of limit up to 2G
spike_limit_mib: 512
check_interval: 5s
queued_retry:
num_workers: 4
queue_size: 100
retry_on_failure: true
extensions:
health_check: {}
zpages: {}
exporters:
kafka/traces:
brokers:
- kafka:9092
topic: 'otlp_spans'
protocol_version: 2.0.0
kafka/metrics:
brokers:
- kafka:9092
topic: 'otlp_metrics'
protocol_version: 2.0.0
service:
extensions: [health_check, zpages]
pipelines:
traces:
receivers: [jaeger, otlp]
processors: [memory_limiter, batch, queued_retry]
exporters: [kafka/traces]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [kafka/metrics]

View File

@ -333,7 +333,6 @@ fi
# echo -e "👉 ${RED}Two ways to go forward\n"
# echo -e "${RED}1) ClickHouse as database (default)\n"
# echo -e "${RED}2) Kafka + Druid as datastore \n"
# read -p "⚙️ Enter your preference (1/2):" choice_setup
# while [[ $choice_setup != "1" && $choice_setup != "2" && $choice_setup != "" ]]
@ -346,8 +345,6 @@ fi
# if [[ $choice_setup == "1" || $choice_setup == "" ]];then
# setup_type='clickhouse'
# else
# setup_type='druid'
# fi
setup_type='clickhouse'

View File

@ -1,4 +1,5 @@
node_modules
.vscode
build
.env
.env
.git

4
frontend/.husky/commit-msg Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd frontend && npm run commitlint

View File

@ -1 +1 @@
12.13.0
16.15.0

View File

@ -1,5 +1,5 @@
# stage1 as builder
FROM node:12.22.0 as builder
# Builder stage
FROM node:16.15.0-slim as builder
# Add Maintainer Info
LABEL maintainer="signoz"
@ -9,24 +9,23 @@ ARG TARGETARCH
WORKDIR /frontend
# copy the package.json to install dependencies
# Copy the package.json to install dependencies
COPY package.json ./
# Install the dependencies and make the folder
RUN yarn install
RUN CI=1 yarn install
COPY . .
# Build the project and copy the files
RUN yarn build
FROM nginx:1.18-alpine
#!/bin/sh
FROM nginx:1.18-alpine
COPY conf/default.conf /etc/nginx/conf.d/default.conf
## Remove default nginx index page
# Remove default nginx index page
RUN rm -rf /usr/share/nginx/html/*
# Copy from the stahg 1
@ -34,4 +33,4 @@ COPY --from=builder /frontend/build /usr/share/nginx/html
EXPOSE 3301
ENTRYPOINT ["nginx", "-g", "daemon off;"]
ENTRYPOINT ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

View File

@ -1,3 +0,0 @@
{
"video": false
}

View File

@ -1,48 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
const Login = ({ email, name }: LoginProps): void => {
const emailInput = cy.findByPlaceholderText('name@yourcompany.com');
emailInput.then((emailInput) => {
const element = emailInput[0];
// element is present
expect(element).not.undefined;
expect(element.nodeName).to.be.equal('INPUT');
});
emailInput.type(email).then((inputElements) => {
const inputElement = inputElements[0];
const inputValue = inputElement.getAttribute('value');
expect(inputValue).to.be.equals(email);
});
const firstNameInput = cy.findByPlaceholderText('Your Name');
firstNameInput.then((firstNameInput) => {
const element = firstNameInput[0];
// element is present
expect(element).not.undefined;
expect(element.nodeName).to.be.equal('INPUT');
});
firstNameInput.type(name).then((inputElements) => {
const inputElement = inputElements[0];
const inputValue = inputElement.getAttribute('value');
expect(inputValue).to.be.equals(name);
});
const gettingStartedButton = cy.findByText('Get Started');
gettingStartedButton.click();
cy
.intercept('POST', '/api/v1/user?email*', {
statusCode: 200,
})
.as('defaultUser');
cy.wait('@defaultUser');
};
export interface LoginProps {
email: string;
name: string;
}
export default Login;

View File

@ -1,49 +0,0 @@
import {
getDefaultOption,
getOptions,
} from 'container/Header/DateTimeSelection/config';
// import { AppState } from 'store/reducers';
const CheckRouteDefaultGlobalTimeOptions = ({
route,
}: CheckRouteDefaultGlobalTimeOptionsProps): void => {
cy.visit(Cypress.env('baseUrl') + route);
const allOptions = getOptions(route);
const defaultValue = getDefaultOption(route);
const defaultSelectedOption = allOptions.find((e) => e.value === defaultValue);
expect(defaultSelectedOption).not.undefined;
cy
.findAllByTestId('dropDown')
.find('span')
.then((el) => {
const elements = el.get();
const item = elements[1];
expect(defaultSelectedOption?.label).to.be.equals(
item.innerText,
'Default option is not matching',
);
});
// cy
// .window()
// .its('store')
// .invoke('getState')
// .then((e: AppState) => {
// const { globalTime } = e;
// const { maxTime, minTime } = globalTime;
// // @TODO match the global min time and max time according to the selected option
// });
};
export interface CheckRouteDefaultGlobalTimeOptionsProps {
route: string;
}
export default CheckRouteDefaultGlobalTimeOptions;

View File

@ -1,11 +0,0 @@
const resizeObserverLoopErrRe = /ResizeObserver loop limit exceeded/;
const unCaughtExpection = (): void => {
cy.on('uncaught:exception', (err) => {
// returning false here prevents Cypress from
// failing the test
return !resizeObserverLoopErrRe.test(err.message);
});
};
export default unCaughtExpection;

View File

@ -1,21 +0,0 @@
{
"data": [
{
"created_at": 1638083159246,
"data": "{}",
"id": 1,
"name": "First Channels",
"type": "slack",
"updated_at": 1638083159246
},
{
"created_at": 1638083159246,
"data": "{}",
"id": 2,
"name": "Second Channels",
"type": "Slack",
"updated_at": 1638083159246
}
],
"message": "Success"
}

View File

@ -1,35 +0,0 @@
[
{
"serviceName": "frontend",
"p99": 1134610000,
"avgDuration": 744523000,
"numCalls": 267,
"callRate": 0.89,
"numErrors": 0,
"errorRate": 0,
"num4XX": 0,
"fourXXRate": 0
},
{
"serviceName": "customer",
"p99": 734422400,
"avgDuration": 348678530,
"numCalls": 267,
"callRate": 0.89,
"numErrors": 0,
"errorRate": 0,
"num4XX": 0,
"fourXXRate": 0
},
{
"serviceName": "driver",
"p99": 239234080,
"avgDuration": 204662290,
"numCalls": 267,
"callRate": 0.89,
"numErrors": 0,
"errorRate": 0,
"num4XX": 0,
"fourXXRate": 0
}
]

View File

@ -1,28 +0,0 @@
{
"status": "success",
"data": {
"rules": [
{
"labels": { "severity": "warning" },
"annotations": {},
"state": "firing",
"name": "First Rule",
"id": 1
},
{
"labels": { "severity": "warning" },
"annotations": {},
"state": "firing",
"name": "Second Rule",
"id": 2
},
{
"labels": { "severity": "P0" },
"annotations": {},
"state": "firing",
"name": "Third Rule",
"id": 3
}
]
}
}

View File

@ -1 +0,0 @@
{ "status": "success", "data": { "resultType": "matrix", "result": [] } }

View File

@ -1,29 +0,0 @@
{
"status": "success",
"data": {
"resultType": "matrix",
"result": [
{
"metric": {},
"values": [
[1634741764.961, "0.9"],
[1634741824.961, "0.9"],
[1634741884.961, "0.8666666666666667"],
[1634741944.961, "1"],
[1634742004.961, "0.9166666666666666"],
[1634742064.961, "0.95"],
[1634742124.961, "0.9333333333333333"],
[1634742184.961, "0.95"],
[1634742244.961, "1.0333333333333334"],
[1634742304.961, "0.9333333333333333"],
[1634742364.961, "0.9166666666666666"],
[1634742424.961, "0.9"],
[1634742484.961, "1.0166666666666666"],
[1634742544.961, "0.8333333333333334"],
[1634742604.961, "0.9166666666666666"],
[1634742664.961, "0.95"]
]
}
]
}
}

View File

@ -1,62 +0,0 @@
[
{
"timestamp": 1634742600000000000,
"p50": 720048500,
"p95": 924409540,
"p99": 974744300,
"numCalls": 48,
"callRate": 0.8,
"numErrors": 0,
"errorRate": 0
},
{
"timestamp": 1634742540000000000,
"p50": 712614000,
"p95": 955580700,
"p99": 1045595400,
"numCalls": 59,
"callRate": 0.98333335,
"numErrors": 0,
"errorRate": 0
},
{
"timestamp": 1634742480000000000,
"p50": 720842000,
"p95": 887187600,
"p99": 943676860,
"numCalls": 53,
"callRate": 0.8833333,
"numErrors": 0,
"errorRate": 0
},
{
"timestamp": 1634742420000000000,
"p50": 712287000,
"p95": 908505540,
"p99": 976507650,
"numCalls": 58,
"callRate": 0.96666664,
"numErrors": 0,
"errorRate": 0
},
{
"timestamp": 1634742360000000000,
"p50": 697125500,
"p95": 975581800,
"p99": 1190121900,
"numCalls": 54,
"callRate": 0.9,
"numErrors": 0,
"errorRate": 0
},
{
"timestamp": 1634742300000000000,
"p50": 711592500,
"p95": 880559900,
"p99": 1100105500,
"numCalls": 40,
"callRate": 0.6666667,
"numErrors": 0,
"errorRate": 0
}
]

View File

@ -1,9 +0,0 @@
[
{
"p50": 710824000,
"p95": 1003231400,
"p99": 1231265500,
"numCalls": 299,
"name": "HTTP GET /dispatch"
}
]

View File

@ -1,35 +0,0 @@
{
"items": {
"1644926280000000000": { "timestamp": 1644926280000000000, "value": 787 },
"1644926340000000000": { "timestamp": 1644926340000000000, "value": 2798 },
"1644926400000000000": { "timestamp": 1644926400000000000, "value": 2828 },
"1644926460000000000": { "timestamp": 1644926460000000000, "value": 2926 },
"1644926520000000000": { "timestamp": 1644926520000000000, "value": 2932 },
"1644926580000000000": { "timestamp": 1644926580000000000, "value": 2842 },
"1644926640000000000": { "timestamp": 1644926640000000000, "value": 2966 },
"1644926700000000000": { "timestamp": 1644926700000000000, "value": 2782 },
"1644926760000000000": { "timestamp": 1644926760000000000, "value": 2843 },
"1644926820000000000": { "timestamp": 1644926820000000000, "value": 2864 },
"1644926880000000000": { "timestamp": 1644926880000000000, "value": 2777 },
"1644926940000000000": { "timestamp": 1644926940000000000, "value": 2820 },
"1644927000000000000": { "timestamp": 1644927000000000000, "value": 2579 },
"1644927060000000000": { "timestamp": 1644927060000000000, "value": 2681 },
"1644927120000000000": { "timestamp": 1644927120000000000, "value": 2828 },
"1644927180000000000": { "timestamp": 1644927180000000000, "value": 2975 },
"1644927240000000000": { "timestamp": 1644927240000000000, "value": 2934 },
"1644927300000000000": { "timestamp": 1644927300000000000, "value": 2793 },
"1644927360000000000": { "timestamp": 1644927360000000000, "value": 2913 },
"1644927420000000000": { "timestamp": 1644927420000000000, "value": 2621 },
"1644927480000000000": { "timestamp": 1644927480000000000, "value": 2631 },
"1644927540000000000": { "timestamp": 1644927540000000000, "value": 2924 },
"1644927600000000000": { "timestamp": 1644927600000000000, "value": 2576 },
"1644927660000000000": { "timestamp": 1644927660000000000, "value": 2878 },
"1644927720000000000": { "timestamp": 1644927720000000000, "value": 2737 },
"1644927780000000000": { "timestamp": 1644927780000000000, "value": 2621 },
"1644927840000000000": { "timestamp": 1644927840000000000, "value": 2823 },
"1644927900000000000": { "timestamp": 1644927900000000000, "value": 3081 },
"1644927960000000000": { "timestamp": 1644927960000000000, "value": 2883 },
"1644928020000000000": { "timestamp": 1644928020000000000, "value": 2823 },
"1644928080000000000": { "timestamp": 1644928080000000000, "value": 455 }
}
}

View File

@ -1,19 +0,0 @@
{
"serviceName": {
"customer": 1642,
"driver": 1642,
"frontend": 39408,
"mysql": 1642,
"redis": 22167,
"route": 16420
},
"status": { "error": 4105, "ok": 78816 },
"duration": { "maxDuration": 1253979000, "minDuration": 415000 },
"operation": {},
"httpCode": {},
"httpUrl": {},
"httpMethod": {},
"httpRoute": {},
"httpHost": {},
"component": {}
}

View File

@ -1,105 +0,0 @@
{
"spans": [
{
"timestamp": "2022-02-15T12:16:09.542074Z",
"spanID": "303b39065c6f5df5",
"traceID": "00000000000000007fc49fab3cb75958",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 313418000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:08.84038Z",
"spanID": "557e8303bc802992",
"traceID": "000000000000000079310bd1d435a92b",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 318203000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:08.867689Z",
"spanID": "347113dd916dd20e",
"traceID": "00000000000000004c22c0409cee0f66",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 512810000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:07.060882Z",
"spanID": "0a8d07f72aa1339b",
"traceID": "0000000000000000488e11a35959de96",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 588705000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:07.134107Z",
"spanID": "0acd4ec344675998",
"traceID": "00000000000000000292efc7945d9bfa",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 801632000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:06.474095Z",
"spanID": "3ae72e433301822a",
"traceID": "00000000000000001ac3004ff1b7eefe",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 306650000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:06.996246Z",
"spanID": "1d765427af673039",
"traceID": "00000000000000002e78f59fabbcdecf",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 311469000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:05.324296Z",
"spanID": "0987c90d83298a1d",
"traceID": "0000000000000000077bcb960609a350",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 290680000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:02.458221Z",
"spanID": "5b0d0d403dd9acf4",
"traceID": "00000000000000007ae5b0aa69242556",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 262763000,
"httpCode": "200",
"httpMethod": "GET"
},
{
"timestamp": "2022-02-15T12:16:00.584939Z",
"spanID": "3beafb277a76b9b4",
"traceID": "00000000000000000ab44953c2fd949e",
"serviceName": "customer",
"operation": "HTTP GET /customer",
"durationNano": 302851000,
"httpCode": "200",
"httpMethod": "GET"
}
],
"totalSpans": 82921
}

View File

@ -1,24 +0,0 @@
/// <reference types="cypress" />
import ROUTES from 'constants/routes';
describe('App Layout', () => {
beforeEach(() => {
cy.visit(Cypress.env('baseUrl'));
});
it('Check the user is in Logged Out State', async () => {
cy.location('pathname').then((e) => {
expect(e).to.be.equal(ROUTES.SIGN_UP);
});
});
it('Logged In State', () => {
const testEmail = 'test@test.com';
const firstName = 'Test';
cy.login({
email: testEmail,
name: firstName,
});
});
});

View File

@ -1,52 +0,0 @@
/// <reference types="cypress" />
import ROUTES from 'constants/routes';
import defaultAllChannels from '../../fixtures/defaultAllChannels.json';
describe('Channels', () => {
beforeEach(() => {
window.localStorage.setItem('isLoggedIn', 'yes');
cy.visit(Cypress.env('baseUrl') + ROUTES.ALL_CHANNELS);
});
it('Channels', () => {
cy
.intercept('**channels**', {
statusCode: 200,
fixture: 'defaultAllChannels',
})
.as('All Channels');
cy.wait('@All Channels');
cy
.get('.ant-tabs-tab')
.children()
.then((e) => {
const child = e.get();
const secondChild = child[1];
expect(secondChild.outerText).to.be.equals('Alert Channels');
expect(secondChild.ariaSelected).to.be.equals('true');
});
cy
.get('tbody')
.should('be.visible')
.then((e) => {
const allChildren = e.children().get();
expect(allChildren.length).to.be.equals(defaultAllChannels.data.length);
allChildren.forEach((e, index) => {
expect(e.firstChild?.textContent).not.null;
expect(e.firstChild?.textContent).to.be.equals(
defaultAllChannels.data[index].name,
);
});
});
});
});

View File

@ -1,44 +0,0 @@
/// <reference types="cypress" />
import ROUTES from 'constants/routes';
describe('default time', () => {
beforeEach(() => {
window.localStorage.setItem('isLoggedIn', 'yes');
});
it('Metrics Page default time', () => {
cy.checkDefaultGlobalOption({
route: ROUTES.APPLICATION,
});
});
it('Dashboard Page default time', () => {
cy.checkDefaultGlobalOption({
route: ROUTES.ALL_DASHBOARD,
});
});
it('Trace Page default time', () => {
cy.checkDefaultGlobalOption({
route: ROUTES.TRACE,
});
});
it('Instrumentation Page default time', () => {
cy.checkDefaultGlobalOption({
route: ROUTES.INSTRUMENTATION,
});
});
it('Service Page default time', () => {
cy.checkDefaultGlobalOption({
route: ROUTES.SERVICE_MAP,
});
});
it('Settings Page default time', () => {
cy.checkDefaultGlobalOption({
route: ROUTES.SETTINGS,
});
});
});

View File

@ -1,126 +0,0 @@
/// <reference types="cypress" />
import getGlobalDropDownFormatedDate from 'lib/getGlobalDropDownFormatedDate';
import { AppState } from 'store/reducers';
import topEndPoints from '../../fixtures/topEndPoints.json';
describe('Global Time Metrics Application', () => {
beforeEach(() => {
cy.visit(Cypress.env('baseUrl'));
const testEmail = 'test@test.com';
const firstName = 'Test';
cy.login({
email: testEmail,
name: firstName,
});
});
it('Metrics Application', async () => {
cy
.intercept('GET', '/api/v1/services*', {
fixture: 'defaultApp.json',
})
.as('defaultApps');
cy.wait('@defaultApps');
//clicking on frontend
cy.get('tr:nth-child(1) > td:first-child').click();
cy
.intercept('GET', '/api/v1/service/top_endpoints*', {
fixture: 'topEndPoints.json',
})
.as('topEndPoints');
cy
.intercept('GET', '/api/v1/service/overview?*', {
fixture: 'serviceOverview.json',
})
.as('serviceOverview');
cy
.intercept(
'GET',
`/api/v1/query_range?query=sum(rate(signoz_latency_count*`,
{
fixture: 'requestPerSecond.json',
},
)
.as('requestPerSecond');
cy
.window()
.its('store')
.invoke('getState')
.then((e: AppState) => {
const { globalTime } = e;
const { maxTime, minTime } = globalTime;
// intercepting metrics application call
cy.wait('@topEndPoints');
cy.wait('@serviceOverview');
//TODO add errorPercentage also
// cy.wait('@errorPercentage');
cy.wait('@requestPerSecond');
cy
.get('tbody tr:first-child td:first-child')
.then((el) => {
const elements = el.get();
expect(elements.length).to.be.equals(1);
const element = elements[0];
expect(element.innerText).to.be.equals(topEndPoints[0].name);
})
.click();
cy
.findAllByTestId('dropDown')
.find('span.ant-select-selection-item')
.then((e) => {
const elements = e;
const element = elements[0];
const customSelectedTime = element.innerText;
const startTime = new Date(minTime / 1000000);
const endTime = new Date(maxTime / 1000000);
const startString = getGlobalDropDownFormatedDate(startTime);
const endString = getGlobalDropDownFormatedDate(endTime);
const result = `${startString} - ${endString}`;
expect(customSelectedTime).to.be.equals(result);
});
cy
.findByTestId('dropDown')
.click()
.then(() => {
cy.findByTitle('Last 30 min').click();
});
cy
.findByTestId('dropDown')
.find('span.ant-select-selection-item')
.then((e) => {
const elements = e;
const element = elements[0];
const selectedTime = element.innerText;
expect(selectedTime).to.be.equals('Last 30 min');
});
});
});
});

View File

@ -1,67 +0,0 @@
/// <reference types="cypress" />
import ROUTES from 'constants/routes';
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
import defaultApps from '../../fixtures/defaultApp.json';
describe('Metrics', () => {
beforeEach(() => {
cy.visit(Cypress.env('baseUrl'));
const testEmail = 'test@test.com';
const firstName = 'Test';
cy.login({
email: testEmail,
name: firstName,
});
});
it('Default Apps', () => {
cy
.intercept('GET', '/api/v1/services*', {
fixture: 'defaultApp.json',
})
.as('defaultApps');
cy.wait('@defaultApps');
cy.location().then((e) => {
expect(e.pathname).to.be.equals(ROUTES.APPLICATION);
cy.get('tbody').then((elements) => {
const trElements = elements.children();
expect(trElements.length).to.be.equal(defaultApps.length);
const getChildren = (row: Element): Element => {
if (row.children.length === 0) {
return row;
}
return getChildren(row.children[0]);
};
// this is row element
trElements.map((index, element) => {
const [
applicationElement,
p99Element,
errorRateElement,
rpsElement,
] = element.children;
const applicationName = getChildren(applicationElement).innerHTML;
const p99Name = getChildren(p99Element).innerHTML;
const errorRateName = getChildren(errorRateElement).innerHTML;
const rpsName = getChildren(rpsElement).innerHTML;
const { serviceName, p99, errorRate, callRate } = defaultApps[index];
expect(applicationName).to.be.equal(serviceName);
expect(p99Name).to.be.equal(convertToNanoSecondsToSecond(p99).toString());
expect(errorRateName).to.be.equals(
parseFloat(errorRate.toString()).toFixed(2),
);
expect(rpsName).to.be.equals(callRate.toString());
});
});
});
});
});
export {};

View File

@ -1,130 +0,0 @@
/// <reference types="cypress" />
import ROUTES from 'constants/routes';
import defaultRules from '../../fixtures/defaultRules.json';
const defaultRuleRoutes = `**/rules/**`;
describe('Alerts', () => {
beforeEach(() => {
window.localStorage.setItem('isLoggedIn', 'yes');
cy
.intercept('get', '*rules*', {
fixture: 'defaultRules',
})
.as('defaultRules');
cy.visit(Cypress.env('baseUrl') + `${ROUTES.LIST_ALL_ALERT}`);
cy.wait('@defaultRules');
});
it('Edit Rules Page Failure', async () => {
cy
.intercept(defaultRuleRoutes, {
statusCode: 500,
})
.as('Get Rules Error');
cy.get('button.ant-btn.ant-btn-link:nth-child(2)').then((e) => {
const firstDelete = e[0];
firstDelete.click();
cy.waitFor('@Get Rules Error');
cy
.window()
.location()
.then((e) => {
expect(e.pathname).to.be.equals(`/alerts/edit/1`);
});
cy.findByText('Something went wrong').then((e) => {
expect(e.length).to.be.equals(1);
});
});
});
it('Edit Rules Page Success', async () => {
const text = 'this is the sample value';
cy
.intercept(defaultRuleRoutes, {
statusCode: 200,
body: {
data: {
data: text,
},
},
})
.as('Get Rules Success');
cy.get('button.ant-btn.ant-btn-link:nth-child(2)').then((e) => {
const firstDelete = e[0];
firstDelete.click();
cy.waitFor('@Get Rules Success');
cy.wait(1000);
cy.findByText('Save').then((e) => {
const [el] = e.get();
el.click();
});
});
});
it('All Rules are rendered correctly', async () => {
cy
.window()
.location()
.then(({ pathname }) => {
expect(pathname).to.be.equals(ROUTES.LIST_ALL_ALERT);
cy.get('tbody').then((e) => {
const tarray = e.children().get();
expect(tarray.length).to.be.equals(3);
tarray.forEach(({ children }, index) => {
const name = children[1]?.textContent;
const label = children[2]?.textContent;
expect(name).to.be.equals(defaultRules.data.rules[index].name);
const defaultLabels = defaultRules.data.rules[index].labels;
expect(label).to.be.equals(defaultLabels['severity']);
});
});
});
});
it('Rules are Deleted', async () => {
cy
.intercept(defaultRuleRoutes, {
body: {
data: 'Deleted',
message: 'Success',
},
statusCode: 200,
})
.as('deleteRules');
cy.get('button.ant-btn.ant-btn-link:first-child').then((e) => {
const firstDelete = e[0];
firstDelete.click();
});
cy.wait('@deleteRules');
cy.get('tbody').then((e) => {
const trray = e.children().get();
expect(trray.length).to.be.equals(2);
});
});
});

View File

@ -1,160 +0,0 @@
/* eslint-disable sonarjs/no-duplicate-string */
import ROUTES from 'constants/routes';
import { AppState } from 'store/reducers';
import { TraceFilterEnum } from 'types/reducer/trace';
import GraphInitialResponse from '../../fixtures/trace/initialAggregates.json';
import FilterInitialResponse from '../../fixtures/trace/initialSpanFilter.json';
import TableInitialResponse from '../../fixtures/trace/initialSpans.json';
const allFilters = '@Filters.all';
const allGraphs = '@Graph.all';
const allTable = '@Table.all';
describe('Trace', () => {
beforeEach(() => {
window.localStorage.setItem('isLoggedIn', 'yes');
cy
.intercept('POST', '**/aggregates', {
fixture: 'trace/initialAggregates',
})
.as('Graph');
cy
.intercept('POST', '**/getFilteredSpans', {
fixture: 'trace/initialSpans',
})
.as('Table');
cy
.intercept('POST', '**/api/v1/getSpanFilters', {
fixture: 'trace/initialSpanFilter',
})
.as('Filters');
cy.visit(Cypress.env('baseUrl') + `${ROUTES.TRACE}`);
});
it('First Initial Load should go with 3 AJAX request', () => {
cy.wait(['@Filters', '@Graph', '@Table']).then((e) => {
const [filter, graph, table] = e;
const { body: filterBody } = filter.request;
const { body: graphBody } = graph.request;
const { body: tableBody } = table.request;
expect(filterBody.exclude.length).to.equal(0);
expect(filterBody.getFilters.length).to.equal(3);
filterBody.getFilters.forEach((filter: TraceFilterEnum) => {
expect(filter).to.be.oneOf(['duration', 'status', 'serviceName']);
});
expect(graphBody.function).to.be.equal('count');
expect(graphBody.exclude.length).to.be.equal(0);
expect(typeof graphBody.exclude).to.be.equal('object');
expect(tableBody.tags.length).to.be.equal(0);
expect(typeof tableBody.tags).equal('object');
expect(tableBody.exclude.length).equals(0);
});
});
it('Render Time Request Response In All 3 Request', () => {
cy.wait(['@Filters', '@Graph', '@Table']).then((e) => {
const [filter, graph, table] = e;
expect(filter.response?.body).to.be.not.undefined;
expect(filter.response?.body).to.be.not.NaN;
expect(JSON.stringify(filter.response?.body)).to.be.equals(
JSON.stringify(FilterInitialResponse),
);
expect(JSON.stringify(graph.response?.body)).to.be.equals(
JSON.stringify(GraphInitialResponse),
);
expect(JSON.stringify(table.response?.body)).to.be.equals(
JSON.stringify(TableInitialResponse),
);
});
cy.get(allFilters).should('have.length', 1);
cy.get(allGraphs).should('have.length', 1);
cy.get(allTable).should('have.length', 1);
});
it('Clear All', () => {
cy.wait(['@Filters', '@Graph', '@Table']);
expect(cy.findAllByText('Clear All')).not.to.be.undefined;
cy
.window()
.its('store')
.invoke('getState')
.then((e: AppState) => {
const { traces } = e;
expect(traces.isFilterExclude.get('status')).to.be.undefined;
expect(traces.selectedFilter.size).to.be.equals(0);
});
cy.findAllByText('Clear All').then((e) => {
const [firstStatusClear] = e;
firstStatusClear.click();
cy.wait(['@Filters', '@Graph', '@Table']);
// insuring the api get call
cy.get(allFilters).should('have.length', 2);
cy.get(allGraphs).should('have.length', 2);
cy.get(allTable).should('have.length', 2);
cy
.window()
.its('store')
.invoke('getState')
.then((e: AppState) => {
const { traces } = e;
expect(traces.isFilterExclude.get('status')).to.be.equals(false);
expect(traces.userSelectedFilter.get('status')).to.be.undefined;
expect(traces.selectedFilter.size).to.be.equals(0);
});
});
});
it('Un Selecting one option from status', () => {
cy.wait(['@Filters', '@Graph', '@Table']);
cy.get('input[type="checkbox"]').then((e) => {
const [errorCheckbox] = e;
errorCheckbox.click();
cy.wait(['@Filters', '@Graph', '@Table']).then((e) => {
const [filter, graph, table] = e;
const filterBody = filter.request.body;
const graphBody = graph.request.body;
const tableBody = table.request.body;
expect(filterBody.exclude).not.to.be.undefined;
expect(filterBody.exclude.length).not.to.be.equal(0);
expect(filterBody.exclude[0] === 'status').to.be.true;
expect(graphBody.exclude).not.to.be.undefined;
expect(graphBody.exclude.length).not.to.be.equal(0);
expect(graphBody.exclude[0] === 'status').to.be.true;
expect(tableBody.exclude).not.to.be.undefined;
expect(tableBody.exclude.length).not.to.be.equal(0);
expect(tableBody.exclude[0] === 'status').to.be.true;
});
cy.get(allFilters).should('have.length', 2);
cy.get(allGraphs).should('have.length', 2);
cy.get(allTable).should('have.length', 2);
});
});
});

View File

@ -1,26 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
// cypress/plugins/index.ts
/// <reference types="cypress" />
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (): void => {
return undefined;
};
export {};

View File

@ -1,24 +0,0 @@
import '@testing-library/cypress/add-commands';
import CheckRouteDefaultGlobalTimeOptions, {
CheckRouteDefaultGlobalTimeOptionsProps,
} from '../CustomFunctions/checkRouteDefaultGlobalTimeOptions';
import Login, { LoginProps } from '../CustomFunctions/Login';
Cypress.Commands.add('login', Login);
Cypress.Commands.add(
'checkDefaultGlobalOption',
CheckRouteDefaultGlobalTimeOptions,
);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
login(props: LoginProps): void;
checkDefaultGlobalOption(
props: CheckRouteDefaultGlobalTimeOptionsProps,
): void;
}
}
}

View File

@ -1,20 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -1,13 +0,0 @@
{
"extends": "../tsconfig.json",
"target": "es5",
"lib": ["es5", "dom"],
"compilerOptions": {
"noEmit": true,
// be explicit about types included
// to avoid clashing with Jest types
"types": ["cypress", "@testing-library/cypress", "node"],
"isolatedModules": false
},
"include": ["../node_modules/cypress", "./**/*.ts"]
}

View File

@ -25,6 +25,11 @@ const config: Config.InitialOptions = {
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
moduleDirectories: ['node_modules', 'src'],
testEnvironmentOptions: {
'jest-playwright': {
browsers: ['chromium', 'firefox', 'webkit'],
},
},
};
export default config;

View File

@ -9,16 +9,17 @@
"prettify": "prettier --write .",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn husky:configure",
"husky:configure": "cd .. && husky install frontend/.husky"
"postinstall": "is-ci || yarn husky:configure",
"playwright": "playwright test --config=./playwright.config.ts",
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1"
},
"engines": {
"node": ">=12.13.0"
"node": ">=16.15.0"
},
"author": "",
"license": "ISC",
@ -46,7 +47,6 @@
"cross-env": "^7.0.3",
"css-loader": "4.3.0",
"css-minimizer-webpack-plugin": "^3.2.0",
"cypress": "^8.3.0",
"d3": "^6.2.0",
"d3-flame-graph": "^3.1.1",
"d3-tip": "^0.9.1",
@ -68,7 +68,7 @@
"react-dom": "17.0.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.2.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.1",
"react-query": "^3.34.19",
"react-redux": "^7.2.2",
@ -109,15 +109,17 @@
"@babel/preset-env": "^7.12.17",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.12.17",
"@commitlint/cli": "^16.2.4",
"@commitlint/config-conventional": "^16.2.4",
"@jest/globals": "^27.5.1",
"@testing-library/cypress": "^8.0.0",
"@playwright/test": "^1.22.0",
"@testing-library/react-hooks": "^7.0.2",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
"@types/d3": "^6.2.0",
"@types/d3-tip": "^3.5.5",
"@types/jest": "^26.0.15",
"@types/jest": "^27.5.1",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
@ -136,7 +138,7 @@
"@typescript-eslint/parser": "^4.28.2",
"autoprefixer": "^9.0.0",
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "^9.0.0",
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^8.1.0",
"critters-webpack-plugin": "^3.0.1",
"eslint": "^7.30.0",
@ -155,6 +157,8 @@
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
"is-ci": "^3.0.1",
"jest-playwright-preset": "^1.7.0",
"less-plugin-npm-import": "^2.1.0",
"lint-staged": "^12.3.7",
"portfinder-sync": "^0.0.2",
@ -164,11 +168,15 @@
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "^3.4.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.5.0"
"webpack-cli": "^4.9.2"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"eslint --fix"
]
},
"resolutions": {
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0"
}
}

View File

@ -0,0 +1,21 @@
import { PlaywrightTestConfig } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config();
const config: PlaywrightTestConfig = {
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
preserveOutput: 'always',
name: 'Signoz',
testDir: './tests',
use: {
trace: 'retain-on-failure',
baseURL: process.env.FRONTEND_API_ENDPOINT,
},
updateSnapshots: 'all',
fullyParallel: false,
quiet: true,
};
export default config;

View File

@ -0,0 +1,21 @@
{
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"status_message": {
"success": "Your last call to change retention period to {{total_retention}} {{s3_part}} was successful.",
"failed": "Your last call to change retention period to {{total_retention}} {{s3_part}} failed. Please try again.",
"pending": "Your last call to change retention period to {{total_retention}} {{s3_part}} is pending. This may take some time.",
"s3_part": "and S3 to {{s3_retention}}"
},
"retention_save_button": {
"pending": "Updating {{name}} retention period",
"success": "Save"
},
"retention_request_race_condition": "Your request to change retention period has failed, as another request is still in process.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving {{name}}."
}

View File

@ -2,5 +2,8 @@
"general": "General",
"alert_channels": "Alert Channels",
"organization_settings": "Organization Settings",
"my_settings": "My Settings"
"my_settings": "My Settings",
"overview_metrics": "Overview Metrics",
"dbcall_metrics": "Database Calls",
"external_metrics": "External Calls"
}

View File

@ -13,16 +13,5 @@
"general": "General",
"alert_channels": "Alert Channels",
"all_errors": "All Exceptions"
},
"settings": {
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"retention_success_message": "Congrats. The retention periods for {{name}} has been updated successfully.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving metrics & traces."
}
}

View File

@ -0,0 +1,21 @@
{
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"status_message": {
"success": "Your last call to change retention period to {{total_retention}} {{s3_part}} was successful.",
"failed": "Your last call to change retention period to {{total_retention}} {{s3_part}} failed. Please try again.",
"pending": "Your last call to change retention period to {{total_retention}} {{s3_part}} is pending. This may take some time.",
"s3_part": "and S3 to {{s3_retention}}"
},
"retention_save_button": {
"pending": "Updating {{name}} retention period",
"success": "Save"
},
"retention_request_race_condition": "Your request to change retention period has failed, as another request is still in process.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving {{name}}."
}

View File

@ -2,5 +2,8 @@
"general": "General",
"alert_channels": "Alert Channels",
"organization_settings": "Organization Settings",
"my_settings": "My Settings"
"my_settings": "My Settings",
"overview_metrics": "Overview Metrics",
"dbcall_metrics": "Database Calls",
"external_metrics": "External Calls"
}

View File

@ -13,16 +13,5 @@
"general": "General",
"alert_channels": "Alert Channels",
"all_errors": "All Exceptions"
},
"settings": {
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"retention_success_message": "Congrats. The retention periods for {{name}} has been updated successfully.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving metrics & traces."
}
}

View File

@ -2,6 +2,7 @@
import { notification } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import loginApi from 'api/user/login';
import { Logout } from 'api/utils';
import Spinner from 'components/Spinner';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
@ -103,7 +104,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
history.push(ROUTES.UN_AUTHORIZED);
}
} else {
history.push(ROUTES.SOMETHING_WENT_WRONG);
Logout();
notification.error({
message: response.error || t('something_went_wrong'),

View File

@ -12,10 +12,7 @@ export const ServiceMetricsPage = Loadable(
);
export const ServiceMapPage = Loadable(
() =>
import(
/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap/ServiceMap'
),
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
);
export const TraceFilter = Loadable(
@ -27,10 +24,7 @@ export const TraceDetail = Loadable(
);
export const UsageExplorerPage = Loadable(
() =>
import(
/* webpackChunkName: "UsageExplorerPage" */ 'modules/Usage/UsageExplorerDef'
),
() => import(/* webpackChunkName: "UsageExplorerPage" */ 'modules/Usage'),
);
export const SignupPage = Loadable(

View File

@ -2,13 +2,15 @@ import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/settings/getRetention';
import { PayloadProps, Props } from 'types/api/settings/getRetention';
const getRetention = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
const getRetention = async <T extends Props>(
props: T,
): Promise<SuccessResponse<PayloadProps<T>> | ErrorResponse> => {
try {
const response = await axios.get<PayloadProps>(`/settings/ttl`);
const response = await axios.get<PayloadProps<T>>(
`/settings/ttl?type=${props}`,
);
return {
statusCode: 200,

View File

@ -11,7 +11,7 @@ const setRetention = async (
const response = await axios.post<PayloadProps>(
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
props.coldStorage
? `&coldStorage=${props.coldStorage};toColdDuration=${props.toColdDuration}`
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
: ''
}`,
);

View File

@ -5,6 +5,7 @@ import history from 'lib/history';
import store from 'store';
import {
LOGGED_IN,
UPDATE_ORG,
UPDATE_USER,
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
UPDATE_USER_ORG_ROLE,
@ -51,5 +52,12 @@ export const Logout = (): void => {
},
});
store.dispatch({
type: UPDATE_ORG,
payload: {
org: [],
},
});
history.push(ROUTES.LOGIN);
};

View File

@ -1,4 +1,3 @@
import { expect } from '@jest/globals';
import dayjs from 'dayjs';
import { convertTimeRange, TIME_UNITS } from '../xAxisConfig';

View File

@ -2,10 +2,11 @@
* @jest-environment jsdom
*/
import { expect } from '@jest/globals';
import { render } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
import NotFound from './index';
@ -13,7 +14,9 @@ describe('Not Found page test', () => {
it('should render Not Found page without errors', () => {
const { asFragment } = render(
<MemoryRouter>
<NotFound />
<Provider store={store}>
<NotFound />
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();

View File

@ -27,12 +27,19 @@ function RouteTab({
onChange={onChange}
destroyInactiveTabPane
activeKey={activeKey}
animated
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
{routes.map(
({ Component, name }): JSX.Element => (
<TabPane tab={name} key={name}>
({ Component, name, route }): JSX.Element => (
<TabPane
tabKey={route}
animated
destroyInactiveTabPane
tab={name}
key={name}
>
<Component />
</TabPane>
),

View File

@ -18,7 +18,7 @@ const ROUTES = {
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/setting/channels/new',
CHANNELS_EDIT: '/setting/channels/edit/:id',
ALL_ERROR: '/errors',
ALL_ERROR: '/exceptions',
ERROR_DETAIL: '/error-detail',
VERSION: '/status',
MY_SETTINGS: '/my-settings',

View File

@ -1,4 +1,4 @@
import { notification, Table, Typography } from 'antd';
import { notification, Table, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import getAll from 'api/errors/getAll';
import ROUTES from 'constants/routes';
@ -47,11 +47,13 @@ function AllErrors(): JSX.Element {
dataIndex: 'exceptionType',
key: 'exceptionType',
render: (value, record): JSX.Element => (
<Link
to={`${ROUTES.ERROR_DETAIL}?serviceName=${record.serviceName}&errorType=${record.exceptionType}`}
>
{value}
</Link>
<Tooltip overlay={(): JSX.Element => value}>
<Link
to={`${ROUTES.ERROR_DETAIL}?serviceName=${record.serviceName}&errorType=${record.exceptionType}`}
>
{value}
</Link>
</Tooltip>
),
sorter: (a, b): number =>
a.exceptionType.charCodeAt(0) - b.exceptionType.charCodeAt(0),
@ -61,13 +63,15 @@ function AllErrors(): JSX.Element {
dataIndex: 'exceptionMessage',
key: 'exceptionMessage',
render: (value): JSX.Element => (
<Typography.Paragraph
ellipsis={{
rows: 2,
}}
>
{value}
</Typography.Paragraph>
<Tooltip overlay={(): JSX.Element => value}>
<Typography.Paragraph
ellipsis={{
rows: 2,
}}
>
{value}
</Typography.Paragraph>
</Tooltip>
),
},
{

View File

@ -117,7 +117,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
dispatch({
type: UPDATE_LATEST_VERSION,
payload: {
latestVersion: getUserLatestVersionResponse.data.payload.name,
latestVersion: getUserLatestVersionResponse.data.payload.tag_name,
},
});
}

View File

@ -1,35 +1,67 @@
import { Button, Col, Modal, notification, Row, Typography } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import {
Button,
Col,
Divider,
Modal,
notification,
Row,
Spin,
Typography,
} from 'antd';
import setRetentionApi from 'api/settings/setRetention';
import TextToolTip from 'components/TextToolTip';
import useComponentPermission from 'hooks/useComponentPermission';
import find from 'lodash-es/find';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { useInterval } from 'react-use';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IDiskType,
PayloadProps as GetDisksPayload,
} from 'types/api/disks/getDisks';
import { PayloadProps as GetRetentionPayload } from 'types/api/settings/getRetention';
import { TTTLType } from 'types/api/settings/common';
import {
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
} from 'types/api/settings/getRetention';
import AppReducer from 'types/reducer/app';
import Retention from './Retention';
import { ButtonContainer, ErrorText, ErrorTextContainer } from './styles';
import StatusMessage from './StatusMessage';
import { ActionItemsContainer, ErrorText, ErrorTextContainer } from './styles';
type NumberOrNull = number | null;
function GeneralSettings({
ttlValuesPayload,
metricsTtlValuesPayload,
tracesTtlValuesPayload,
getAvailableDiskPayload,
metricsTtlValuesRefetch,
tracesTtlValuesRefetch,
}: GeneralSettingsProps): JSX.Element {
const { t } = useTranslation();
const [modal, setModal] = useState<boolean>(false);
const [postApiLoading, setPostApiLoading] = useState<boolean>(false);
const { t } = useTranslation(['generalSettings']);
const [modalMetrics, setModalMetrics] = useState<boolean>(false);
const [postApiLoadingMetrics, setPostApiLoadingMetrics] = useState<boolean>(
false,
);
const [postApiLoadingTraces, setPostApiLoadingTraces] = useState<boolean>(
false,
);
const [modalTraces, setModalTraces] = useState<boolean>(false);
const [availableDisks] = useState<IDiskType[]>(getAvailableDiskPayload);
const [currentTTLValues, setCurrentTTLValues] = useState(ttlValuesPayload);
const [metricsCurrentTTLValues, setMetricsCurrentTTLValues] = useState(
metricsTtlValuesPayload,
);
const [tracesCurrentTTLValues, setTracesCurrentTTLValues] = useState(
tracesTtlValuesPayload,
);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [setRetentionPermission] = useComponentPermission(
@ -55,195 +87,93 @@ function GeneralSettings({
] = useState<NumberOrNull>(null);
useEffect(() => {
if (currentTTLValues) {
setMetricsTotalRetentionPeriod(currentTTLValues.metrics_ttl_duration_hrs);
setMetricsS3RetentionPeriod(
currentTTLValues.metrics_move_ttl_duration_hrs
? currentTTLValues.metrics_move_ttl_duration_hrs
: null,
if (metricsCurrentTTLValues) {
setMetricsTotalRetentionPeriod(
metricsCurrentTTLValues.metrics_ttl_duration_hrs,
);
setTracesTotalRetentionPeriod(currentTTLValues.traces_ttl_duration_hrs);
setTracesS3RetentionPeriod(
currentTTLValues.traces_move_ttl_duration_hrs
? currentTTLValues.traces_move_ttl_duration_hrs
setMetricsS3RetentionPeriod(
metricsCurrentTTLValues.metrics_move_ttl_duration_hrs
? metricsCurrentTTLValues.metrics_move_ttl_duration_hrs
: null,
);
}
}, [currentTTLValues]);
}, [metricsCurrentTTLValues]);
const onModalToggleHandler = (): void => {
setModal((modal) => !modal);
useEffect(() => {
if (tracesCurrentTTLValues) {
setTracesTotalRetentionPeriod(
tracesCurrentTTLValues.traces_ttl_duration_hrs,
);
setTracesS3RetentionPeriod(
tracesCurrentTTLValues.traces_move_ttl_duration_hrs
? tracesCurrentTTLValues.traces_move_ttl_duration_hrs
: null,
);
}
}, [tracesCurrentTTLValues]);
useInterval(
async (): Promise<void> => {
if (metricsTtlValuesPayload.status === 'pending') {
metricsTtlValuesRefetch();
}
},
metricsTtlValuesPayload.status === 'pending' ? 1000 : null,
);
useInterval(
async (): Promise<void> => {
if (tracesTtlValuesPayload.status === 'pending') {
tracesTtlValuesRefetch();
}
},
tracesTtlValuesPayload.status === 'pending' ? 1000 : null,
);
const onModalToggleHandler = (type: TTTLType): void => {
if (type === 'metrics') setModalMetrics((modal) => !modal);
if (type === 'traces') setModalTraces((modal) => !modal);
};
const onPostApiLoadingHandler = (type: TTTLType): void => {
if (type === 'metrics') setPostApiLoadingMetrics((modal) => !modal);
if (type === 'traces') setPostApiLoadingTraces((modal) => !modal);
};
const onClickSaveHandler = useCallback(() => {
if (!setRetentionPermission) {
notification.error({
message: `Sorry you don't have permission to make these changes`,
});
return;
}
onModalToggleHandler();
}, [setRetentionPermission]);
const onClickSaveHandler = useCallback(
(type: TTTLType) => {
if (!setRetentionPermission) {
notification.error({
message: `Sorry you don't have permission to make these changes`,
});
return;
}
onModalToggleHandler(type);
},
[setRetentionPermission],
);
const s3Enabled = useMemo(
() => !!find(availableDisks, (disks: IDiskType) => disks?.type === 's3'),
[availableDisks],
);
const renderConfig = [
{
name: 'Metrics',
retentionFields: [
{
name: t('settings.total_retention_period'),
value: metricsTotalRetentionPeriod,
setValue: setMetricsTotalRetentionPeriod,
},
{
name: t('settings.move_to_s3'),
value: metricsS3RetentionPeriod,
setValue: setMetricsS3RetentionPeriod,
hide: !s3Enabled,
},
],
},
{
name: 'Traces',
retentionFields: [
{
name: t('settings.total_retention_period'),
value: tracesTotalRetentionPeriod,
setValue: setTracesTotalRetentionPeriod,
},
{
name: t('settings.move_to_s3'),
value: tracesS3RetentionPeriod,
setValue: setTracesS3RetentionPeriod,
hide: !s3Enabled,
},
],
},
].map((category): JSX.Element | null => {
if (
Array.isArray(category.retentionFields) &&
category.retentionFields.length > 0
) {
return (
<Col flex="40%" style={{ minWidth: 475 }} key={category.name}>
<Typography.Title level={3}>{category.name}</Typography.Title>
{category.retentionFields.map((retentionField) => (
<Retention
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
setRetentionValue={retentionField.setValue}
hide={!!retentionField.hide}
/>
))}
</Col>
);
}
return null;
});
// eslint-disable-next-line sonarjs/cognitive-complexity
const onOkHandler = async (): Promise<void> => {
try {
setPostApiLoading(true);
const apiCalls = [];
if (
!(
currentTTLValues?.metrics_move_ttl_duration_hrs ===
metricsS3RetentionPeriod &&
currentTTLValues.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod
)
) {
apiCalls.push(() =>
setRetentionApi({
type: 'metrics',
totalDuration: `${metricsTotalRetentionPeriod || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${metricsS3RetentionPeriod || -1}h`,
}),
);
} else {
apiCalls.push(() => Promise.resolve(null));
}
if (
!(
currentTTLValues?.traces_move_ttl_duration_hrs ===
tracesS3RetentionPeriod &&
currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod
)
) {
apiCalls.push(() =>
setRetentionApi({
type: 'traces',
totalDuration: `${tracesTotalRetentionPeriod || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${tracesS3RetentionPeriod || -1}h`,
}),
);
} else {
apiCalls.push(() => Promise.resolve(null));
}
const apiCallSequence = ['metrics', 'traces'];
const apiResponses = await Promise.all(apiCalls.map((api) => api()));
apiResponses.forEach((apiResponse, idx) => {
const name = apiCallSequence[idx];
if (apiResponse) {
if (apiResponse.statusCode === 200) {
notification.success({
message: 'Success!',
placement: 'topRight',
description: t('settings.retention_success_message', { name }),
});
} else {
notification.error({
message: 'Error',
description: t('settings.retention_error_message', { name }),
placement: 'topRight',
});
}
}
});
onModalToggleHandler();
setPostApiLoading(false);
} catch (error) {
notification.error({
message: 'Error',
description: t('settings.retention_failed_message'),
placement: 'topRight',
});
}
// Updates the currentTTL Values in order to avoid pushing the same values.
setCurrentTTLValues({
metrics_ttl_duration_hrs: metricsTotalRetentionPeriod || -1,
metrics_move_ttl_duration_hrs: metricsS3RetentionPeriod || -1,
traces_ttl_duration_hrs: tracesTotalRetentionPeriod || -1,
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
});
setModal(false);
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const [isDisabled, errorText] = useMemo((): [boolean, string] => {
const [isMetricsSaveDisabled, isTracesSaveDisabled, errorText] = useMemo((): [
boolean,
boolean,
string,
// eslint-disable-next-line sonarjs/cognitive-complexity
] => {
// Various methods to return dynamic error message text.
const messages = {
compareError: (name: string | number): string =>
t('settings.retention_comparison_error', { name }),
t('retention_comparison_error', { name }),
nullValueError: (name: string | number): string =>
t('settings.retention_null_value_error', { name }),
t('retention_null_value_error', { name }),
};
// Defaults to button not disabled and empty error message text.
let isDisabled = false;
let isMetricsSaveDisabled = false;
let isTracesSaveDisabled = false;
let errorText = '';
if (s3Enabled) {
@ -251,19 +181,20 @@ function GeneralSettings({
(metricsTotalRetentionPeriod || metricsS3RetentionPeriod) &&
Number(metricsTotalRetentionPeriod) <= Number(metricsS3RetentionPeriod)
) {
isDisabled = true;
isMetricsSaveDisabled = true;
errorText = messages.compareError('metrics');
} else if (
(tracesTotalRetentionPeriod || tracesS3RetentionPeriod) &&
Number(tracesTotalRetentionPeriod) <= Number(tracesS3RetentionPeriod)
) {
isDisabled = true;
isTracesSaveDisabled = true;
errorText = messages.compareError('traces');
}
}
if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) {
isDisabled = true;
isMetricsSaveDisabled = true;
isTracesSaveDisabled = true;
if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) {
errorText = messages.nullValueError('metrics and traces');
} else if (!metricsTotalRetentionPeriod) {
@ -273,25 +204,240 @@ function GeneralSettings({
}
}
if (
currentTTLValues?.metrics_ttl_duration_hrs === metricsTotalRetentionPeriod &&
currentTTLValues.metrics_move_ttl_duration_hrs ===
metricsS3RetentionPeriod &&
currentTTLValues.traces_ttl_duration_hrs === tracesTotalRetentionPeriod &&
currentTTLValues.traces_move_ttl_duration_hrs === tracesS3RetentionPeriod
) {
isDisabled = true;
}
return [isDisabled, errorText];
metricsCurrentTTLValues?.metrics_ttl_duration_hrs ===
metricsTotalRetentionPeriod &&
metricsCurrentTTLValues.metrics_move_ttl_duration_hrs ===
metricsS3RetentionPeriod
)
isMetricsSaveDisabled = true;
if (
tracesCurrentTTLValues.traces_ttl_duration_hrs ===
tracesTotalRetentionPeriod &&
tracesCurrentTTLValues.traces_move_ttl_duration_hrs ===
tracesS3RetentionPeriod
)
isTracesSaveDisabled = true;
return [isMetricsSaveDisabled, isTracesSaveDisabled, errorText];
}, [
currentTTLValues,
metricsCurrentTTLValues.metrics_move_ttl_duration_hrs,
metricsCurrentTTLValues?.metrics_ttl_duration_hrs,
metricsS3RetentionPeriod,
metricsTotalRetentionPeriod,
s3Enabled,
t,
tracesCurrentTTLValues.traces_move_ttl_duration_hrs,
tracesCurrentTTLValues.traces_ttl_duration_hrs,
tracesS3RetentionPeriod,
tracesTotalRetentionPeriod,
]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const onOkHandler = async (type: TTTLType): Promise<void> => {
try {
onPostApiLoadingHandler(type);
const setTTLResponse = await setRetentionApi({
type,
totalDuration: `${
(type === 'metrics'
? metricsTotalRetentionPeriod
: tracesTotalRetentionPeriod) || -1
}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${
(type === 'metrics'
? metricsS3RetentionPeriod
: tracesS3RetentionPeriod) || -1
}h`,
});
let hasSetTTLFailed = false;
if (setTTLResponse.statusCode === 409) {
hasSetTTLFailed = true;
notification.error({
message: 'Error',
description: t('retention_request_race_condition'),
placement: 'topRight',
});
}
if (type === 'metrics') {
metricsTtlValuesRefetch();
if (!hasSetTTLFailed)
// Updates the currentTTL Values in order to avoid pushing the same values.
setMetricsCurrentTTLValues({
metrics_ttl_duration_hrs: metricsTotalRetentionPeriod || -1,
metrics_move_ttl_duration_hrs: metricsS3RetentionPeriod || -1,
status: '',
});
} else if (type === 'traces') {
tracesTtlValuesRefetch();
if (!hasSetTTLFailed)
// Updates the currentTTL Values in order to avoid pushing the same values.
setTracesCurrentTTLValues({
traces_ttl_duration_hrs: tracesTotalRetentionPeriod || -1,
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
status: '',
});
}
} catch (error) {
notification.error({
message: 'Error',
description: t('retention_failed_message'),
placement: 'topRight',
});
}
onPostApiLoadingHandler(type);
onModalToggleHandler(type);
};
const renderConfig = [
{
name: 'Metrics',
retentionFields: [
{
name: t('total_retention_period'),
value: metricsTotalRetentionPeriod,
setValue: setMetricsTotalRetentionPeriod,
},
{
name: t('move_to_s3'),
value: metricsS3RetentionPeriod,
setValue: setMetricsS3RetentionPeriod,
hide: !s3Enabled,
},
],
save: {
modal: modalMetrics,
modalOpen: (): void => onClickSaveHandler('metrics'),
apiLoading: postApiLoadingMetrics,
saveButtonText:
metricsTtlValuesPayload.status === 'pending' ? (
<span>
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
{t('retention_save_button.pending', { name: 'metrics' })}
</span>
) : (
<span>{t('retention_save_button.success')}</span>
),
isDisabled:
metricsTtlValuesPayload.status === 'pending' || isMetricsSaveDisabled,
},
statusComponent: (
<StatusMessage
total_retention={metricsTtlValuesPayload.expected_metrics_ttl_duration_hrs}
status={metricsTtlValuesPayload.status}
s3_retention={
metricsTtlValuesPayload.expected_metrics_move_ttl_duration_hrs
}
/>
),
},
{
name: 'Traces',
retentionFields: [
{
name: t('total_retention_period'),
value: tracesTotalRetentionPeriod,
setValue: setTracesTotalRetentionPeriod,
},
{
name: t('move_to_s3'),
value: tracesS3RetentionPeriod,
setValue: setTracesS3RetentionPeriod,
hide: !s3Enabled,
},
],
save: {
modal: modalTraces,
modalOpen: (): void => onClickSaveHandler('traces'),
apiLoading: postApiLoadingTraces,
saveButtonText:
tracesTtlValuesPayload.status === 'pending' ? (
<span>
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
{t('retention_save_button.pending', { name: 'traces' })}
</span>
) : (
<span>{t('retention_save_button.success')}</span>
),
isDisabled:
tracesTtlValuesPayload.status === 'pending' || isTracesSaveDisabled,
},
statusComponent: (
<StatusMessage
total_retention={tracesTtlValuesPayload.expected_traces_ttl_duration_hrs}
status={tracesTtlValuesPayload.status}
s3_retention={tracesTtlValuesPayload.expected_traces_move_ttl_duration_hrs}
/>
),
},
].map((category, idx, renderArr): JSX.Element | null => {
if (
Array.isArray(category.retentionFields) &&
category.retentionFields.length > 0
) {
return (
<React.Fragment key={category.name}>
<Col xs={22} xl={11} key={category.name}>
<Typography.Title level={3}>{category.name}</Typography.Title>
{category.retentionFields.map((retentionField) => (
<Retention
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
setRetentionValue={retentionField.setValue}
hide={!!retentionField.hide}
/>
))}
<ActionItemsContainer>
<Button
type="primary"
onClick={category.save.modalOpen}
disabled={category.save.isDisabled}
>
{category.save.saveButtonText}
</Button>
{category.statusComponent}
</ActionItemsContainer>
<Modal
title={t('retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={(): void =>
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
}
onOk={(): Promise<void> =>
onOkHandler(category.name.toLowerCase() as TTTLType)
}
centered
visible={category.save.modal}
confirmLoading={category.save.apiLoading}
>
<Typography>
{t('retention_confirmation_description', {
name: category.name.toLowerCase(),
})}
</Typography>
</Modal>
</Col>
{idx < renderArr.length && (
<Col xs={0} xl={1} style={{ textAlign: 'center' }}>
<Divider type="vertical" dashed style={{ height: '100%' }} />
</Col>
)}
</React.Fragment>
);
}
return null;
});
return (
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
{Element}
@ -306,34 +452,20 @@ function GeneralSettings({
</ErrorTextContainer>
<Row justify="space-around">{renderConfig}</Row>
<Modal
title={t('settings.retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={onModalToggleHandler}
onOk={onOkHandler}
centered
visible={modal}
confirmLoading={postApiLoading}
>
<Typography>{t('settings.retention_confirmation_description')}</Typography>
</Modal>
<ButtonContainer>
<Button onClick={onClickSaveHandler} disabled={isDisabled} type="primary">
Save
</Button>
</ButtonContainer>
</Col>
);
}
interface GeneralSettingsProps {
ttlValuesPayload: GetRetentionPayload;
getAvailableDiskPayload: GetDisksPayload;
metricsTtlValuesPayload: GetRetentionPeriodMetricsPayload;
tracesTtlValuesPayload: GetRetentionPeriodTracesPayload;
metricsTtlValuesRefetch: UseQueryResult<
ErrorResponse | SuccessResponse<GetRetentionPeriodMetricsPayload>
>['refetch'];
tracesTtlValuesRefetch: UseQueryResult<
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
>['refetch'];
}
export default GeneralSettings;

View File

@ -0,0 +1,69 @@
import { green, orange, volcano } from '@ant-design/colors';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Card, Col, Row } from 'antd';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { TStatus } from 'types/api/settings/getRetention';
import { convertHoursValueToRelevantUnitString } from './utils';
function StatusMessage({
total_retention,
s3_retention,
status,
}: StatusMessageProps): JSX.Element | null {
const { t } = useTranslation(['generalSettings']);
const messageColor = useMemo((): string => {
if (status === 'success') return green[6];
if (status === 'pending') return orange[6];
if (status === 'failed') return volcano[6];
return 'inherit';
}, [status]);
if (!status) {
return null;
}
const s3Part =
s3_retention && s3_retention !== -1
? t('status_message.s3_part', {
s3_retention: convertHoursValueToRelevantUnitString(s3_retention),
})
: '';
const statusMessage =
total_retention && total_retention !== -1
? t(`status_message.${status}`, {
total_retention: convertHoursValueToRelevantUnitString(total_retention),
s3_part: s3Part,
})
: null;
return statusMessage ? (
<Card
style={{
width: '100%',
}}
>
<Row style={{ gap: '1rem', alignItems: 'center', justifyContent: 'center' }}>
<Col xs={1}>
<InfoCircleOutlined style={{ fontSize: '1.5rem' }} />
</Col>
<Col
xs={22}
style={{
color: messageColor,
}}
>
{statusMessage}
</Col>
</Row>
</Card>
) : null;
}
interface StatusMessageProps {
status: TStatus;
total_retention: number | undefined;
s3_retention: number | undefined;
}
export default StatusMessage;

View File

@ -5,15 +5,33 @@ import Spinner from 'components/Spinner';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TTTLType } from 'types/api/settings/common';
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
import GeneralSettingsContainer from './GeneralSettings';
type TRetentionAPIReturn<T extends TTTLType> = Promise<
SuccessResponse<GetRetentionPeriodAPIPayloadProps<T>> | ErrorResponse
>;
function GeneralSettings(): JSX.Element {
const { t } = useTranslation('common');
const [getRetentionPeriodApiResponse, getDisksResponse] = useQueries([
const [
getRetentionPeriodMetricsApiResponse,
getRetentionPeriodTracesApiResponse,
getDisksResponse,
] = useQueries([
{
queryFn: getRetentionPeriodApi,
queryKey: 'getRetentionPeriodApi',
queryFn: (): TRetentionAPIReturn<'metrics'> =>
getRetentionPeriodApi('metrics'),
queryKey: 'getRetentionPeriodApiMetrics',
},
{
queryFn: (): TRetentionAPIReturn<'traces'> =>
getRetentionPeriodApi('traces'),
queryKey: 'getRetentionPeriodApiTraces',
},
{
queryFn: getDisks,
@ -21,21 +39,38 @@ function GeneralSettings(): JSX.Element {
},
]);
if (getRetentionPeriodApiResponse.isError || getDisksResponse.isError) {
// Error State - When RetentionPeriodMetricsApi or getDiskApi gets errored out.
if (getRetentionPeriodMetricsApiResponse.isError || getDisksResponse.isError) {
return (
<Typography>
{getRetentionPeriodApiResponse.data?.error ||
{getRetentionPeriodMetricsApiResponse.data?.error ||
getDisksResponse.data?.error ||
t('something_went_wrong')}
</Typography>
);
}
// Error State - When RetentionPeriodTracesApi or getDiskApi gets errored out.
if (getRetentionPeriodTracesApiResponse.isError || getDisksResponse.isError) {
return (
<Typography>
{getRetentionPeriodTracesApiResponse.data?.error ||
getDisksResponse.data?.error ||
t('something_went_wrong')}
</Typography>
);
}
// Loading State - When Metrics, Traces and Disk API are in progress and the promise has not been resolved/reject.
if (
getRetentionPeriodApiResponse.isLoading ||
getRetentionPeriodMetricsApiResponse.isLoading ||
getDisksResponse.isLoading ||
!getDisksResponse.data?.payload ||
!getRetentionPeriodApiResponse.data?.payload
!getRetentionPeriodMetricsApiResponse.data?.payload ||
getRetentionPeriodTracesApiResponse.isLoading ||
getDisksResponse.isLoading ||
!getDisksResponse.data?.payload ||
!getRetentionPeriodTracesApiResponse.data?.payload
) {
return <Spinner tip="Loading.." height="70vh" />;
}
@ -44,7 +79,10 @@ function GeneralSettings(): JSX.Element {
<GeneralSettingsContainer
{...{
getAvailableDiskPayload: getDisksResponse.data?.payload,
ttlValuesPayload: getRetentionPeriodApiResponse.data?.payload,
metricsTtlValuesPayload: getRetentionPeriodMetricsApiResponse.data?.payload,
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
}}
/>
);

View File

@ -90,3 +90,11 @@ export const RetentionFieldLabel = styled(TypographyComponent)`
export const RetentionFieldInputContainer = styled.div`
display: inline-flex;
`;
export const ActionItemsContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-top: 10%;
`;

View File

@ -23,9 +23,13 @@ export const TimeUnits: ITimeUnit[] = [
},
];
interface ITimeUnitConversion {
value: number;
timeUnitValue: SettingPeriod;
}
export const convertHoursValueToRelevantUnit = (
value: number,
): { value: number; timeUnitValue: SettingPeriod } => {
): ITimeUnitConversion => {
if (value)
for (let idx = TimeUnits.length - 1; idx >= 0; idx -= 1) {
const timeUnit = TimeUnits[idx];
@ -40,3 +44,13 @@ export const convertHoursValueToRelevantUnit = (
}
return { value, timeUnitValue: TimeUnits[0].value };
};
export const convertHoursValueToRelevantUnitString = (
value: number,
): string => {
if (!value) return '';
const convertedTimeUnit = convertHoursValueToRelevantUnit(value);
return `${convertedTimeUnit.value} ${convertedTimeUnit.timeUnitValue}${
convertedTimeUnit.value >= 2 ? 's' : ''
}`;
};

View File

@ -1,53 +0,0 @@
import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import React, { useCallback } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
ToggleAddWidget,
ToggleAddWidgetProps,
} from 'store/actions/dashboard/toggleAddWidget';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import DashboardReducer from 'types/reducer/dashboards';
import { Button, Container } from './styles';
function AddWidget({ toggleAddWidget }: Props): JSX.Element {
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const onToggleHandler = useCallback(() => {
toggleAddWidget(true);
}, [toggleAddWidget]);
return (
<Container>
{!isAddWidget ? (
<Button onClick={onToggleHandler} icon={<PlusOutlined />}>
Add Widgets
</Button>
) : (
<Typography>Click a widget icon to add it here</Typography>
)}
</Container>
);
}
interface DispatchProps {
toggleAddWidget: (
props: ToggleAddWidgetProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch),
});
type Props = DispatchProps;
export default connect(null, mapDispatchToProps)(AddWidget);

View File

@ -1,18 +0,0 @@
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
export const Button = styled(ButtonComponent)`
&&& {
display: flex;
justify-content: center;
align-items: center;
border: none;
}
`;
export const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`;

View File

@ -0,0 +1,16 @@
import { Typography } from 'antd';
import React from 'react';
import { Container } from './styles';
function EmptyWidget(): JSX.Element {
return (
<Container>
<Typography.Paragraph>
Click one of the widget types above (Time Series / Value) to add here
</Typography.Paragraph>
</Container>
);
}
export default EmptyWidget;

View File

@ -0,0 +1,8 @@
import styled from 'styled-components';
export const Container = styled.div`
height: 100%;
display: flex;
justify-content: center;
align-items: center;
`;

View File

@ -1,7 +1,5 @@
import { Button, Typography } from 'antd';
import getQueryResult from 'api/widgets/getQuery';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import { GraphOnClickHandler } from 'components/Graph';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
@ -17,7 +15,8 @@ import GetMaxMinTime from 'lib/getMaxMinTime';
import GetMinMax from 'lib/getMinMax';
import getStartAndEndTime from 'lib/getStartAndEndTime';
import getStep from 'lib/getStep';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
@ -37,13 +36,6 @@ function FullView({
GlobalReducer
>((state) => state.globalTime);
const [state, setState] = useState<FullViewState>({
error: false,
errorMessage: '',
loading: true,
payload: undefined,
});
const getSelectedTime = useCallback(
() =>
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
@ -55,107 +47,82 @@ function FullView({
enum: widget?.timePreferance || 'GLOBAL_TIME',
});
const onFetchDataHandler = useCallback(async () => {
try {
const maxMinTime = GetMaxMinTime({
graphType: widget.panelTypes,
maxTime,
minTime,
});
const maxMinTime = GetMaxMinTime({
graphType: widget.panelTypes,
maxTime,
minTime,
});
const getMinMax = (
time: timePreferenceType,
): { min: string | number; max: string | number } => {
if (time === 'GLOBAL_TIME') {
const minMax = GetMinMax(globalSelectedTime);
return {
min: convertToNanoSecondsToSecond(minMax.minTime / 1000),
max: convertToNanoSecondsToSecond(minMax.maxTime / 1000),
};
}
const minMax = getStartAndEndTime({
type: selectedTime.enum,
maxTime: maxMinTime.maxTime,
minTime: maxMinTime.minTime,
});
return { min: parseInt(minMax.start, 10), max: parseInt(minMax.end, 10) };
const getMinMax = (
time: timePreferenceType,
): { min: string | number; max: string | number } => {
if (time === 'GLOBAL_TIME') {
const minMax = GetMinMax(globalSelectedTime);
return {
min: convertToNanoSecondsToSecond(minMax.minTime / 1000),
max: convertToNanoSecondsToSecond(minMax.maxTime / 1000),
};
const queryMinMax = getMinMax(selectedTime.enum);
const response = await Promise.all(
widget.query
.filter((e) => e.query.length !== 0)
.map(async (query) => {
const result = await getQueryResult({
end: queryMinMax.max.toString(),
query: query.query,
start: queryMinMax.min.toString(),
step: `${getStep({
start: queryMinMax.min,
end: queryMinMax.max,
inputFormat: 's',
})}`,
});
return {
query: query.query,
queryData: result,
legend: query.legend,
};
}),
);
const isError = response.find((e) => e.queryData.statusCode !== 200);
if (isError !== undefined) {
setState((state) => ({
...state,
error: true,
errorMessage: isError.queryData.error || 'Something went wrong',
loading: false,
}));
} else {
const chartDataSet = getChartData({
queryData: response.map((e) => ({
query: e.query,
legend: e.legend,
queryData: e.queryData.payload?.result || [],
})),
});
setState((state) => ({
...state,
loading: false,
payload: chartDataSet,
}));
}
} catch (error) {
setState((state) => ({
...state,
error: true,
errorMessage: (error as AxiosError).toString(),
loading: false,
}));
}
}, [widget, maxTime, minTime, selectedTime.enum, globalSelectedTime]);
useEffect(() => {
onFetchDataHandler();
}, [onFetchDataHandler]);
const minMax = getStartAndEndTime({
type: selectedTime.enum,
maxTime: maxMinTime.maxTime,
minTime: maxMinTime.minTime,
});
return { min: parseInt(minMax.start, 10), max: parseInt(minMax.end, 10) };
};
if (state.error && !state.loading) {
return (
<NotFoundContainer>
<Typography>{state.errorMessage}</Typography>
</NotFoundContainer>
);
const queryMinMax = getMinMax(selectedTime.enum);
const queryLength = widget.query.filter((e) => e.query.length !== 0);
const response = useQueries(
queryLength.map((query) => {
return {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
queryFn: () => {
return getQueryResult({
end: queryMinMax.max.toString(),
query: query.query,
start: queryMinMax.min.toString(),
step: `${getStep({
start: queryMinMax.min,
end: queryMinMax.max,
inputFormat: 's',
})}`,
});
},
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
retryOnMount: false,
};
}),
);
const isError =
response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
response.some((e) => e.isError === true);
const isLoading = response.some((e) => e.isLoading === true);
const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
const data = response.map((responseOfQuery) =>
responseOfQuery?.data?.payload?.result.map((e, index) => ({
query: queryLength[index]?.query,
queryData: e,
legend: queryLength[index]?.legend,
})),
);
if (isLoading) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
if (state.loading || state.payload === undefined) {
if (isError || data === undefined || data[0] === undefined) {
return (
<div>
<Spinner height="80vh" size="large" tip="Loading..." />
</div>
<NotFoundContainer>
<Typography>{errorMessage}</Typography>
</NotFoundContainer>
);
}
@ -169,17 +136,27 @@ function FullView({
setSelectedTime,
}}
/>
<Button onClick={onFetchDataHandler} type="primary">
<Button
onClick={(): void => {
response.forEach((e) => e.refetch());
}}
type="primary"
>
Refresh
</Button>
</TimeContainer>
)}
{/* <GraphContainer> */}
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
data: getChartData({
queryData: data.map((e) => ({
query: e?.map((e) => e.query).join(' ') || '',
queryData: e?.map((e) => e.queryData) || [],
legend: e?.map((e) => e.legend).join('') || '',
})),
}),
isStacked: widget.isStacked,
opacity: widget.opacity,
title: widget.title,
@ -188,18 +165,10 @@ function FullView({
yAxisUnit,
}}
/>
{/* </GraphContainer> */}
</>
);
}
interface FullViewState {
loading: boolean;
error: boolean;
errorMessage: string;
payload: ChartData | undefined;
}
interface FullViewProps {
widget: Widgets;
fullViewOptions?: boolean;

View File

@ -1,13 +1,14 @@
import { Typography } from 'antd';
import getQueryResult from 'api/widgets/getQuery';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import getChartData from 'lib/getChartData';
import GetMaxMinTime from 'lib/getMaxMinTime';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import React, { useCallback, useEffect, useState } from 'react';
import isEmpty from 'lodash-es/isEmpty';
import React, { memo, useCallback, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useQueries } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@ -20,6 +21,8 @@ import AppActions from 'types/actions';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll';
import { LayoutProps } from '..';
import EmptyWidget from '../EmptyWidget';
import WidgetHeader from '../WidgetHeader';
import FullView from './FullView';
import { ErrorContainer, FullViewContainer, Modal } from './styles';
@ -27,91 +30,65 @@ import { ErrorContainer, FullViewContainer, Modal } from './styles';
function GridCardGraph({
widget,
deleteWidget,
isDeleted,
name,
yAxisUnit,
layout = [],
setLayout,
}: GridCardGraphProps): JSX.Element {
const [state, setState] = useState<GridCardGraphState>({
loading: true,
errorMessage: '',
error: false,
payload: undefined,
});
const [hovered, setHovered] = useState(false);
const [modal, setModal] = useState(false);
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
(state) => state.globalTime,
);
const [deleteModal, setDeletModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
useEffect(() => {
(async (): Promise<void> => {
try {
const getMaxMinTime = GetMaxMinTime({
graphType: widget?.panelTypes,
maxTime,
minTime,
});
const getMaxMinTime = GetMaxMinTime({
graphType: widget?.panelTypes,
maxTime,
minTime,
});
const { start, end } = GetStartAndEndTime({
type: widget.timePreferance,
maxTime: getMaxMinTime.maxTime,
minTime: getMaxMinTime.minTime,
});
const { start, end } = GetStartAndEndTime({
type: widget?.timePreferance,
maxTime: getMaxMinTime.maxTime,
minTime: getMaxMinTime.minTime,
});
const response = await Promise.all(
widget.query
.filter((e) => e.query.length !== 0)
.map(async (query) => {
const result = await getQueryResult({
end,
query: query.query,
start,
step: '60',
});
const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
return {
query: query.query,
queryData: result,
legend: query.legend,
};
}),
);
const isError = response.find((e) => e.queryData.statusCode !== 200);
if (isError !== undefined) {
setState((state) => ({
...state,
error: true,
errorMessage: isError.queryData.error || 'Something went wrong',
loading: false,
}));
} else {
const chartDataSet = getChartData({
queryData: response.map((e) => ({
query: e.query,
legend: e.legend,
queryData: e.queryData.payload?.result || [],
})),
const response = useQueries(
queryLength?.map((query) => {
return {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
queryFn: () => {
return getQueryResult({
end,
query: query?.query,
start,
step: '60',
});
},
queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
retryOnMount: false,
};
}),
);
setState((state) => ({
...state,
loading: false,
payload: chartDataSet,
}));
}
} catch (error) {
setState((state) => ({
...state,
error: true,
errorMessage: (error as AxiosError).toString(),
loading: false,
}));
}
})();
}, [widget, maxTime, minTime]);
const isError =
response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
response.some((e) => e.isError === true);
const isLoading = response.some((e) => e.isLoading === true);
const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
const data = response.map((responseOfQuery) =>
responseOfQuery?.data?.payload?.result.map((e, index) => ({
query: queryLength[index]?.query,
queryData: e,
legend: queryLength[index]?.legend,
})),
);
const onToggleModal = useCallback(
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
@ -121,18 +98,20 @@ function GridCardGraph({
);
const onDeleteHandler = useCallback(() => {
deleteWidget({ widgetId: widget.id });
onToggleModal(setDeletModal);
// eslint-disable-next-line no-param-reassign
isDeleted.current = true;
}, [deleteWidget, widget, onToggleModal, isDeleted]);
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
deleteWidget({ widgetId, setLayout });
onToggleModal(setDeleteModal);
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
const getModals = (): JSX.Element => {
return (
<>
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeletModal)}
onCancel={(): void => onToggleModal(setDeleteModal)}
visible={deleteModal}
title="Delete"
height="10vh"
@ -163,7 +142,16 @@ function GridCardGraph({
);
};
if (state.error) {
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
if (isLoading) {
return <Spinner height="20vh" tip="Loading..." />;
}
if (
(isError || data === undefined || data[0] === undefined) &&
!isEmptyLayout
) {
return (
<>
{getModals()}
@ -172,17 +160,21 @@ function GridCardGraph({
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeletModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
<ErrorContainer>{state.errorMessage}</ErrorContainer>
<ErrorContainer>{errorMessage}</ErrorContainer>
</>
);
}
if (state.loading === true || state.payload === undefined) {
return <Spinner height="20vh" tip="Loading..." />;
}
const chartData = getChartData({
queryData: data.map((e) => ({
query: e?.map((e) => e.query).join(' ') || '',
queryData: e?.map((e) => e.queryData) || [],
legend: e?.map((e) => e.legend).join('') || '',
})),
});
return (
<span
@ -199,38 +191,37 @@ function GridCardGraph({
setHovered(false);
}}
>
<WidgetHeader
parentHover={hovered}
title={widget.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeletModal)}
/>
{!isEmptyLayout && (
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
)}
{getModals()}
{!isEmptyLayout && getModals()}
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
name,
yAxisUnit,
}}
/>
{!isEmpty(widget) && (
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: chartData,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
name,
yAxisUnit,
}}
/>
)}
{isEmptyLayout && <EmptyWidget />}
</span>
);
}
interface GridCardGraphState {
loading: boolean;
error: boolean;
errorMessage: string;
payload: ChartData | undefined;
}
interface DispatchProps {
deleteWidget: ({
widgetId,
@ -239,9 +230,12 @@ interface DispatchProps {
interface GridCardGraphProps extends DispatchProps {
widget: Widgets;
isDeleted: React.MutableRefObject<boolean>;
name: string;
yAxisUnit: string | undefined;
// eslint-disable-next-line react/require-default-props
layout?: Layout[];
// eslint-disable-next-line react/require-default-props
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
}
const mapDispatchToProps = (
@ -250,4 +244,4 @@ const mapDispatchToProps = (
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
});
export default connect(null, mapDispatchToProps)(GridCardGraph);
export default connect(null, mapDispatchToProps)(memo(GridCardGraph));

View File

@ -0,0 +1,106 @@
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import useComponentPermission from 'hooks/useComponentPermission';
import React 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 { 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 { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
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}
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}
>
{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>
<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: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
}
export default GraphLayout;

View File

@ -104,7 +104,7 @@ function WidgetHeader({
overlay={menu}
trigger={['click']}
overlayStyle={{ minWidth: 100 }}
placement="bottomCenter"
placement="bottom"
>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}

View File

@ -1,277 +1,297 @@
/* eslint-disable react/no-unstable-nested-components */
import { SaveFilled } from '@ant-design/icons';
import { notification } from 'antd';
import updateDashboardApi from 'api/dashboard/update';
import Spinner from 'components/Spinner';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import useComponentPermission from 'hooks/useComponentPermission';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
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 { v4 } from 'uuid';
import AddWidget from './AddWidget';
import Graph from './Graph';
import {
Button,
ButtonContainer,
Card,
CardContainer,
ReactGridLayout,
} from './styles';
import { updateDashboard } from './utils';
import GraphLayoutContainer from './GraphLayout';
import { UpdateDashboard } from './utils';
function GridGraph(): JSX.Element {
const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
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}
/>
);
},
}));
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 [layouts, setLayout] = useState<LayoutProps[]>([]);
const dispatch = useDispatch<Dispatch<AppActions>>();
const AddWidgetWrapper = useCallback(() => <AddWidget />, []);
const isMounted = useRef(true);
const isDeleted = useRef(false);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [saveLayout] = useComponentPermission(['save_layout'], role);
const getPreLayouts: () => LayoutProps[] = useCallback(() => {
if (widgets === undefined) {
return [];
}
// when the layout is not present
if (data.layout === undefined) {
return widgets.map((e, index) => {
return {
h: 2,
w: 6,
y: Infinity,
i: (index + 1).toString(),
x: (index % 2) * 6,
Component: (): JSX.Element => (
<Graph
name={`${e.id + index}non-expanded`}
isDeleted={isDeleted}
widget={widgets[index]}
yAxisUnit={e.yAxisUnit}
/>
),
};
});
}
return data.layout
.filter((_, index) => widgets[index])
.map((e, index) => ({
...e,
Component: (): JSX.Element => {
if (widgets[index]) {
return (
<Graph
name={e.i + index}
isDeleted={isDeleted}
widget={widgets[index]}
yAxisUnit={widgets[index].yAxisUnit}
/>
);
}
return <div />;
},
}));
}, [widgets, data.layout]);
const [layouts, setLayout] = useState<LayoutProps[]>(
getPreLayouts(widgets, selectedDashboard.data.layout || []),
);
useEffect(() => {
if (
loading === false &&
(isMounted.current === true || isDeleted.current === true)
) {
const preLayouts = getPreLayouts();
setLayout(() => {
const getX = (): number => {
if (preLayouts && preLayouts?.length > 0) {
const last = preLayouts[(preLayouts?.length || 0) - 1];
(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);
return (last.w + last.x) % 12;
}
return 0;
};
const updatedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
layout: updatedLayout,
widgets: updatedWidget,
},
};
return [
...preLayouts,
{
i: (preLayouts.length + 1).toString(),
x: getX(),
y: Infinity,
w: 6,
h: 2,
Component: AddWidgetWrapper,
maxW: 6,
isDraggable: false,
isResizable: false,
isBounded: true,
},
];
});
}
return (): void => {
isMounted.current = false;
};
}, [widgets, layouts.length, AddWidgetWrapper, loading, getPreLayouts]);
const onDropHandler = useCallback(
async (allLayouts: Layout[], currentLayout: Layout, event: DragEvent) => {
event.preventDefault();
if (event.dataTransfer) {
try {
const graphType = event.dataTransfer.getData('text') as GRAPH_TYPES;
const generateWidgetId = v4();
await updateDashboard({
data,
generateWidgetId,
graphType,
selectedDashboard,
layout: allLayouts
.map((e, index) => ({
...e,
i: index.toString(),
// when a new element drops
w: e.i === '__dropping-elem__' ? 6 : e.w,
h: e.i === '__dropping-elem__' ? 2 : e.h,
}))
// removing add widgets layout config
.filter((e) => e.maxW === undefined),
await updateDashboardApi({
data: updatedDashboard.data,
uuid: updatedDashboard.uuid,
});
} catch (error) {
notification.error({
message:
error instanceof Error ? error.toString() : 'Something went wrong',
dispatch({
type: UPDATE_DASHBOARD,
payload: updatedDashboard,
});
}
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onLayoutSaveHandler = useCallback(
async (layout: Layout[]) => {
try {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
// Save layout only when users has the has the permission to do so.
if (saveLayoutPermission) {
const response = await updateDashboardApi({
data: {
title: data.title,
description: data.description,
name: data.name,
tags: data.tags,
widgets: data.widgets,
layout,
},
uuid: selectedDashboard.uuid,
});
if (response.statusCode === 200) {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: false,
}));
} else {
setSaveLayoutState((state) => ({
...state,
error: true,
errorMessage: response.error || 'Something went wrong',
loading: false,
}));
}
}
} catch (error) {
console.error(error);
}
},
[data, selectedDashboard],
[
data.description,
data.name,
data.tags,
data.title,
data.widgets,
saveLayoutPermission,
selectedDashboard.uuid,
],
);
const onLayoutSaveHandler = async (): Promise<void> => {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
const setLayoutFunction = useCallback(
(layout: Layout[]) => {
setLayout(
layout.map((e) => {
const currentWidget =
widgets?.find((widget) => widget.id === e.i) || ({} as Widgets);
const response = await updateDashboardApi({
data: {
title: data.title,
description: data.description,
name: data.name,
tags: data.tags,
widgets: data.widgets,
layout: saveLayoutState.payload.filter((e) => e.maxW === undefined),
},
uuid: selectedDashboard.uuid,
});
if (response.statusCode === 200) {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: false,
}));
} else {
setSaveLayoutState((state) => ({
...state,
error: true,
errorMessage: response.error || 'Something went wrong',
loading: false,
}));
return {
...e,
Component: (): JSX.Element => (
<Graph
name={currentWidget.id}
widget={currentWidget}
yAxisUnit={currentWidget?.yAxisUnit}
layout={layout}
setLayout={setLayout}
/>
),
};
}),
);
},
[widgets],
);
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: 'EMPTY_WIDGET',
selectedDashboard,
layout,
isRedirected: false,
});
setLayoutFunction(layout);
} catch (error) {
notification.error({
message: error instanceof Error ? error.toString() : 'Something went wrong',
});
}
}, [data, selectedDashboard, setLayoutFunction]);
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
setLayoutFunction(layout);
await onLayoutSaveHandler(layout);
};
const onLayoutChangeHandler = (layout: Layout[]): void => {
setSaveLayoutState({
loading: false,
error: false,
errorMessage: '',
payload: layout,
});
};
const onAddPanelHandler = useCallback(() => {
try {
setAddPanelLoading(true);
const isEmptyLayoutPresent =
layouts.find((e) => e.i === 'empty') !== undefined;
if (layouts.length === 0) {
return <Spinner height="40vh" size="large" tip="Loading..." />;
}
if (!isEmptyLayoutPresent) {
onEmptyWidgetHandler()
.then(() => {
setAddPanelLoading(false);
toggleAddWidget(true);
})
.catch(() => {
notification.error(t('something_went_wrong'));
});
} else {
toggleAddWidget(true);
setAddPanelLoading(false);
}
} catch (error) {
if (typeof error === 'string') {
notification.error({
message: error || t('something_went_wrong'),
});
}
}
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget]);
return (
<>
{saveLayout && (
<ButtonContainer>
<Button
loading={saveLayoutState.loading}
onClick={onLayoutSaveHandler}
icon={<SaveFilled />}
danger={saveLayoutState.error}
>
Save Layout
</Button>
</ButtonContainer>
)}
<ReactGridLayout
isResizable
isDraggable
cols={12}
rowHeight={100}
autoSize
width={100}
isDroppable
useCSSTransforms
onDrop={onDropHandler}
onLayoutChange={onLayoutChangeHandler}
>
{layouts.map(({ Component, ...rest }, index) => {
const widget = (widgets || [])[index] || {};
const type = widget?.panelTypes || 'TIME_SERIES';
const isQueryType = type === 'VALUE';
return (
<CardContainer key={rest.i} data-grid={rest}>
<Card isQueryType={isQueryType}>
<Component />
</Card>
</CardContainer>
);
})}
</ReactGridLayout>
</>
<GraphLayoutContainer
{...{
addPanelLoading,
layouts,
onAddPanelHandler,
onLayoutChangeHandler,
onLayoutSaveHandler,
saveLayoutState,
widgets,
setLayout,
}}
/>
);
}
interface LayoutProps extends Layout {
Component: () => JSX.Element;
interface ComponentProps {
setLayout: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
}
interface State {
export interface LayoutProps extends Layout {
Component: (props: ComponentProps) => JSX.Element;
}
export interface State {
loading: boolean;
error: boolean;
payload: Layout[];
errorMessage: string;
}
export default memo(GridGraph);
interface DispatchProps {
toggleAddWidget: (
props: ToggleAddWidgetProps,
) => (dispatch: Dispatch<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,13 +1,11 @@
import { Button as ButtonComponent, Card as CardComponent } from 'antd';
import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import RGL, { WidthProvider } from 'react-grid-layout';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
const ReactGridLayoutComponent = WidthProvider(RGL);
interface Props {
isQueryType: boolean;
}
export const Card = styled(CardComponent)<Props>`
export const Card = styled(CardComponent)`
&&& {
height: 100%;
}
@ -18,20 +16,34 @@ export const Card = styled(CardComponent)<Props>`
}
`;
export const CardContainer = styled.div`
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=');
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: se-resize;
interface Props {
isDarkMode: boolean;
}
export const CardContainer = styled.div<Props>`
:hover {
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
background-position: bottom right;
padding: 0 3px 3px 0;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
cursor: se-resize;
${({ isDarkMode }): StyledCSS => {
const uri = `data:image/svg+xml,%3Csvg viewBox='0 0 6 6' style='background-color:%23ffffff00' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' xml:space='preserve' x='0px' y='0px' width='6px' height='6px'%0A%3E%3Cg opacity='0.302'%3E%3Cpath d='M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z' fill='${
isDarkMode ? 'white' : 'grey'
}'/%3E%3C/g%3E%3C/svg%3E`;
return css`
background-image: ${(): string => `url("${uri}")`};
`;
}}
}
`;
@ -39,9 +51,22 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)`
border: 1px solid #434343;
margin-top: 1rem;
position: relative;
min-height: 40vh;
.react-grid-item.react-grid-placeholder {
background: grey;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
`;
export const ButtonContainer = styled.div`
export const ButtonContainer = styled(Space)`
display: flex;
justify-content: end;
margin-top: 1rem;

View File

@ -1,18 +1,20 @@
import { notification } from 'antd';
import updateDashboardApi from 'api/dashboard/update';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import history from 'lib/history';
import { Layout } from 'react-grid-layout';
import store from 'store';
import { Dashboard } from 'types/api/dashboard/getAll';
export const updateDashboard = async ({
export const UpdateDashboard = async ({
data,
graphType,
generateWidgetId,
layout,
selectedDashboard,
}: UpdateDashboardProps): Promise<void> => {
const response = await updateDashboardApi({
isRedirected,
}: UpdateDashboardProps): Promise<Dashboard | undefined> => {
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
title: data.title,
description: data.description,
@ -46,17 +48,27 @@ export const updateDashboard = async ({
layout,
},
uuid: selectedDashboard.uuid,
});
};
if (response.statusCode === 200) {
history.push(
`${history.location.pathname}/new?graphType=${graphType}&widgetId=${generateWidgetId}`,
);
} else {
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;
}
notification.error({
message: response.error || 'Something went wrong',
});
return undefined;
}
return undefined;
};
interface UpdateDashboardProps {
@ -65,4 +77,5 @@ interface UpdateDashboardProps {
generateWidgetId: string;
layout: Layout[];
selectedDashboard: Dashboard;
isRedirected: boolean;
}

View File

@ -27,12 +27,7 @@ import AppReducer from 'types/reducer/app';
import CurrentOrganization from './CurrentOrganization';
import SignedInAS from './SignedInAs';
import {
Container,
LogoutContainer,
MenuContainer,
ToggleButton,
} from './styles';
import { Container, LogoutContainer, ToggleButton } from './styles';
function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
const { isDarkMode, user, currentVersion } = useSelector<AppState, AppReducer>(
@ -70,7 +65,7 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
};
const menu = (
<MenuContainer>
<Menu style={{ padding: '1rem' }}>
<Menu.ItemGroup>
<SignedInAS />
<Divider />
@ -92,7 +87,7 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
</div>
</LogoutContainer>
</Menu.ItemGroup>
</MenuContainer>
</Menu>
);
return (

View File

@ -1,4 +1,4 @@
import { Menu, Switch, Typography } from 'antd';
import { Switch, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
@ -48,10 +48,6 @@ export const LogoutContainer = styled.div`
align-items: center;
`;
export const MenuContainer = styled(Menu)`
padding: 1rem;
`;
export interface DarkModeProps {
checked?: boolean;
defaultChecked?: boolean;

View File

@ -1,7 +0,0 @@
import React from 'react';
function IsRouteAccessible(): JSX.Element {
return <div>asd</div>;
}
export default IsRouteAccessible;

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