Compare commits

..

61 Commits

Author SHA1 Message Date
Vishal Sharma
0cbaa17d9f chore: allow unlimited dashboards and alerts in community version (#4989)
* chore: allow unlimited dashboards and alerts in community version

* chore: update ee plan
2024-05-14 18:05:59 +05:30
Nityananda Gohain
30bfad527f chore: enable limits for trace queries (#4997) 2024-05-14 17:03:29 +05:30
Srikanth Chekuri
9f1c45bc32 chore: add toUnixTimestamp to supported functions (#4877) 2024-05-14 10:34:43 +05:30
Vikrant Gupta
51becf7cfb fix: added right padding to the notifications bar to show cancel button (#4969) 2024-05-12 16:45:16 +05:30
Vibhu Pandey
7460e650af feat(workflow): integrate with workflow identity pool (#4945)
* feat(workflows): add wif workflow
* feat(workflows): add name of compute instance
* feat(workflows): fix permissions
* feat(workflows):  add an OR true since github runs with -e
* ci(testing-deployment): include GITHUB envs
* ci(testing-deployment): move GCP information to secrets
* ci(staging-deployment): wif workflow

---------

Co-authored-by: Prashant Shahi <prashant@signoz.io>
2024-05-10 23:23:31 +05:30
Vikrant Gupta
211fe4fdd5 fix: prevent page from crashing in case items in filters is null (#4964)
* fix: prevent page from crashing in case items in filters is null

* fix: added null check for filters as well
2024-05-06 19:18:27 +05:30
Vikrant Gupta
e2992b42c1 fix: make integrations available for the ee cloud user (#4963) 2024-05-06 19:17:50 +05:30
Nityananda Gohain
3957d91a9b fix: add read-in-order config (#4918) 2024-05-06 15:01:53 +05:30
Vishal Sharma
967aa16f21 feat: sort tags and events in trace detail (#4962) 2024-05-05 09:03:31 +05:30
Vikrant Gupta
08b1a87cb5 Revert "fix: step interval not getting updated on time range change (#4944)" (#4955)
This reverts commit 5c1c09c790.
2024-05-01 22:46:32 +05:30
SagarRajput-7
03ddcdd20e feat: added test cases for pipeline pages (#4872)
* feat: added test cases for pipeline pages

* feat: added test cases for changeHistory

* chore: change history table test case added

* chore: added create pipeline button test cases

* chore: updated useAnalytics mocking
2024-05-01 18:49:04 +05:30
SagarRajput-7
1aec7f3ca6 feat: added tooltips for facing issue btn (#4948) 2024-05-01 18:36:56 +05:30
Vikrant Gupta
241edcb88a fix: text change for saved views in traces (#4953) 2024-05-01 18:28:05 +05:30
Srikanth Chekuri
27d12871af chore: disallow small step intervals for large durations (#4950) 2024-05-01 17:03:46 +05:30
Yunus M
e78e1d4b63 fix: add safety checks to handle null response from query range API (#4939) 2024-05-01 15:49:30 +05:30
Yunus M
64bf580323 feat: show milliseconds in timestamp in logs views (#4949)
* feat: show milliseconds in timestamp in logs views

* fix: remove console log

---------

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-05-01 15:27:48 +05:30
SagarRajput-7
152aa4b518 fix: fixed facing issue btn alignment issue (#4936)
* fix: fixed facing issue btn alignment issue

* fix: fixed facing issue btn alignment issue

* fix: moved intercom help messages to util file
2024-05-01 14:49:42 +05:30
Vikrant Gupta
b3d5831574 fix: ch queries sending builder as query type in query range api for exceptions alerts (#4941)
* fix: ch queries sending builder as query type in query range api for exceptions alerts

* fix: ch queries sending builder as query type in query range api for exceptions alerts

* fix: alerts routing from logs explorer and dashboards
2024-05-01 14:39:39 +05:30
Vikrant Gupta
b85b9f42ed fix: time interval not getting updated in case of edit dashboard (#4940) 2024-05-01 13:00:18 +05:30
Vikrant Gupta
5c1c09c790 fix: step interval not getting updated on time range change (#4944) 2024-05-01 12:47:33 +05:30
Vishal Sharma
33960b05fd chore: update facing issues text (#4942) 2024-04-30 23:38:15 +05:30
Vikrant Gupta
191d9b0648 feat: introducing collapsable rows for dashboards (#4806)
* feat: dashboard panel grouping initial setup

* feat: added panel map to the dashboard response and subsequent types for the same

* feat: added panel map to the dashboard response and subsequent types for the same

* feat: added settings modal

* feat: handle panel collapse and open changes

* feat: handle creating panel map when dashboard layout changes

* feat: handle creating panel map when dashboard layout changes

* feat: refactor code

* feat: handle multiple collapsable rows

* fix: type issues

* feat: handle row collapse button and scroll

* feat: handle y axis movement for rows

* feat: handle delete row

* feat: handle settings name change

* feat: disable collapse/uncollapse when dashboard loading to avoid async states

* feat: decrease the height of the grouping row

* fix: row height management

* fix: handle empty row case

* feat: remove resize handle from the row

* feat: handle re-arrangement of panels

* feat: increase height of default new widget

* feat: added safety checks
2024-04-30 14:36:47 +05:30
Srikanth Chekuri
7d81bc3417 fix: value panel restriction should be on enabled queries (#4934) 2024-04-30 09:53:03 +05:30
Srikanth Chekuri
506916661d fix: metric limit works with cache (#4935) 2024-04-30 01:25:50 +05:30
Nityananda Gohain
5326f2d23b fix: dont enrich if non empty keys are not same (#4930)
* fix: dont enrich if non empty keys are not same

* fix: update if any of the type and dataType is empty but other is matching
2024-04-29 22:40:40 +05:30
dependabot[bot]
dfaa344dce chore(deps): bump express from 4.18.2 to 4.19.2 in /frontend (#4840)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 21:29:27 +05:30
SagarRajput-7
882b540a0b chore: [SIG-583]: Jest coverage collection config (#4920)
* chore: [SIG-583]: Jest coverage collection config

* fix: added missing attribute
2024-04-27 11:31:37 +05:30
Ankit Nayan
1c4b579c3d fix: frontend/package.json & frontend/yarn.lock to reduce vulnerabilities (#4341)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-6144788

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2024-04-26 15:00:06 +05:30
Yunus M
706f25cc5d fix: frontend/Dockerfile to reduce vulnerabilities (#4913)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE318-EXPAT-6241039
- https://snyk.io/vuln/SNYK-ALPINE318-LIBX11-6042398
- https://snyk.io/vuln/SNYK-ALPINE318-LIBXML2-6245694
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2024-04-26 11:41:53 +05:30
Raj Kamal Singh
e6e0a59f5f Feat: integrations: clickhouse (#4879)
* chore: get built-in clickhouse integration started

* chore: update config pre-requisites for clickhouse integration

* chore: add details of metrics data collected for clickhouse integration

* chore: clickhouse integration: move list of data-collected to its own file

* chore: clickhouse integration: get overview dashboard started

* chore: start with logs collection instructions for clickhouse

* chore: regex parsing for clickhouse text logs

* chore: timestamp parsing for clickhouse logs

* chore: severity parsing for clickhouse logs

* chore: clickhouse logs parsing: move parsed message to body if available

* chore: update pre-reqs for collecting from system.query_log table

* feat: add instructions for collecting from system.query_log table

* feat: add logs attribs collected

* chore: some cleanup of clickhouse overview dashboard

* feat: finish up with clickhouse overview dashboard for clickhouse integration
2024-04-26 09:45:57 +05:30
Prashant Shahi
b2c170c752 Merge pull request #4919 from SigNoz/release/v0.44
post-release: sync release changes in develop
2024-04-25 22:13:44 +05:30
Prashant Shahi
453be9074d Merge branch 'main' into release/v0.44 2024-04-25 18:07:31 +05:30
Prashant Shahi
3272444e13 chore(signoz): 📌 pin versions: SigNoz 0.44.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-25 18:01:10 +05:30
Vishal Sharma
71b3e6d522 fix: rate in table panel (#4916)
* fix: rate in table panel

* test: added test cases for rate operation in table panel
2024-04-25 14:15:33 +05:30
Prashant Shahi
6cf7cc9f4f chore: bump SigNoz/prometheus from v1.10.1 to v1.11.0 (#4912)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-25 13:12:30 +05:30
Prashant Shahi
5ec2f17d09 chore: pin SigNoz OtelCollector 0.88.21 and update ClickHouse dsn (#4909)
* chore: 📌 pin versions: SigNoz OtelCollector 0.88.21
* chore(clickhouse): update dsn as per the new parsing logic

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-24 21:15:07 +05:30
Prashant Shahi
a45fb8ec0c fix(clickhouse): update endpoint of the healthcheck in deployment files (#4908)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-24 19:06:05 +05:30
SagarRajput-7
bd148bbd5a fix: restrict visibility of facing-issue button to only cloud users with intercom setup (#4907)
* fix: restrict visibilty of facing-issue button to only cloud users with intercom setup

* fix: restrict visibilty of facing-issue button to only cloud users with intercom setup

* fix: added a comment

* fix: added chat support feature flag condition

* fix: added a comment

* fix: changed folder structure
2024-04-24 18:56:19 +05:30
SagarRajput-7
1306e99ca8 fix: alert threshold form is resetting to default query option on stage & run (#4876)
* fix: alert threshold form is resetting to default query option on stage & run

* fix: alert threshold - added safety check when the queryOption is deleted
2024-04-24 15:58:59 +05:30
SagarRajput-7
1a8f063b4b feat: [SIG-585]: Added facingIssueBtn at Dashboard list, detail and alert list, detail etc. pages (#4899)
* feat: [SIG-585]: Added facingIssueBtn at Dashboard list, detail and alert list, detail etc. pages

* feat: [SIG-585]: Added facingIssueBtn for dashboard & alert listing

* feat: [SIG-585]: Added facingIssueBtn for dashboard & alert detail and dashboard panel edit

* feat: [SIG-585]: Code cleanup

* feat: [SIG-585]: Changed logEvent attribute and event content

* feat: [SIG-585]: Changed alignment of button and button text

* feat: [SIG-585]: Changed button color to amber

* feat: [SIG-585]: Code cleanup
2024-04-24 15:48:48 +05:30
SagarRajput-7
c7668b9a78 chore: added jest coverage setup (#4871)
* chore: added jest coverage setup

* chore: added sample test changes

* chore: revert sample code change

* chore: changed script to add yarn install handle and removed checkouts to develop

* chore: added fix for checkout issue

* chore: added fix for checkout issue

* chore: added fix for checkout issue

* chore: added fix for checkout issue

* chore: added sample test changes

* chore: revert sample test cases

* chore: adding coverage threshold

* chore: added coverage threshold and sample test code

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: code cleanup and threshold adjustment

* chore: testing fetch and checkout
2024-04-23 20:06:02 +05:30
Vikrant Gupta
5e3dba2587 fix: do not save dashboard panel on creating a new panel if discard is pressed (#4884)
* fix: do not save dashboard panel on creating a new panel if discard is pressed

* fix: remove console log
2024-04-23 19:39:41 +05:30
Vikrant Gupta
374f30e0cd fix: billing container scroll issue when trial banner present (#4893) 2024-04-22 19:31:54 +05:30
Vikrant Gupta
38d2833931 fix: handle the old variables by name instead of id (#4890) 2024-04-20 17:54:29 +05:30
SagarRajput-7
731eacbbca feat: [SIG-584]: moved facing issue btn tracking from trackEvent to logEvent (#4888)
* feat: [SIG-584]: moved facing issue btn tracking from trackEvent to logEvent

* feat: [SIG-584]: removed logEvent from useAnalytic hook
2024-04-20 16:40:47 +05:30
dependabot[bot]
a63bb139bf chore(deps): bump golang.org/x/net from 0.21.0 to 0.23.0 (#4889)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-20 07:52:44 +05:30
Vikrant Gupta
a140bef0e6 fix: handle the case where the functions are recieved as undefined in the query response (#4880) 2024-04-18 11:17:28 +05:30
Vikrant Gupta
48e5436167 fix: handle the edge cases for alerts create form (#4875) 2024-04-17 16:40:21 +05:30
Yunus M
0fc664a387 feat: show timestamp in list view of trace explorer (#4860)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-04-16 12:41:13 +05:30
Prashant Shahi
5817d50652 Merge pull request #4853 from SigNoz/release/v0.43.x
Release/v0.43.x
2024-04-15 20:07:57 +05:45
Prashant Shahi
bb318cf52a Merge pull request #4852 from SigNoz/release/v0.43.x
chore(pre-release): 📌 pin versions: SigNoz 0.43.0, OtelCollector 0.88.20
2024-04-15 19:50:20 +05:45
Prashant Shahi
ec0185da61 Merge branch 'develop' into release/v0.43.x 2024-04-15 18:24:21 +05:45
Srikanth Chekuri
fc2bdb610f chore: make send resolved notifs configurable (#4833) 2024-04-15 13:46:12 +05:30
Srikanth Chekuri
a9464de62d chore: use last 1day data for apdex latency metric meta (#4846) 2024-04-15 13:37:08 +05:30
Yunus M
57bfdedfe1 feat: send event if users click in facing issues button in get started (#4859)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-04-15 13:26:20 +05:30
SagarRajput-7
7bdc9c0cb0 fix: fixed sidenav alignment with and without get-started (#4829) 2024-04-15 11:40:40 +05:30
SagarRajput-7
0d5934d56b chore: added test cases for Logs (#4828)
* chore: add test cases for Logs

* chore: add test cases for Logs - explorer

* chore: add test cases for Logs - toolbarAction

* chore: add test cases for Logs - list and table view

* chore: add test cases for Logs - list and table view

* chore: code fix
2024-04-15 11:30:25 +05:30
Vikrant Gupta
3a5a61aff9 fix: wrong payload being sent in the dashboard payload (#4854)
* fix: wrong payload being sent in the dashboard payload

* fix: sync the update set dashboard function

* fix: syncronise the var updates

* fix: jest test cases

* fix: added review comments

* fix: do not make query range API call until the queue is empty

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-04-15 11:11:14 +05:30
Rajat Dabade
a54b7baa7d feat: add support for pie chart panel type (#4751) 2024-04-13 09:55:02 +05:30
Prashant Shahi
cd63dd972d chore(pre-release): 📌 pin versions: SigNoz 0.43.0, SigNoz OtelCollector 0.88.20
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-10 23:55:39 +05:45
Nityananda Gohain
389058b9b4 feat: allow query restrictions for log queries (#4778)
* feat: allow query restrictions for log queries

* fix: error check

* fix: set default only if not present

* chore: add error log for query restriction error

* fix: add limtations for traces

* fix: fix wrapper

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-04-10 17:25:57 +05:30
164 changed files with 21821 additions and 2223 deletions

View File

@@ -0,0 +1,31 @@
name: Jest Coverage - changed files
on:
pull_request:
branches: develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: "refs/heads/develop"
token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication
- name: Fetch branch
run: git fetch origin ${{ github.event.pull_request.head.ref }}
- run: |
git checkout ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: cd frontend && npm install -g yarn && yarn
- name: npm run test:changedsince
run: cd frontend && npm run i18n:generate-hash && npm run test:changedsince

View File

@@ -9,34 +9,46 @@ jobs:
name: Deploy latest develop branch to staging
runs-on: ubuntu-latest
environment: staging
permissions:
contents: 'read'
id-token: 'write'
steps:
- name: Executing remote ssh commands using ssh key
uses: appleboy/ssh-action@v1.0.3
env:
GITHUB_BRANCH: develop
GITHUB_SHA: ${{ github.sha }}
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
host: ${{ secrets.HOST_DNS }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
envs: GITHUB_BRANCH,GITHUB_SHA
command_timeout: 60m
script: |
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
export OTELCOL_TAG="main"
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
docker system prune --force
docker pull signoz/signoz-otel-collector:main
docker pull signoz/signoz-schema-migrator:main
cd ~/signoz
git status
git add .
git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin
git checkout ${GITHUB_BRANCH}
git pull
make build-ee-query-service-amd64
make build-frontend-amd64
make run-signoz
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: 'sdk'
uses: 'google-github-actions/setup-gcloud@v2'
- name: 'ssh'
shell: bash
env:
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
GITHUB_SHA: ${{ github.sha }}
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
export OTELCOL_TAG="main"
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
docker system prune --force
docker pull signoz/signoz-otel-collector:main
docker pull signoz/signoz-schema-migrator:main
cd ~/signoz
git status
git add .
git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin
git checkout ${GITHUB_BRANCH}
git pull
make build-ee-query-service-amd64
make build-frontend-amd64
make run-signoz
EOF
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@@ -9,35 +9,47 @@ jobs:
runs-on: ubuntu-latest
environment: testing
if: ${{ github.event.label.name == 'testing-deploy' }}
permissions:
contents: 'read'
id-token: 'write'
steps:
- name: Executing remote ssh commands using ssh key
uses: appleboy/ssh-action@v1.0.3
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: 'sdk'
uses: 'google-github-actions/setup-gcloud@v2'
- name: 'ssh'
shell: bash
env:
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
GITHUB_SHA: ${{ github.sha }}
with:
host: ${{ secrets.HOST_DNS }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
envs: GITHUB_BRANCH,GITHUB_SHA
command_timeout: 60m
script: |
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
export DEV_BUILD="1"
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
docker system prune --force
cd ~/signoz
git status
git add .
git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin
git checkout develop
git pull
# This is added to include the scenerio when new commit in PR is force-pushed
git branch -D ${GITHUB_BRANCH}
git checkout --track origin/${GITHUB_BRANCH}
make build-ee-query-service-amd64
make build-frontend-amd64
make run-signoz
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
export DEV_BUILD="1"
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
docker system prune --force
cd ~/signoz
git status
git add .
git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin
git checkout develop
git pull
# This is added to include the scenerio when new commit in PR is force-pushed
git branch -D ${GITHUB_BRANCH}
git checkout --track origin/${GITHUB_BRANCH}
make build-ee-query-service-amd64
make build-frontend-amd64
make run-signoz
EOF
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@@ -22,7 +22,7 @@ x-clickhouse-defaults: &clickhouse-defaults
"wget",
"--spider",
"-q",
"localhost:8123/ping"
"0.0.0.0:8123/ping"
]
interval: 30s
timeout: 5s
@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.42.0
image: signoz/query-service:0.44.0
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.42.0
image: signoz/frontend:0.44.0
deploy:
restart_policy:
condition: on-failure
@@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.88.17
image: signoz/signoz-otel-collector:0.88.21
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.17
image: signoz/signoz-schema-migrator:0.88.21
deploy:
restart_policy:
condition: on-failure

View File

@@ -111,18 +111,18 @@ processors:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
endpoint: tcp://clickhouse:9000/signoz_metrics
# logging: {}
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
extensions:

View File

@@ -22,4 +22,4 @@ rule_files:
scrape_configs: []
remote_read:
- url: tcp://clickhouse:9000/?database=signoz_metrics
- url: tcp://clickhouse:9000/signoz_metrics

View File

@@ -46,7 +46,7 @@ services:
"wget",
"--spider",
"-q",
"localhost:8123/ping"
"0.0.0.0:8123/ping"
]
interval: 30s
timeout: 5s
@@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.17}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.21}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -81,7 +81,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.88.17
image: signoz/signoz-otel-collector:0.88.21
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -21,7 +21,7 @@ x-clickhouse-defaults: &clickhouse-defaults
"wget",
"--spider",
"-q",
"localhost:8123/ping"
"0.0.0.0:8123/ping"
]
interval: 30s
timeout: 5s
@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.42.0}
image: signoz/query-service:${DOCKER_TAG:-0.44.0}
container_name: signoz-query-service
command:
[
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.42.0}
image: signoz/frontend:${DOCKER_TAG:-0.44.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.17}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.21}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -229,7 +229,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.17}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.21}
container_name: signoz-otel-collector
command:
[

View File

@@ -122,21 +122,20 @@ extensions:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
# logging: {}
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
# logging: {}
service:
telemetry:

View File

@@ -22,4 +22,4 @@ rule_files:
scrape_configs: []
remote_read:
- url: tcp://clickhouse:9000/?database=signoz_metrics
- url: tcp://clickhouse:9000/signoz_metrics

View File

@@ -152,7 +152,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
// PAT APIs
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)

View File

@@ -1,236 +0,0 @@
package api
import (
"bytes"
"fmt"
"net/http"
"sync"
"text/template"
"time"
"go.signoz.io/signoz/pkg/query-service/app/metrics"
"go.signoz.io/signoz/pkg/query-service/app/parser"
"go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate"
"go.uber.org/zap"
)
func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.CustomMetricsFunction) {
zap.L().Info("CustomMetricsFunction feature is not enabled in this plan")
ah.APIHandler.QueryRangeMetricsV2(w, r)
return
}
metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r)
if apiErrorObj != nil {
zap.L().Error("Error in parsing metric query params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}
// prometheus instant query needs same timestamp
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.PROM {
metricsQueryRangeParams.Start = metricsQueryRangeParams.End
}
// round up the end to nearest multiple
if metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER {
end := (metricsQueryRangeParams.End) / 1000
step := metricsQueryRangeParams.Step
metricsQueryRangeParams.End = (end / step * step) * 1000
}
type channelResult struct {
Series []*basemodel.Series
TableName string
Err error
Name string
Query string
}
execClickHouseQueries := func(queries map[string]string) ([]*basemodel.Series, []string, error, map[string]string) {
var seriesList []*basemodel.Series
var tableName []string
ch := make(chan channelResult, len(queries))
var wg sync.WaitGroup
for name, query := range queries {
wg.Add(1)
go func(name, query string) {
defer wg.Done()
seriesList, tableName, err := ah.opts.DataConnector.GetMetricResultEE(r.Context(), query)
for _, series := range seriesList {
series.QueryName = name
}
if err != nil {
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query}
return
}
ch <- channelResult{Series: seriesList, TableName: tableName}
}(name, query)
}
wg.Wait()
close(ch)
var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel
for r := range ch {
if r.Err != nil {
errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue
}
seriesList = append(seriesList, r.Series...)
tableName = append(tableName, r.TableName)
}
if len(errs) != 0 {
return nil, nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
}
return seriesList, tableName, nil, nil
}
execPromQueries := func(metricsQueryRangeParams *basemodel.QueryRangeParamsV2) ([]*basemodel.Series, error, map[string]string) {
var seriesList []*basemodel.Series
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
var wg sync.WaitGroup
for name, query := range metricsQueryRangeParams.CompositeMetricQuery.PromQueries {
if query.Disabled {
continue
}
wg.Add(1)
go func(name string, query *basemodel.PromQuery) {
var seriesList []*basemodel.Series
defer wg.Done()
tmpl := template.New("promql-query")
tmpl, tmplErr := tmpl.Parse(query.Query)
if tmplErr != nil {
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
return
}
var queryBuf bytes.Buffer
tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables)
if tmplErr != nil {
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
return
}
query.Query = queryBuf.String()
queryModel := basemodel.QueryRangeParams{
Start: time.UnixMilli(metricsQueryRangeParams.Start),
End: time.UnixMilli(metricsQueryRangeParams.End),
Step: time.Duration(metricsQueryRangeParams.Step * int64(time.Second)),
Query: query.Query,
}
promResult, _, err := ah.opts.DataConnector.GetQueryRangeResult(r.Context(), &queryModel)
if err != nil {
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query}
return
}
matrix, _ := promResult.Matrix()
for _, v := range matrix {
var s basemodel.Series
s.QueryName = name
s.Labels = v.Metric.Copy().Map()
for _, p := range v.Floats {
s.Points = append(s.Points, basemodel.MetricPoint{Timestamp: p.T, Value: p.F})
}
seriesList = append(seriesList, &s)
}
ch <- channelResult{Series: seriesList}
}(name, query)
}
wg.Wait()
close(ch)
var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel
for r := range ch {
if r.Err != nil {
errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue
}
seriesList = append(seriesList, r.Series...)
}
if len(errs) != 0 {
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
}
return seriesList, nil, nil
}
var seriesList []*basemodel.Series
var tableName []string
var err error
var errQuriesByName map[string]string
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
case basemodel.QUERY_BUILDER:
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME)
if runQueries.Err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: runQueries.Err}, nil)
return
}
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(runQueries.Queries)
case basemodel.CLICKHOUSE:
queries := make(map[string]string)
for name, chQuery := range metricsQueryRangeParams.CompositeMetricQuery.ClickHouseQueries {
if chQuery.Disabled {
continue
}
tmpl := template.New("clickhouse-query")
tmpl, err := tmpl.Parse(chQuery.Query)
if err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
return
}
var query bytes.Buffer
// replace go template variables
querytemplate.AssignReservedVars(metricsQueryRangeParams)
err = tmpl.Execute(&query, metricsQueryRangeParams.Variables)
if err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
return
}
queries[name] = query.String()
}
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(queries)
case basemodel.PROM:
seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams)
default:
err = fmt.Errorf("invalid query type")
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, errQuriesByName)
return
}
if err != nil {
apiErrObj := &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
len(seriesList) > 1 &&
(metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER ||
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.CLICKHOUSE) {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: fmt.Errorf("invalid: query resulted in more than one series for value type")}, nil)
return
}
type ResponseFormat struct {
ResultType string `json:"resultType"`
Result []*basemodel.Series `json:"result"`
TableName []string `json:"tableName"`
}
resp := ResponseFormat{ResultType: "matrix", Result: seriesList, TableName: tableName}
ah.Respond(w, resp)
}

View File

@@ -329,7 +329,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
r.Use(loggingMiddleware)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterMetricsRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)

View File

@@ -52,14 +52,14 @@ var BasicPlan = basemodel.FeatureSet{
Name: basemodel.QueryBuilderPanels,
Active: true,
Usage: 0,
UsageLimit: 20,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: basemodel.QueryBuilderAlerts,
Active: true,
Usage: 0,
UsageLimit: 10,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{

View File

@@ -1,4 +1,4 @@
FROM nginx:1.25.2-alpine
FROM nginx:1.26-alpine
# Add Maintainer Info
LABEL maintainer="signoz"

View File

@@ -4,6 +4,7 @@ const config: Config.InitialOptions = {
clearMocks: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'cobertura', 'html', 'json-summary'],
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
modulePathIgnorePatterns: ['dist'],
moduleNameMapper: {
@@ -35,6 +36,14 @@ const config: Config.InitialOptions = {
browsers: ['chromium', 'firefox', 'webkit'],
},
},
coverageThreshold: {
global: {
statements: 80,
branches: 65,
functions: 80,
lines: 80,
},
},
};
export default config;

View File

@@ -21,7 +21,9 @@
"playwright:codegen:local": "playwright codegen http://localhost:3301",
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1"
"commitlint": "commitlint --edit $1",
"test": "jest --coverage",
"test:changedsince": "jest --changedSince=develop --coverage --silent"
},
"engines": {
"node": ">=16.15.0"
@@ -44,11 +46,14 @@
"@sentry/webpack-plugin": "2.16.0",
"@signozhq/design-tokens": "0.0.8",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"axios": "1.6.2",
"axios": "1.6.4",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",

View File

@@ -15,6 +15,7 @@
"button_test_channel": "Test",
"button_return": "Back",
"field_channel_name": "Name",
"field_send_resolved": "Send resolved alerts",
"field_channel_type": "Type",
"field_webhook_url": "Webhook URL",
"field_slack_recipient": "Recipient",

View File

@@ -16,6 +16,7 @@
"new_dashboard_title": "Sample Title",
"layout_saved_successfully": "Layout saved successfully",
"add_panel": "Add Panel",
"add_row": "Add Row",
"save_layout": "Save Layout",
"variable_updated_successfully": "Variable updated successfully",
"error_while_updating_variable": "Error while updating variable",

View File

@@ -15,6 +15,7 @@
"button_test_channel": "Test",
"button_return": "Back",
"field_channel_name": "Name",
"field_send_resolved": "Send resolved alerts",
"field_channel_type": "Type",
"field_webhook_url": "Webhook URL",
"field_slack_recipient": "Recipient",

View File

@@ -16,6 +16,7 @@
"new_dashboard_title": "Sample Title",
"layout_saved_successfully": "Layout saved successfully",
"add_panel": "Add Panel",
"add_row": "Add Row",
"save_layout": "Save Layout",
"full_view": "Full Screen View",
"variable_updated_successfully": "Variable updated successfully",

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
email_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
to: props.to,
html: props.html,
headers: props.headers,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
msteams_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
pagerduty_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
routing_key: props.routing_key,
client: props.client,
client_url: props.client_url,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
slack_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_url: props.api_url,
channel: props.channel,
title: props.title,

View File

@@ -30,7 +30,7 @@ const create = async (
name: props.name,
webhook_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
url: props.api_url,
http_config: httpConfig,
},

View File

@@ -12,7 +12,7 @@ const editEmail = async (
name: props.name,
email_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
to: props.to,
html: props.html,
headers: props.headers,

View File

@@ -12,7 +12,7 @@ const editMsTeams = async (
name: props.name,
msteams_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,

View File

@@ -12,7 +12,7 @@ const editOpsgenie = async (
name: props.name,
opsgenie_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_key: props.api_key,
description: props.description,
priority: props.priority,

View File

@@ -12,7 +12,7 @@ const editPager = async (
name: props.name,
pagerduty_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
routing_key: props.routing_key,
client: props.client,
client_url: props.client_url,

View File

@@ -12,7 +12,7 @@ const editSlack = async (
name: props.name,
slack_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_url: props.api_url,
channel: props.channel,
title: props.title,

View File

@@ -29,7 +29,7 @@ const editWebhook = async (
name: props.name,
webhook_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
url: props.api_url,
http_config: httpConfig,
},

View File

@@ -1,4 +1,4 @@
import axios from 'api';
import { ApiV4Instance } from 'api';
import { AxiosResponse } from 'axios';
import { MetricMetaProps } from 'types/api/metrics/getApDex';
@@ -6,4 +6,6 @@ export const getMetricMeta = (
metricName: string,
servicename: string,
): Promise<AxiosResponse<MetricMetaProps>> =>
axios.get(`/metric_meta?metricName=${metricName}&serviceName=${servicename}`);
ApiV4Instance.get(
`/metric/metric_metadata?metricName=${metricName}&serviceName=${servicename}`,
);

View File

@@ -1,27 +0,0 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricNameProps,
MetricNamesPayloadProps,
} from 'types/api/metrics/getMetricName';
export const getMetricName = async (
props: MetricNameProps,
): Promise<SuccessResponse<MetricNamesPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/list?match=${props || ''}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,6 +1,7 @@
import { ApiV2Instance as axios } from 'api';
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
TagKeyProps,
@@ -8,15 +9,19 @@ import {
TagValueProps,
TagValuesPayloadProps,
} from 'types/api/metrics/getResourceAttributes';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
export const getResourceAttributesTagKeys = async (
props: TagKeyProps,
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/tagKey?metricName=${props.metricName}${
props.match ? `&match=${props.match}` : ''
}`,
`/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator: MetricAggregateOperator.RATE,
searchText: props.match,
dataSource: DataSource.METRICS,
aggregateAttribute: props.metricName,
})}`,
);
return {
@@ -35,7 +40,13 @@ export const getResourceAttributesTagValues = async (
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/tagValue?metricName=${props.metricName}&tagKey=${props.tagKey}`,
`/autocomplete/attribute_values?${createQueryParams({
aggregateOperator: MetricAggregateOperator.RATE,
dataSource: DataSource.METRICS,
aggregateAttribute: props.metricName,
attributeKey: props.tagKey,
searchText: '',
})}`,
);
return {

View File

@@ -157,8 +157,8 @@ function ListLogView({
const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
? dayjs(flattenLogData.timestamp).format()
: dayjs(flattenLogData.timestamp / 1e6).format(),
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
[flattenLogData.timestamp],
);

View File

@@ -90,12 +90,12 @@ function RawLogView({
const text = useMemo(
() =>
typeof data.timestamp === 'string'
? `${dayjs(data.timestamp).format()} | ${attributesText} ${severityText} ${
data.body
}`
: `${dayjs(
data.timestamp / 1e6,
).format()} | ${attributesText} ${severityText} ${data.body}`,
? `${dayjs(data.timestamp).format(
'YYYY-MM-DD HH:mm:ss.SSS',
)} | ${attributesText} ${severityText} ${data.body}`
: `${dayjs(data.timestamp / 1e6).format(
'YYYY-MM-DD HH:mm:ss.SSS',
)} | ${attributesText} ${severityText} ${data.body}`,
[data.timestamp, data.body, severityText, attributesText],
);

View File

@@ -76,8 +76,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? dayjs(field).format()
: dayjs(field / 1e6).format();
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return {
children: (
<div className="table-timestamp">

View File

@@ -1,8 +1,9 @@
/* eslint-disable react/jsx-props-no-spreading */
import './DynamicColumnTable.syles.scss';
import { Button, Dropdown, MenuProps, Switch } from 'antd';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -20,6 +21,7 @@ function DynamicColumnTable({
columns,
dynamicColumns,
onDragColumn,
facingIssueBtn,
...restProps
}: DynamicColumnTableProps): JSX.Element {
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
@@ -83,19 +85,22 @@ function DynamicColumnTable({
return (
<div className="DynamicColumnTable">
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
/>
</Dropdown>
)}
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
/>
</Dropdown>
)}
</Flex>
<ResizeTable
columns={columnsData}

View File

@@ -2,6 +2,7 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
import { TableDataSource } from './contants';
@@ -12,6 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: FacingIssueBtnProps;
}
export type GetVisibleColumnsFunction = (

View File

@@ -0,0 +1,9 @@
.facing-issue-button {
color: var(--bg-amber-500);
border-color: var(--bg-amber-500);
.ant-btn:hover {
color: var(--bg-amber-400) !important;
border-color: var(--bg-amber-300) !important;
}
}

View File

@@ -0,0 +1,62 @@
import './FacingIssueBtn.style.scss';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { defaultTo } from 'lodash-es';
import { HelpCircle } from 'lucide-react';
import { isCloudUser } from 'utils/app';
export interface FacingIssueBtnProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
}
function FacingIssueBtn({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
}: FacingIssueBtnProps): JSX.Element | null {
const handleFacingIssuesClick = (): void => {
logEvent(eventName, attributes);
if (window.Intercom) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
};
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip title={onHoverText} autoAdjustOverflow>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
</div>
) : null;
}
FacingIssueBtn.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
};
export default FacingIssueBtn;

View File

@@ -0,0 +1,57 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AlertDef } from 'types/api/alerts/def';
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
export const chartHelpMessage = (
selectedDashboard: Dashboard | undefined,
graphType: PANEL_TYPES,
): string => `
Hi Team,
I need help in creating this chart. Here are my dashboard details
Name: ${selectedDashboard?.data.title || ''}
Panel type: ${graphType}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardHelpMessage = (
data: DashboardData | undefined,
selectedDashboard: Dashboard | undefined,
): string => `
Hi Team,
I need help with this dashboard. Here are my dashboard details
Name: ${data?.title || ''}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardListMessage = `Hi Team,
I need help with dashboards.
Thanks`;
export const listAlertMessage = `Hi Team,
I need help with managing alerts.
Thanks`;
export const alertHelpMessage = (
alertDef: AlertDef,
ruleId: number,
): string => `
Hi Team,
I need help in configuring this alert. Here are my alert rule details
Name: ${alertDef?.alert || ''}
Alert Type: ${alertDef?.alertType || ''}
State: ${(alertDef as any)?.state || ''}
Alert Id: ${ruleId}
Thanks`;

View File

@@ -29,6 +29,7 @@ export const getComponentForPanelType = (
[PANEL_TYPES.LIST]:
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
[PANEL_TYPES.BAR]: Uplot,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
};

View File

@@ -30,4 +30,5 @@ export enum QueryParams {
integration = 'integration',
pagination = 'pagination',
relativeTime = 'relativeTime',
alertType = 'alertType',
}

View File

@@ -285,9 +285,15 @@ export enum PANEL_TYPES {
LIST = 'list',
TRACE = 'trace',
BAR = 'bar',
PIE = 'pie',
EMPTY_WIDGET = 'EMPTY_WIDGET',
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum PANEL_GROUP_TYPES {
ROW = 'row',
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum ATTRIBUTE_TYPES {
SUM = 'Sum',

View File

@@ -315,7 +315,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
className={cx(
'app-layout',
isDarkMode ? 'darkMode' : 'lightMode',
!collapsed ? 'docked' : '',
!collapsed && !renderFullScreen ? 'docked' : '',
)}
>
{isToDisplayLayout && !renderFullScreen && (

View File

@@ -1,4 +1,5 @@
.billing-container {
margin-bottom: 40px;
padding-top: 36px;
width: 65%;

View File

@@ -53,6 +53,7 @@ function CreateAlertChannels({
EmailChannel
>
>({
send_resolved: true,
text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
@@ -119,7 +120,7 @@ function CreateAlertChannels({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
}),
@@ -158,7 +159,7 @@ function CreateAlertChannels({
let request: WebhookChannel = {
api_url: selectedConfig?.api_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
};
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
@@ -226,7 +227,7 @@ function CreateAlertChannels({
return {
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
routing_key: selectedConfig?.routing_key || '',
client: selectedConfig?.client || '',
client_url: selectedConfig?.client_url || '',
@@ -274,7 +275,7 @@ function CreateAlertChannels({
() => ({
api_key: selectedConfig?.api_key || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
description: selectedConfig?.description || '',
message: selectedConfig?.message || '',
priority: selectedConfig?.priority || '',
@@ -312,7 +313,7 @@ function CreateAlertChannels({
const prepareEmailRequest = useCallback(
() => ({
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
to: selectedConfig?.to || '',
html: selectedConfig?.html || '',
headers: selectedConfig?.headers || {},
@@ -350,7 +351,7 @@ function CreateAlertChannels({
() => ({
webhook_url: selectedConfig?.webhook_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
}),

View File

@@ -1,7 +1,9 @@
import { Form, Row } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import FormAlertRules from 'container/FormAlertRules';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import history from 'lib/history';
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -18,15 +20,25 @@ import SelectAlertType from './SelectAlertType';
function CreateRules(): JSX.Element {
const [initValues, setInitValues] = useState<AlertDef | null>(null);
const [alertType, setAlertType] = useState<AlertTypes>(
AlertTypes.METRICS_BASED_ALERT,
);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const version = queryParams.get('version');
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
const compositeQuery = useGetCompositeQueryParam();
function getAlertTypeFromDataSource(): AlertTypes | null {
if (!compositeQuery) {
return null;
}
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
return ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
}
const [alertType, setAlertType] = useState<AlertTypes>(
(alertTypeFromParams as AlertTypes) || getAlertTypeFromDataSource(),
);
const [formInstance] = Form.useForm();
@@ -48,21 +60,17 @@ function CreateRules(): JSX.Element {
version: version || ENTITY_VERSION_V4,
});
}
queryParams.set(QueryParams.alertType, typ);
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
history.replace(generatedUrl);
};
useEffect(() => {
if (!compositeQuery) {
return;
}
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
const alertType = ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
if (alertType) {
onSelectType(alertType);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compositeQuery]);
}, [alertType]);
if (!initValues) {
return (

View File

@@ -72,7 +72,7 @@ function EditAlertChannels({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,
@@ -115,7 +115,7 @@ function EditAlertChannels({
return {
api_url: selectedConfig?.api_url || '',
name: name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
username,
password,
id,
@@ -284,7 +284,7 @@ function EditAlertChannels({
() => ({
webhook_url: selectedConfig?.webhook_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,

View File

@@ -1,4 +1,4 @@
import { Form, FormInstance, Input, Select, Typography } from 'antd';
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
import { Store } from 'antd/lib/form/interface';
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
import { FeatureKeys } from 'constants/features';
@@ -95,6 +95,22 @@ function FormAlertChannels({
/>
</Form.Item>
<Form.Item
label={t('field_send_resolved')}
labelAlign="left"
name="send_resolved"
>
<Switch
defaultChecked={initialValue?.send_resolved}
onChange={(value): void => {
setSelectedConfig((state) => ({
...state,
send_resolved: value,
}));
}}
/>
</Form.Item>
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
<Select.Option value="slack" key="slack">

View File

@@ -1,45 +1,50 @@
.create-alert-modal {
.ant-modal-content {
background-color: var(--bg-ink-300);
.ant-modal-confirm-title {
color: var(--bg-vanilla-100);
}
.ant-modal-content {
background-color: var(--bg-ink-300);
.ant-modal-confirm-title {
color: var(--bg-vanilla-100);
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-vanilla-100);
}
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-vanilla-100);
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-slate-400);
border: none;
color: var(--bg-vanilla-100);
}
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-slate-400);
border: none;
color: var(--bg-vanilla-100);
}
}
}
}
.lightMode {
.ant-modal-content {
background-color: var(--bg-vanilla-100);
.ant-modal-confirm-title {
color: var(--bg-ink-500);
}
.ant-modal-content {
background-color: var(--bg-vanilla-100);
.ant-modal-confirm-title {
color: var(--bg-ink-500);
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-ink-500);
}
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-ink-500);
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-vanilla-300);
border: none;
color: var(--bg-ink-500);
}
}
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-vanilla-300);
border: none;
color: var(--bg-ink-500);
}
}
}
}
.facing-issue-btn {
margin-top: 20px;
width: 100%;
}

View File

@@ -11,6 +11,8 @@ import {
} from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { alertHelpMessage } from 'components/facingIssueBtn/util';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -138,15 +140,21 @@ function FormAlertRules({
useEffect(() => {
// Set selectedQueryName based on the length of queryOptions
setAlertDef((def) => ({
...def,
condition: {
...def.condition,
selectedQueryName:
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
},
}));
}, [currentQuery?.queryType, queryOptions]);
const selectedQueryName = alertDef?.condition?.selectedQueryName;
if (
!selectedQueryName ||
!queryOptions.some((option) => option.value === selectedQueryName)
) {
setAlertDef((def) => ({
...def,
condition: {
...def.condition,
selectedQueryName:
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
},
}));
}
}, [alertDef, currentQuery?.queryType, queryOptions]);
const onCancelHandler = useCallback(() => {
history.replace(ROUTES.LIST_ALL_ALERT);
@@ -482,6 +490,8 @@ function FormAlertRules({
alertDef?.broadcastToAll ||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
const isRuleCreated = !ruleId || ruleId === 0;
return (
<>
{Element}
@@ -514,6 +524,7 @@ function FormAlertRules({
runQuery={handleRunQuery}
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}
/>
<RuleOptions
@@ -563,6 +574,22 @@ function FormAlertRules({
</StyledLeftContainer>
<Col flex="1 1 300px">
<UserGuide queryType={currentQuery.queryType} />
<FacingIssueBtn
attributes={{
alert: alertDef?.alert,
alertType: alertDef?.alertType,
id: ruleId,
ruleType: alertDef?.ruleType,
state: (alertDef as any)?.state,
panelType,
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
}}
className="facing-issue-btn"
eventName="Alert: Facing Issues in alert"
buttonText="Need help with this alert?"
message={alertHelpMessage(alertDef, ruleId)}
onHoverText="Click here to get help with this alert"
/>
</Col>
</PanelContainer>
</>

View File

@@ -26,5 +26,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
LIST: false,
TRACE: false,
BAR: true,
PIE: false,
EMPTY_WIDGET: false,
};

View File

@@ -59,7 +59,7 @@ function WidgetGraphComponent({
const lineChartRef = useRef<ToggleGraphProps>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
Array(queryResponse.data?.payload?.data?.result?.length || 0).fill(true),
);
const graphRef = useRef<HTMLDivElement>(null);
@@ -135,7 +135,7 @@ function WidgetGraphComponent({
i: uuid,
w: 6,
x: 0,
h: 3,
h: 6,
y: 0,
},
];

View File

@@ -35,7 +35,11 @@ function GridCardGraph({
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const {
toScrollWidgetId,
setToScrollWidgetId,
variablesToGetUpdated,
} = useDashboard();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
@@ -90,7 +94,11 @@ function GridCardGraph({
const isEmptyWidget =
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
const queryEnabledCondition = isVisible && !isEmptyWidget && isQueryEnabled;
const queryEnabledCondition =
isVisible &&
!isEmptyWidget &&
isQueryEnabled &&
isEmpty(variablesToGetUpdated);
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) {
@@ -166,7 +174,8 @@ function GridCardGraph({
const menuList =
widget.panelTypes === PANEL_TYPES.TABLE ||
widget.panelTypes === PANEL_TYPES.LIST
widget.panelTypes === PANEL_TYPES.LIST ||
widget.panelTypes === PANEL_TYPES.PIE
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
: headerMenuList;

View File

@@ -1,10 +1,14 @@
import './GridCardLayout.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd';
import { Flex, Form, Input, Modal, Tooltip, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form';
import cx from 'classnames';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
@@ -12,12 +16,21 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { defaultTo } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import { FullscreenIcon } from 'lucide-react';
import {
FullscreenIcon,
GripVertical,
MoveDown,
MoveUp,
Settings,
Trash2,
} from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { sortLayout } from 'providers/Dashboard/util';
import { useCallback, useEffect, useState } from 'react';
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout';
import { ItemCallback, Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@@ -27,6 +40,7 @@ import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
import { EditMenuAction, ViewMenuAction } from './config';
import GridCard from './GridCard';
@@ -45,6 +59,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
selectedDashboard,
layouts,
setLayouts,
panelMap,
setPanelMap,
setSelectedDashboard,
isDashboardLocked,
} = useDashboard();
@@ -65,6 +81,26 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
const [dashboardLayout, setDashboardLayout] = useState<Layout[]>([]);
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
const [currentSelectRowId, setCurrentSelectRowId] = useState<string | null>(
null,
);
const [currentPanelMap, setCurrentPanelMap] = useState<
Record<string, { widgets: Layout[]; collapsed: boolean }>
>({});
useEffect(() => {
setCurrentPanelMap(panelMap);
}, [panelMap]);
const [form] = useForm<{
title: string;
}>();
const updateDashboardMutation = useUpdateDashboard();
const { notifications } = useNotifications();
@@ -87,7 +123,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
);
useEffect(() => {
setDashboardLayout(layouts);
setDashboardLayout(sortLayout(layouts));
}, [layouts]);
const onSaveHandler = (): void => {
@@ -97,6 +133,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
...selectedDashboard,
data: {
...selectedDashboard.data,
panelMap: { ...currentPanelMap },
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
},
uuid: selectedDashboard.uuid,
@@ -106,8 +143,9 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(updatedDashboard.payload.data.layout);
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
setSelectedDashboard(updatedDashboard.payload);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
}
featureResponse.refetch();
@@ -130,7 +168,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
dashboardLayout,
);
if (!isEqual(filterLayout, filterDashboardLayout)) {
setDashboardLayout(layout);
const updatedLayout = sortLayout(layout);
setDashboardLayout(updatedLayout);
}
};
@@ -167,35 +206,335 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardLayout]);
function handleAddRow(): void {
if (!selectedDashboard) return;
const id = uuid();
const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = {
widgets: [],
collapsed: false,
};
const currentRowIdx = 0;
for (let j = currentRowIdx; j < dashboardLayout.length; j++) {
if (!currentPanelMap[dashboardLayout[j].i]) {
newRowWidgetMap.widgets.push(dashboardLayout[j]);
} else {
break;
}
}
const updatedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
layout: [
{
i: id,
w: 12,
minW: 12,
minH: 1,
maxH: 1,
x: 0,
h: 1,
y: 0,
},
...dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
],
panelMap: { ...currentPanelMap, [id]: newRowWidgetMap },
widgets: [
...(selectedDashboard.data.widgets || []),
{
id,
title: 'Sample Row',
description: '',
panelTypes: PANEL_GROUP_TYPES.ROW,
},
],
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutate(updatedDashboard, {
// eslint-disable-next-line sonarjs/no-identical-functions
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
setSelectedDashboard(updatedDashboard.payload);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
}
featureResponse.refetch();
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
}
const handleRowSettingsClick = (id: string): void => {
setIsSettingsModalOpen(true);
setCurrentSelectRowId(id);
};
const onSettingsModalSubmit = (): void => {
const newTitle = form.getFieldValue('title');
if (!selectedDashboard) return;
if (!currentSelectRowId) return;
const currentWidget = selectedDashboard?.data?.widgets?.find(
(e) => e.id === currentSelectRowId,
);
if (!currentWidget) return;
currentWidget.title = newTitle;
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
(e) => e.id !== currentSelectRowId,
);
updatedWidgets?.push(currentWidget);
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
if (setPanelMap)
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
form.setFieldValue('title', '');
setIsSettingsModalOpen(false);
setCurrentSelectRowId(null);
featureResponse.refetch();
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleRowCollapse = (id: string): void => {
if (!selectedDashboard) return;
const rowProperties = { ...currentPanelMap[id] };
const updatedPanelMap = { ...currentPanelMap };
let updatedDashboardLayout = [...dashboardLayout];
if (rowProperties.collapsed === true) {
rowProperties.collapsed = false;
const widgetsInsideTheRow = rowProperties.widgets;
let maxY = 0;
widgetsInsideTheRow.forEach((w) => {
maxY = Math.max(maxY, w.y + w.h);
});
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
if (currentRowWidget && widgetsInsideTheRow.length) {
maxY -= currentRowWidget.h + currentRowWidget.y;
}
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
updatedDashboardLayout[j].y += maxY;
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
updatedDashboardLayout[j].i
// eslint-disable-next-line @typescript-eslint/no-loop-func
].widgets.map((w) => ({
...w,
y: w.y + maxY,
}));
}
}
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
} else {
rowProperties.collapsed = true;
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
let widgetsInsideTheRow: Layout[] = [];
let isPanelMapUpdated = false;
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
if (currentPanelMap[dashboardLayout[j].i]) {
rowProperties.widgets = widgetsInsideTheRow;
widgetsInsideTheRow = [];
isPanelMapUpdated = true;
break;
} else {
widgetsInsideTheRow.push(dashboardLayout[j]);
}
}
if (!isPanelMapUpdated) {
rowProperties.widgets = widgetsInsideTheRow;
}
let maxY = 0;
widgetsInsideTheRow.forEach((w) => {
maxY = Math.max(maxY, w.y + w.h);
});
const currentRowWidget = dashboardLayout[currentIdx];
if (currentRowWidget && widgetsInsideTheRow.length) {
maxY -= currentRowWidget.h + currentRowWidget.y;
}
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
updatedDashboardLayout[j].y += maxY;
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
updatedDashboardLayout[j].i
// eslint-disable-next-line @typescript-eslint/no-loop-func
].widgets.map((w) => ({
...w,
y: w.y + maxY,
}));
}
}
updatedDashboardLayout = updatedDashboardLayout.filter(
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
);
}
setCurrentPanelMap((prev) => ({
...prev,
...updatedPanelMap,
[id]: {
...rowProperties,
},
}));
setDashboardLayout(sortLayout(updatedDashboardLayout));
};
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
if (currentPanelMap[oldItem.i]) {
const differenceY = newItem.y - oldItem.y;
const widgetsInsideRow = currentPanelMap[oldItem.i].widgets.map((w) => ({
...w,
y: w.y + differenceY,
}));
setCurrentPanelMap((prev) => ({
...prev,
[oldItem.i]: {
...prev[oldItem.i],
widgets: widgetsInsideRow,
},
}));
}
};
const handleRowDelete = (): void => {
if (!selectedDashboard) return;
if (!currentSelectRowId) return;
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
(e) => e.id !== currentSelectRowId,
);
const updatedLayout =
selectedDashboard.data.layout?.filter((e) => e.i !== currentSelectRowId) ||
[];
const updatedPanelMap = { ...currentPanelMap };
delete updatedPanelMap[currentSelectRowId];
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
layout: updatedLayout,
panelMap: updatedPanelMap,
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
if (setPanelMap)
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
setIsDeleteModalOpen(false);
setCurrentSelectRowId(null);
featureResponse.refetch();
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
return (
<>
<ButtonContainer>
<Tooltip title="Open in Full Screen">
<Button
className="periscope-btn"
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
disabled={updateDashboardMutation.isLoading}
/>
</Tooltip>
<Flex justify="flex-end" gap={8} align="center">
<FacingIssueBtn
attributes={{
uuid: selectedDashboard?.uuid,
title: data?.title,
screen: 'Dashboard Details',
}}
eventName="Dashboard: Facing Issues in dashboard"
buttonText="Need help with this dashboard?"
message={dashboardHelpMessage(data, selectedDashboard)}
onHoverText="Click here to get help for this dashboard"
/>
<ButtonContainer>
<Tooltip title="Open in Full Screen">
<Button
className="periscope-btn"
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
disabled={updateDashboardMutation.isLoading}
/>
</Tooltip>
{!isDashboardLocked && addPanelPermission && (
<Button
className="periscope-btn"
onClick={onAddPanelHandler}
icon={<PlusOutlined />}
data-testid="add-panel"
>
{t('dashboard:add_panel')}
</Button>
)}
</ButtonContainer>
{!isDashboardLocked && addPanelPermission && (
<Button
className="periscope-btn"
onClick={onAddPanelHandler}
icon={<PlusOutlined />}
data-testid="add-panel"
>
{t('dashboard:add_panel')}
</Button>
)}
{!isDashboardLocked && addPanelPermission && (
<Button
className="periscope-btn"
onClick={(): void => handleAddRow()}
icon={<PlusOutlined />}
data-testid="add-row"
>
{t('dashboard:add_row')}
</Button>
)}
</ButtonContainer>
</Flex>
<FullScreen handle={handle} className="fullscreen-grid-container">
<ReactGridLayout
cols={12}
rowHeight={100}
rowHeight={45}
autoSize
width={100}
useCSSTransforms
@@ -204,6 +543,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
isResizable={!isDashboardLocked && addPanelPermission}
allowOverlap={false}
onLayoutChange={handleLayoutChange}
onDragStop={handleDragStop}
draggableHandle=".drag-handle"
layout={dashboardLayout}
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
@@ -212,6 +552,58 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
const { i: id } = layout;
const currentWidget = (widgets || [])?.find((e) => e.id === id);
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
const rowWidgetProperties = currentPanelMap[id] || {};
return (
<CardContainer
className="row-card"
isDarkMode={isDarkMode}
key={id}
data-grid={JSON.stringify(currentWidget)}
>
<div className={cx('row-panel')}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<Button
disabled={updateDashboardMutation.isLoading}
icon={
rowWidgetProperties.collapsed ? (
<MoveDown size={14} />
) : (
<MoveUp size={14} />
)
}
type="text"
onClick={(): void => handleRowCollapse(id)}
/>
<Typography.Text>{currentWidget.title}</Typography.Text>
<Button
icon={<Settings size={14} />}
type="text"
onClick={(): void => handleRowSettingsClick(id)}
/>
</div>
{rowWidgetProperties.collapsed && (
<Button
type="text"
icon={<GripVertical size={14} />}
className="drag-handle"
/>
)}
{!rowWidgetProperties.collapsed && (
<Button
type="text"
icon={<Trash2 size={14} />}
onClick={(): void => {
setIsDeleteModalOpen(true);
setCurrentSelectRowId(id);
}}
/>
)}
</div>
</CardContainer>
);
}
return (
<CardContainer
className={isDashboardLocked ? '' : 'enable-resize'}
@@ -224,7 +616,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
>
<GridCard
widget={currentWidget || ({ id, query: {} } as Widgets)}
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
headerMenuList={widgetActions}
variables={variables}
version={selectedDashboard?.data?.version}
@@ -235,6 +627,46 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
);
})}
</ReactGridLayout>
<Modal
open={isSettingsModalOpen}
title="Row Options"
destroyOnClose
footer={null}
onCancel={(): void => {
setIsSettingsModalOpen(false);
setCurrentSelectRowId(null);
}}
>
<Form form={form} onFinish={onSettingsModalSubmit} requiredMark>
<Form.Item required name={['title']}>
<Input
placeholder="Enter row name here..."
defaultValue={defaultTo(
widgets?.find((widget) => widget.id === currentSelectRowId)
?.title as string,
'Sample Title',
)}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Apply Changes
</Button>
</Form.Item>
</Form>
</Modal>
<Modal
open={isDeleteModalOpen}
title="Delete Row"
destroyOnClose
onCancel={(): void => {
setIsDeleteModalOpen(false);
setCurrentSelectRowId(null);
}}
onOk={(): void => handleRowDelete()}
>
<Typography.Text>Are you sure you want to delete this row</Typography.Text>
</Modal>
</FullScreen>
</>
);

View File

@@ -16,6 +16,6 @@ export const EMPTY_WIDGET_LAYOUT = {
i: PANEL_TYPES.EMPTY_WIDGET,
w: 6,
x: 0,
h: 3,
h: 6,
y: 0,
};

View File

@@ -29,6 +29,17 @@ interface Props {
export const CardContainer = styled.div<Props>`
overflow: auto;
&.row-card {
.row-panel {
height: 100%;
display: flex;
justify-content: space-between;
background: var(--bg-ink-400);
align-items: center;
overflow: hidden;
}
}
&.enable-resize {
:hover {
.react-resizable-handle {

View File

@@ -42,6 +42,7 @@ const GridPanelSwitch = forwardRef<
thresholds,
},
[PANEL_TYPES.LIST]: null,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.BAR]: {
data,

View File

@@ -38,6 +38,7 @@ export type PropsTypePropsMap = {
[PANEL_TYPES.VALUE]: GridValueComponentProps;
[PANEL_TYPES.TABLE]: GridTableComponentProps;
[PANEL_TYPES.TRACE]: null;
[PANEL_TYPES.PIE]: null;
[PANEL_TYPES.LIST]: null;
[PANEL_TYPES.BAR]: UplotProps & {
ref: ForwardedRef<ToggleGraphProps | undefined>;

View File

@@ -4,6 +4,7 @@ import { Input, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/facingIssueBtn/util';
import {
DynamicColumnsKey,
TableDataSource,
@@ -358,6 +359,15 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
pagination={{
defaultCurrent: Number(paginationParam) || 1,
}}
facingIssueBtn={{
attributes: {
screen: 'Alert list page',
},
eventName: 'Alert: Facing Issues in alert',
buttonText: 'Facing issues with alerts?',
message: listAlertMessage,
onHoverText: 'Click here to get help with alerts',
}}
/>
</>
);

View File

@@ -3,6 +3,7 @@ import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
import { ItemType } from 'antd/es/menu/hooks/useItems';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import { dashboardListMessage } from 'components/facingIssueBtn/util';
import {
DynamicColumnsKey,
TableDataSource,
@@ -385,6 +386,15 @@ function DashboardsList(): JSX.Element {
dataSource={data}
onChange={handleChange}
showSorterTooltip
facingIssueBtn={{
attributes: {
screen: 'Dashboard list page',
},
eventName: 'Dashboard: Facing Issues in dashboard',
buttonText: 'Facing issues with dashboards?',
message: dashboardListMessage,
onHoverText: 'Click here to get help with dashboards',
}}
/>
</TableContainer>
</Card>

View File

@@ -85,8 +85,8 @@ function LogControls(): JSX.Element | null {
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format()
: dayjs(log.timestamp / 1e6).format();
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return FlatLogData({
...log,

View File

@@ -531,8 +531,8 @@ function LogsExplorerViews({
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format()
: dayjs(log.timestamp / 1e6).format();
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return FlatLogData({
timestamp,
@@ -608,6 +608,7 @@ function LogsExplorerViews({
className="periscope-btn"
onClick={handleToggleShowFormatOptions}
icon={<Sliders size={14} />}
data-testid="periscope-btn"
/>
{showFormatMenuItems && (

View File

@@ -0,0 +1,151 @@
import { render, RenderResult } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { VirtuosoMockContext } from 'react-virtuoso';
import i18n from 'ReactI18';
import store from 'store';
import LogsExplorerViews from '..';
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
const logExplorerRoute = '/logs/logs-explorer';
const queryRangeURL = 'http://localhost/api/v3/query_range';
const lodsQueryServerRequest = (): void =>
server.use(
rest.post(queryRangeURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(logsQueryRangeSuccessResponse)),
),
);
// mocking the graph components in this test as this should be handled separately
jest.mock(
'container/TimeSeriesView/TimeSeriesView',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function () {
return <div>Time Series Chart</div>;
},
);
jest.mock(
'container/LogsExplorerChart',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function () {
return <div>Histogram Chart</div>;
},
);
jest.mock('constants/panelTypes', () => ({
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
}));
jest.mock('d3-interpolate', () => ({
interpolate: jest.fn(),
}));
jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
__esModule: true,
useGetExplorerQueryRange: jest.fn(),
}));
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
beforeEach(() => {
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
});
});
const renderer = (): RenderResult =>
render(
<MemoryRouter initialEntries={[logExplorerRoute]}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<MockQueryClientProvider>
<QueryBuilderProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews selectedView={SELECTED_VIEWS.SEARCH} showHistogram />
</VirtuosoMockContext.Provider>
</QueryBuilderProvider>
</MockQueryClientProvider>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
describe('LogsExplorerViews -', () => {
it('render correctly with props - list and table', async () => {
lodsQueryServerRequest();
const { queryByText, queryByTestId } = renderer();
expect(queryByTestId('periscope-btn')).toBeInTheDocument();
await userEvent.click(queryByTestId('periscope-btn') as HTMLElement);
expect(document.querySelector('.menu-container')).toBeInTheDocument();
const menuItems = document.querySelectorAll('.menu-items .item');
expect(menuItems.length).toBe(3);
// switch to table view
// eslint-disable-next-line sonarjs/no-duplicate-string
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText(
'{"container_id":"container_id","container_name":"container_name","driver":"driver","eta":"2m0s","location":"frontend","log_level":"INFO","message":"Dispatch successful","service":"frontend","span_id":"span_id","trace_id":"span_id"}',
),
).toBeInTheDocument();
});
it('check isLoading state', async () => {
lodsQueryServerRequest();
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
isLoading: true,
isFetching: false,
});
const { queryByText, queryByTestId } = renderer();
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText(
'Just a bit of patience, just a little bits enough ⎯ were getting your logs!',
),
).toBeInTheDocument();
});
it('check error state', async () => {
lodsQueryServerRequest();
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
isLoading: false,
isFetching: false,
isError: true,
});
const { queryByText, queryByTestId } = renderer();
expect(
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,51 @@
export const logsQueryRangeSuccessNewFormatResponse = {
data: {
result: [],
resultType: '',
newResult: {
status: 'success',
data: {
resultType: '',
result: [
{
queryName: 'A',
series: null,
list: [
{
timestamp: '2024-02-15T21:20:22Z',
data: {
attributes_bool: {},
attributes_float64: {},
attributes_int64: {},
attributes_string: {
container_id: 'container_id',
container_name: 'container_name',
driver: 'driver',
eta: '2m0s',
location: 'frontend',
log_level: 'INFO',
message: 'Dispatch successful',
service: 'frontend',
span_id: 'span_id',
trace_id: 'span_id',
},
body:
'2024-02-15T21:20:22.035Z\tINFO\tfrontend\tDispatch successful\t{"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}',
id: 'id',
resources_string: {
'container.name': 'container_name',
},
severity_number: 0,
severity_text: '',
span_id: '',
trace_flags: 0,
trace_id: '',
},
},
],
},
],
},
},
},
};

View File

@@ -1,3 +1,5 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { DownloadOptions } from 'container/Download/Download.types';
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
@@ -20,7 +22,7 @@ export enum FORMULA {
ERROR_PERCENTAGE = 'A*100/B',
DATABASE_CALLS_AVG_DURATION = 'A/B',
APDEX_TRACES = '((B + C)/2)/A',
APDEX_DELTA_SPAN_METRICS = '(B + C/2)/A',
APDEX_DELTA_SPAN_METRICS = '((B + C)/2)/A',
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
}

View File

@@ -33,6 +33,8 @@ export const getNearestHighestBucketValue = (
value: number,
buckets: number[],
): string => {
// sort the buckets
buckets.sort((a, b) => a - b);
const nearestBucket = buckets.find((bucket) => bucket >= value);
return nearestBucket?.toString() || '+Inf';
};

View File

@@ -10,6 +10,7 @@ export const PANEL_TYPES_INITIAL_QUERY = {
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
};

View File

@@ -1,146 +1,59 @@
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import {
listViewInitialLogQuery,
listViewInitialTraceQuery,
PANEL_TYPES_INITIAL_QUERY,
} from './constants';
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
import menuItems from './menuItems';
import { Card, Container, Text } from './styles';
function DashboardGraphSlider(): JSX.Element {
const {
handleToggleDashboardSlider,
layouts,
selectedDashboard,
} = useDashboard();
const { data } = selectedDashboard || {};
const { notifications } = useNotifications();
const updateDashboardMutation = useUpdateDashboard();
const { handleToggleDashboardSlider } = useDashboard();
// eslint-disable-next-line sonarjs/cognitive-complexity
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
updateDashboardMutation.mutateAsync(
{
uuid: selectedDashboard?.uuid || '',
data: {
title: data?.title || '',
variables: data?.variables || {},
description: data?.description || '',
name: data?.name || '',
tags: data?.tags || [],
version: data?.version || 'v3',
layout: [
handleToggleDashboardSlider(false);
const queryParamsLog = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify({
...PANEL_TYPES_INITIAL_QUERY[name],
builder: {
...PANEL_TYPES_INITIAL_QUERY[name].builder,
queryData: [
{
i: id,
w: 6,
x: 0,
h: 3,
y: 0,
},
...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) ||
[]),
],
widgets: [
...(data?.widgets || []),
{
id,
title: '',
description: '',
isStacked: false,
nullZeroValues: '',
opacity: '',
panelTypes: name,
query:
name === PANEL_TYPES.LIST
? listViewInitialLogQuery
: PANEL_TYPES_INITIAL_QUERY[name],
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
selectedLogFields: [
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
],
selectedTracesFields: [
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
],
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
},
{
onSuccess: (data) => {
if (data.payload) {
handleToggleDashboardSlider(false);
const queryParamsLog = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify({
...PANEL_TYPES_INITIAL_QUERY[name],
builder: {
...PANEL_TYPES_INITIAL_QUERY[name].builder,
queryData: [
{
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
}),
};
}),
};
const queryParams = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
const queryParams = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
if (name === PANEL_TYPES.LIST) {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
);
} else {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
);
}
}
},
onError: () => {
notifications.success({
message: SOMETHING_WENT_WRONG,
});
},
},
);
if (name === PANEL_TYPES.LIST) {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
);
} else {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
);
}
};
return (

View File

@@ -1,6 +1,13 @@
import { Color } from '@signozhq/design-tokens';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { BarChart3, LineChart, List, SigmaSquare, Table } from 'lucide-react';
import {
BarChart3,
LineChart,
List,
PieChart,
SigmaSquare,
Table,
} from 'lucide-react';
const Items: ItemsProps[] = [
{
@@ -28,6 +35,11 @@ const Items: ItemsProps[] = [
icon: <BarChart3 size={32} color={Color.BG_ROBIN_400} />,
display: 'Bar',
},
{
name: PANEL_TYPES.PIE,
icon: <PieChart size={32} color={Color.BG_ROBIN_400} />,
display: 'Pie',
},
];
export interface ItemsProps {

View File

@@ -1,9 +1,9 @@
import { Row } from 'antd';
import { isNull } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useState } from 'react';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { convertVariablesToDbFormat } from './util';
import VariableItem from './VariableItem';
function DashboardVariableSelection(): JSX.Element | null {
@@ -11,15 +11,14 @@ function DashboardVariableSelection(): JSX.Element | null {
selectedDashboard,
setSelectedDashboard,
updateLocalStorageDashboardVariables,
variablesToGetUpdated,
setVariablesToGetUpdated,
} = useDashboard();
const { data } = selectedDashboard || {};
const { variables } = data || {};
const [update, setUpdate] = useState<boolean>(false);
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const [variablesTableData, setVariablesTableData] = useState<any>([]);
useEffect(() => {
@@ -45,8 +44,27 @@ function DashboardVariableSelection(): JSX.Element | null {
}, [variables]);
const onVarChanged = (name: string): void => {
setLastUpdatedVar(name);
setUpdate(!update);
/**
* this function takes care of adding the dependent variables to current update queue and removing
* the updated variable name from the queue
*/
const dependentVariables = variablesTableData
?.map((variable: any) => {
if (variable.type === 'QUERY') {
const re = new RegExp(`\\{\\{\\s*?\\.${name}\\s*?\\}\\}`); // regex for `{{.var}}`
const queryValue = variable.queryValue || '';
const dependVarReMatch = queryValue.match(re);
if (dependVarReMatch !== null && dependVarReMatch.length > 0) {
return variable.name;
}
}
return null;
})
.filter((val: string | null) => !isNull(val));
setVariablesToGetUpdated((prev) => [
...prev.filter((v) => v !== name),
...dependentVariables,
]);
};
const onValueUpdate = (
@@ -54,39 +72,46 @@ function DashboardVariableSelection(): JSX.Element | null {
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
if (id) {
const newVariablesArr = variablesTableData.map(
(variable: IDashboardVariable) => {
const variableCopy = { ...variable };
if (variableCopy.id === id) {
variableCopy.selectedValue = value;
variableCopy.allSelected = allSelected;
}
return variableCopy;
},
);
updateLocalStorageDashboardVariables(name, value, allSelected);
const variables = convertVariablesToDbFormat(newVariablesArr);
if (selectedDashboard) {
setSelectedDashboard({
...selectedDashboard,
data: {
...selectedDashboard?.data,
variables: {
...variables,
},
},
setSelectedDashboard((prev) => {
if (prev) {
const oldVariables = prev?.data.variables;
// this is added to handle case where we have two different
// schemas for variable response
if (oldVariables[id]) {
oldVariables[id] = {
...oldVariables[id],
selectedValue: value,
allSelected,
};
}
if (oldVariables[name]) {
oldVariables[name] = {
...oldVariables[name],
selectedValue: value,
allSelected,
};
}
return {
...prev,
data: {
...prev?.data,
variables: {
...oldVariables,
},
},
};
}
return prev;
});
}
onVarChanged(name);
setUpdate(!update);
}
};
@@ -107,13 +132,12 @@ function DashboardVariableSelection(): JSX.Element | null {
<VariableItem
key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={variables}
lastUpdatedVar={lastUpdatedVar}
variableData={{
name: variable.name,
...variable,
change: update,
}}
onValueUpdate={onValueUpdate}
variablesToGetUpdated={variablesToGetUpdated}
/>
))}
</Row>

View File

@@ -53,7 +53,7 @@ describe('VariableItem', () => {
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -68,7 +68,7 @@ describe('VariableItem', () => {
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -82,7 +82,7 @@ describe('VariableItem', () => {
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -110,7 +110,7 @@ describe('VariableItem', () => {
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -131,7 +131,7 @@ describe('VariableItem', () => {
variableData={customVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);
@@ -146,7 +146,7 @@ describe('VariableItem', () => {
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
variablesToGetUpdated={[]}
/>
</MockQueryClientProvider>,
);

View File

@@ -32,7 +32,7 @@ interface VariableItemProps {
arg1: IDashboardVariable['selectedValue'],
allSelected: boolean,
) => void;
lastUpdatedVar: string;
variablesToGetUpdated: string[];
}
const getSelectValue = (
@@ -49,7 +49,7 @@ function VariableItem({
variableData,
existingVariables,
onValueUpdate,
lastUpdatedVar,
variablesToGetUpdated,
}: VariableItemProps): JSX.Element {
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
@@ -108,16 +108,10 @@ function VariableItem({
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
/* eslint-disable no-useless-escape */
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
// If the variable is dependent on the last updated variable
// and contains the last updated variable in its query (of the form `{{.var}}`)
// then we need to update the value of the variable
const queryValue = variableData.queryValue || '';
const dependVarReMatch = queryValue.match(re);
if (
variableData.type === 'QUERY' &&
dependVarReMatch !== null &&
dependVarReMatch.length > 0
variableData.name &&
variablesToGetUpdated.includes(variableData.name)
) {
let value = variableData.selectedValue;
let allSelected = false;

View File

@@ -4,6 +4,7 @@ import { Button, Tabs, Tooltip, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import { getDefaultWidgetData } from 'container/NewWidget/utils';
import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
@@ -11,6 +12,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo } from 'lodash-es';
import { Atom, Play, Terminal } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -55,8 +57,11 @@ function QuerySection({
const getWidget = useCallback(() => {
const widgetId = urlQuery.get('widgetId');
return widgets?.find((e) => e.id === widgetId);
}, [widgets, urlQuery]);
return defaultTo(
widgets?.find((e) => e.id === widgetId),
getDefaultWidgetData(widgetId || '', selectedGraph),
);
}, [urlQuery, widgets, selectedGraph]);
const selectedWidget = getWidget() as Widgets;

View File

@@ -67,12 +67,13 @@ function LeftContainer({
setRequestData((prev) => ({
...prev,
selectedTime: selectedTime.enum || prev.selectedTime,
globalSelectedInterval,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: stagedQuery,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stagedQuery, selectedTime]);
}, [stagedQuery, selectedTime, globalSelectedInterval]);
const queryResponse = useGetQueryRange(
requestData,

View File

@@ -0,0 +1,4 @@
.facing-issue-btn-container {
display: grid;
grid-template-columns: 1fr max-content;
}

View File

@@ -26,6 +26,7 @@ export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -36,6 +37,7 @@ export const panelTypeVsSoftMinMax: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -45,6 +47,7 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.TIME_SERIES]: false,
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.TRACE]: false,
@@ -56,6 +59,7 @@ export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -66,6 +70,7 @@ export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -76,6 +81,7 @@ export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
@@ -88,6 +94,7 @@ export const panelTypeVsPanelTimePreferences: {
[PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: true,
[PANEL_TYPES.BAR]: true,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,

View File

@@ -1,6 +1,10 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './NewWidget.styles.scss';
import { LockFilled, WarningOutlined } from '@ant-design/icons';
import { Button, Modal, Space, Tooltip, Typography } from 'antd';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { chartHelpMessage } from 'components/facingIssueBtn/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
@@ -14,6 +18,7 @@ import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { defaultTo, isUndefined } from 'lodash-es';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
@@ -45,7 +50,11 @@ import {
RightContainerWrapper,
} from './styles';
import { NewWidgetProps } from './types';
import { getIsQueryModified, handleQueryChange } from './utils';
import {
getDefaultWidgetData,
getIsQueryModified,
handleQueryChange,
} from './utils';
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const {
@@ -80,10 +89,26 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const { dashboardId } = useParams<DashboardWidgetPageParams>();
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
useEffect(() => {
const widgetId = query.get('widgetId');
const selectedWidget = widgets?.find((e) => e.id === widgetId);
const isWidgetNotPresent = isUndefined(selectedWidget);
if (isWidgetNotPresent) {
setIsNewDashboard(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getWidget = useCallback(() => {
const widgetId = query.get('widgetId');
return widgets?.find((e) => e.id === widgetId);
}, [query, widgets]);
const selectedWidget = widgets?.find((e) => e.id === widgetId);
return defaultTo(
selectedWidget,
getDefaultWidgetData(widgetId || '', selectedGraph),
) as Widgets;
}, [query, selectedGraph, widgets]);
const [selectedWidget, setSelectedWidget] = useState(getWidget());
@@ -227,6 +252,20 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
return;
}
const widgetId = query.get('widgetId');
let updatedLayout = selectedDashboard.data.layout || [];
if (isNewDashboard) {
updatedLayout = [
{
i: widgetId || '',
w: 6,
x: 0,
h: 6,
y: 0,
},
...updatedLayout,
];
}
const dashboard: Dashboard = {
...selectedDashboard,
uuid: selectedDashboard.uuid,
@@ -254,6 +293,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
},
...afterWidgets,
],
layout: [...updatedLayout],
},
};
@@ -274,6 +314,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
});
}, [
selectedDashboard,
query,
isNewDashboard,
preWidgets,
selectedWidget,
selectedTime.enum,
@@ -363,33 +405,49 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
return (
<Container>
<ButtonContainer>
{isSaveDisabled && (
<Tooltip title={MESSAGE.PANEL}>
<div className="facing-issue-btn-container">
<FacingIssueBtn
attributes={{
uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title,
panelType: graphType,
widgetId: query.get('widgetId'),
queryType: currentQuery.queryType,
screen: 'Dashboard list page',
}}
eventName="Dashboard: Facing Issues in dashboard"
buttonText="Need help with this chart?"
message={chartHelpMessage(selectedDashboard, graphType)}
onHoverText="Click here to get help in creating chart"
/>
<ButtonContainer>
{isSaveDisabled && (
<Tooltip title={MESSAGE.PANEL}>
<Button
icon={<LockFilled />}
type="primary"
disabled={isSaveDisabled}
onClick={onSaveDashboard}
>
Save Changes
</Button>
</Tooltip>
)}
{!isSaveDisabled && (
<Button
icon={<LockFilled />}
type="primary"
data-testid="new-widget-save"
loading={updateDashboardMutation.isLoading}
disabled={isSaveDisabled}
onClick={onSaveDashboard}
>
Save Changes
</Button>
</Tooltip>
)}
{!isSaveDisabled && (
<Button
type="primary"
data-testid="new-widget-save"
loading={updateDashboardMutation.isLoading}
disabled={isSaveDisabled}
onClick={onSaveDashboard}
>
Save Changes
</Button>
)}
<Button onClick={onClickDiscardHandler}>Discard Changes</Button>
</ButtonContainer>
)}
<Button onClick={onClickDiscardHandler}>Discard Changes</Button>
</ButtonContainer>
</div>
<PanelContainer>
<LeftContainerWrapper flex={5}>

View File

@@ -3,7 +3,13 @@ import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import {
listViewInitialLogQuery,
listViewInitialTraceQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/NewDashboard/ComponentsSlider/constants';
import { isEqual, set, unset } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -25,6 +31,7 @@ export type PartialPanelTypes = {
[PANEL_TYPES.TABLE]: 'table';
[PANEL_TYPES.TIME_SERIES]: 'graph';
[PANEL_TYPES.VALUE]: 'value';
[PANEL_TYPES.PIE]: 'pie';
};
export const panelTypeDataSourceFormValuesMap: Record<
@@ -163,6 +170,50 @@ export const panelTypeDataSourceFormValuesMap: Record<
},
},
},
[PANEL_TYPES.PIE]: {
[DataSource.LOGS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
],
},
},
[DataSource.METRICS]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
'functions',
'spaceAggregation',
],
},
},
[DataSource.TRACES]: {
builder: {
queryData: [
'filters',
'aggregateOperator',
'aggregateAttribute',
'groupBy',
'limit',
'having',
'orderBy',
],
},
},
},
[PANEL_TYPES.LIST]: {
[DataSource.LOGS]: {
builder: {
@@ -257,3 +308,38 @@ export function handleQueryChange(
},
};
}
export const getDefaultWidgetData = (
id: string,
name: PANEL_TYPES,
): Widgets => ({
id,
title: '',
description: '',
isStacked: false,
nullZeroValues: '',
opacity: '',
panelTypes: name,
query:
name === PANEL_TYPES.LIST
? listViewInitialLogQuery
: PANEL_TYPES_INITIAL_QUERY[name],
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
selectedLogFields: [
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
],
selectedTracesFields: [
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
],
});

View File

@@ -159,7 +159,7 @@ export default function EnvironmentDetails(): JSX.Element {
<div className="request-entity-container">
<Typography.Text>
Cannot find what youre looking for? Request a data source
Cannot find what youre looking for? Request an environment
</Typography.Text>
<div className="form-section">

View File

@@ -10,6 +10,7 @@ import {
LeftCircleOutlined,
} from '@ant-design/icons';
import { Button, Space, Steps, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
@@ -17,6 +18,7 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
import useAnalytics from 'hooks/analytics/useAnalytics';
import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es';
import { HelpCircle } from 'lucide-react';
import { useState } from 'react';
import { useOnboardingContext } from '../../context/OnboardingContext';
@@ -379,6 +381,31 @@ export default function ModuleStepsContainer({
history.push('/');
};
const handleFacingIssuesClick = (): void => {
logEvent('Onboarding V2: Facing Issues Sending Data to SigNoz', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
module: activeStep?.module?.id,
step: activeStep?.step?.id,
});
const message = `Hi Team,
I am facing issues sending data to SigNoz. Here are my application details
Data Source: ${selectedDataSource?.name}
Framework:
Environment:
Module: ${activeStep?.module?.id}
Thanks
`;
if (window.Intercom) {
window.Intercom('showNewMessage', message);
}
};
return (
<div className="onboarding-module-steps">
<div className="steps-container">
@@ -455,6 +482,15 @@ export default function ModuleStepsContainer({
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
</Button>
<Button
className="periscope-btn"
onClick={handleFacingIssuesClick}
danger
icon={<HelpCircle size={14} />}
>
Facing issues sending data to SigNoz?
</Button>
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
.piechart-no-data {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.piechart-container {
height: 90%;
width: 100%;
}
.piechart-tooltip {
.piechart-indicator {
width: 15px;
height: 3px;
border-radius: 2px;
}
.tooltip-value {
font-size: 12px;
font-weight: 600;
}
}
.piechart-legend {
width: 100%;
height: 40px;
overflow-y: scroll;
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
.piechart-legend-item {
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
.piechart-legend-label {
width: 10px;
height: 10px;
border-radius: 50%;
}
}
}

View File

@@ -0,0 +1,218 @@
import './PiePanelWrapper.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Group } from '@visx/group';
import { Pie } from '@visx/shape';
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
import { themeColors } from 'constants/theme';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { useRef, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { PanelWrapperProps, TooltipData } from './panelWrapper.types';
import { getLabel, lightenColor, tooltipStyles } from './utils';
// refernce: https://www.youtube.com/watch?v=bL3P9CqQkKw
function PiePanelWrapper({
queryResponse,
widget,
}: PanelWrapperProps): JSX.Element {
const [active, setActive] = useState<{
label: string;
value: string;
color: string;
} | null>(null);
const {
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
} = useTooltip<TooltipData>();
const { containerRef, TooltipInPortal } = useTooltipInPortal({
scroll: true,
detectBounds: true,
});
const panelData =
queryResponse.data?.payload?.data.newResult.data.result || [];
const isDarkMode = useIsDarkMode();
const pieChartData: {
label: string;
value: string;
color: string;
}[] = [].concat(
...(panelData
.map((d) =>
d.series?.map((s) => ({
label:
d.series?.length === 1
? getLabel(Object.values(s.labels)[0], widget.query, d.queryName)
: getLabel(Object.values(s.labels)[0], {} as Query, d.queryName, true),
value: s.values[0].value,
color: generateColor(
d.series?.length === 1
? getLabel(Object.values(s.labels)[0], widget.query, d.queryName)
: getLabel(Object.values(s.labels)[0], {} as Query, d.queryName, true),
themeColors.chartcolors,
),
})),
)
.filter((d) => d !== undefined) as never[]),
);
let size = 0;
let width = 0;
let height = 0;
const chartRef = useRef<HTMLDivElement>(null);
if (chartRef.current) {
const { offsetWidth, offsetHeight } = chartRef.current;
size = Math.min(offsetWidth, offsetHeight);
width = offsetWidth;
height = offsetHeight;
}
const half = size / 2;
const getFillColor = (color: string): string => {
if (active === null) {
return color;
}
const lightenedColor = lightenColor(color, 0.4); // Adjust the opacity value (0.7 in this case)
return active.color === color ? color : lightenedColor;
};
return (
<>
{!pieChartData.length && <div className="piechart-no-data">No data</div>}
{pieChartData.length > 0 && (
<>
<div className="piechart-container" ref={chartRef}>
<svg width={width} height={height} ref={containerRef}>
<Group top={height / 2} left={width / 2}>
<Pie
data={pieChartData}
pieValue={(data: {
label: string;
value: string;
color: string;
}): number => parseFloat(data.value)}
outerRadius={({ data }): number => {
if (!active) return half - 3;
return data.label === active.label ? half : half - 3;
}}
padAngle={0.02}
cornerRadius={3}
width={size}
height={size}
>
{
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
(pie) =>
pie.arcs.map((arc, index) => {
const { label } = arc.data;
const [centroidX, centroidY] = pie.path.centroid(arc);
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.6;
const arcPath = pie.path(arc);
const arcFill = arc.data.color;
return (
<g
// eslint-disable-next-line react/no-array-index-key
key={`arc-${label}-${index}`}
onMouseEnter={(): void => {
showTooltip({
tooltipData: {
label,
value: arc.data.value,
color: arc.data.color,
key: label,
},
tooltipTop: centroidY + height / 2,
tooltipLeft: centroidX + width / 2,
});
setActive(arc.data);
}}
onMouseLeave={(): void => {
hideTooltip();
setActive(null);
}}
>
<path d={arcPath || ''} fill={getFillColor(arcFill)} />
{hasSpaceForLabel && (
<text
x={centroidX}
y={centroidY}
dy=".33em"
fill="#000"
fontSize={10}
textAnchor="middle"
pointerEvents="none"
>
{arc.data.label}
</text>
)}
</g>
);
})
}
</Pie>
</Group>
</svg>
{tooltipOpen && tooltipData && (
<TooltipInPortal
top={tooltipTop}
left={tooltipLeft}
style={{
...tooltipStyles,
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400,
}}
className="piechart-tooltip"
>
<div
style={{
background: tooltipData.color,
}}
className="piechart-indicator"
/>
{tooltipData.key}
<div className="tooltip-value">{tooltipData.value}</div>
</TooltipInPortal>
)}
</div>
<div className="piechart-legend">
{pieChartData.length > 0 &&
pieChartData.map((data) => (
<div
key={data.label}
className="piechart-legend-item"
onMouseEnter={(): void => {
setActive(data);
}}
onMouseLeave={(): void => {
setActive(null);
}}
>
<div
style={{
backgroundColor: getFillColor(data.color),
}}
className="piechart-legend-label"
/>
{data.label}
</div>
))}
</div>
</>
)}
</>
);
}
export default PiePanelWrapper;

View File

@@ -1,6 +1,7 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import ListPanelWrapper from './ListPanelWrapper';
import PiePanelWrapper from './PiePanelWrapper';
import TablePanelWrapper from './TablePanelWrapper';
import UplotPanelWrapper from './UplotPanelWrapper';
import ValuePanelWrapper from './ValuePanelWrapper';
@@ -12,5 +13,6 @@ export const PanelTypeVsPanelWrapper = {
[PANEL_TYPES.VALUE]: ValuePanelWrapper,
[PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
[PANEL_TYPES.PIE]: PiePanelWrapper,
[PANEL_TYPES.BAR]: UplotPanelWrapper,
};

View File

@@ -22,3 +22,10 @@ export type PanelWrapperProps = {
onDragSelect: (start: number, end: number) => void;
selectedGraph?: PANEL_TYPES;
};
export type TooltipData = {
label: string;
key: string;
value: string;
color: string;
};

View File

@@ -0,0 +1,73 @@
import { defaultStyles } from '@visx/tooltip';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export const tooltipStyles = {
...defaultStyles,
minWidth: 60,
backgroundColor: 'rgba(0,0,0,0.9)',
color: 'white',
zIndex: 9999,
display: 'flex',
gap: '10px',
justifyContent: 'center',
alignItems: 'center',
padding: '5px 10px',
};
export const getLabel = (
label: string,
query: Query,
queryName: string,
isQueryContentMultipleResult = false, // If there are more than one aggregation return by the query, this should be set to true. Default is false.
): string => {
let finalQuery;
if (!isQueryContentMultipleResult) {
finalQuery = query.builder.queryData.find((q) => q.queryName === queryName);
if (!finalQuery) {
// If the query is not found in queryData, then check in queryFormulas
finalQuery = query.builder.queryFormulas.find(
(q) => q.queryName === queryName,
);
}
}
if (finalQuery) {
if (finalQuery.legend !== '') {
return finalQuery.legend;
}
if (label !== undefined) {
return label;
}
return queryName;
}
return label;
};
// Function to convert a hex color to RGB format
const hexToRgb = (
color: string,
): { r: number; g: number; b: number } | null => {
const hex = color.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => r + r + g + g + b + b,
);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
export const lightenColor = (color: string, opacity: number): string => {
// Convert the hex color to RGB format
const rgbColor = hexToRgb(color);
if (!rgbColor) return color; // Return the original color if unable to parse
// Extract the RGB components
const { r, g, b } = rgbColor;
// Create a new RGBA color string with the specified opacity
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
};

View File

@@ -0,0 +1,87 @@
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
import ChangeHistory from '../index';
import { pipelineData, pipelineDataHistory } from './testUtils';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
describe('ChangeHistory test', () => {
it('should render changeHistory correctly', () => {
const { getAllByText, getByText } = render(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<ChangeHistory pipelineData={pipelineData} />
</I18nextProvider>
</Provider>
</QueryClientProvider>
</MemoryRouter>,
);
// change History table headers
[
'Version',
'Deployment Stage',
'Last Deploy Message',
'Last Deployed Time',
'Edited by',
].forEach((text) => expect(getByText(text)).toBeInTheDocument());
// table content
expect(getAllByText('test-user').length).toBe(2);
expect(getAllByText('Deployment was successful').length).toBe(2);
});
it('test deployment stage and icon based on history data', () => {
const { getByText, container } = render(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<ChangeHistory
pipelineData={{
...pipelineData,
history: pipelineDataHistory,
}}
/>
</I18nextProvider>
</Provider>
</QueryClientProvider>
</MemoryRouter>,
);
// assertion for different deployment stages
expect(container.querySelector('[data-icon="loading"]')).toBeInTheDocument();
expect(getByText('In Progress')).toBeInTheDocument();
expect(
container.querySelector('[data-icon="exclamation-circle"]'),
).toBeInTheDocument();
expect(getByText('Dirty')).toBeInTheDocument();
expect(
container.querySelector('[data-icon="close-circle"]'),
).toBeInTheDocument();
expect(getByText('Failed')).toBeInTheDocument();
expect(
container.querySelector('[data-icon="minus-circle"]'),
).toBeInTheDocument();
expect(getByText('Unknown')).toBeInTheDocument();
expect(container.querySelectorAll('.ant-table-row').length).toBe(5);
});
});

View File

@@ -0,0 +1,240 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { Pipeline } from 'types/api/pipeline/def';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const pipelineData: Pipeline = {
id: 'test-id-1',
version: 24,
elementType: 'log_pipelines',
active: false,
is_valid: false,
disabled: false,
deployStatus: 'DEPLOYED',
deployResult: 'Deployment was successful',
lastHash: 'log_pipelines:24',
lastConf: 'oiwernveroi',
createdBy: 'test-created-by',
pipelines: [
{
id: 'test-id-2',
orderId: 1,
name: 'hotrod logs parser',
alias: 'hotrodlogsparser',
description: 'Trying to test Logs Pipeline feature',
enabled: true,
filter: {
op: 'AND',
items: [
{
key: {
key: 'container_name',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
id: 'sampleid',
value: 'hotrod',
op: '=',
},
],
},
config: [
{
type: 'regex_parser',
id: 'parsetext(regex)',
output: 'parseattribsjson',
on_error: 'send',
orderId: 1,
enabled: true,
name: 'parse text (regex)',
parse_to: 'attributes',
regex:
'.+\\t+(?P<log_level>.+)\\t+(?P<location>.+)\\t+(?P<message>.+)\\t+(?P<attribs_json>.+)',
parse_from: 'body',
},
{
type: 'json_parser',
id: 'parseattribsjson',
output: 'removetempattribs_json',
orderId: 2,
enabled: true,
name: 'parse attribs json',
parse_to: 'attributes',
parse_from: 'attributes.attribs_json',
},
{
type: 'remove',
id: 'removetempattribs_json',
output: 'c2062723-895e-4614-ba38-29c5d5ee5927',
orderId: 3,
enabled: true,
name: 'remove temp attribs_json',
field: 'attributes.attribs_json',
},
{
type: 'add',
id: 'c2062723-895e-4614-ba38-29c5d5ee5927',
orderId: 4,
enabled: true,
name: 'test add ',
field: 'resource["container.name"]',
value: 'hotrod',
},
],
createdBy: 'test@email',
createdAt: '2024-01-02T13:56:02.858300964Z',
},
{
id: 'tes-id-1',
orderId: 2,
name: 'Logs Parser - test - Customer Service',
alias: 'LogsParser-test-CustomerService',
description: 'Trying to test Logs Pipeline feature',
enabled: true,
filter: {
op: 'AND',
items: [
{
key: {
key: 'service',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
id: 'sample-test-1',
value: 'customer',
op: '=',
},
],
},
config: [
{
type: 'grok_parser',
id: 'Testtest',
on_error: 'send',
orderId: 1,
enabled: true,
name: 'Test test',
parse_to: 'attributes',
pattern:
'^%{DATE:date}Z INFO customer/database.go:73 Loading customer {"service": "customer", "component": "mysql", "trace_id": "test-id", "span_id": "1427a3fcad8b1514", "customer_id": "567"}',
parse_from: 'body',
},
],
createdBy: 'test@email',
createdAt: '2024-01-02T13:56:02.863764227Z',
},
],
history: [
{
id: 'test-id-4',
version: 24,
elementType: 'log_pipelines',
active: false,
isValid: false,
disabled: false,
deployStatus: 'DEPLOYED',
deployResult: 'Deployment was successful',
lastHash: 'log_pipelines:24',
lastConf: 'eovineroiv',
createdBy: 'test-created-by',
createdByName: 'test-user',
createdAt: '2024-01-02T13:56:02Z',
},
{
id: 'test-4',
version: 23,
elementType: 'log_pipelines',
active: false,
isValid: false,
disabled: false,
deployStatus: 'DEPLOYED',
deployResult: 'Deployment was successful',
lastHash: 'log_pipelines:23',
lastConf: 'eivrounreovi',
createdBy: 'test-created-by',
createdByName: 'test-user',
createdAt: '2023-12-29T12:59:20Z',
},
],
};
export const pipelineDataHistory: Pipeline['history'] = [
{
id: 'test-id-4',
version: 24,
elementType: 'log_pipelines',
active: false,
isValid: false,
disabled: false,
deployStatus: 'DEPLOYED',
deployResult: 'Deployment was successful',
lastHash: 'log_pipelines:24',
lastConf: 'eovineroiv',
createdBy: 'test-created-by',
createdByName: 'test-user',
createdAt: '2024-01-02T13:56:02Z',
},
{
id: 'test-4',
version: 23,
elementType: 'log_pipelines',
active: false,
isValid: false,
disabled: false,
deployStatus: 'IN_PROGRESS',
deployResult: 'Deployment is in progress',
lastHash: 'log_pipelines:23',
lastConf: 'eivrounreovi',
createdBy: 'test-created-by',
createdByName: 'test-user',
createdAt: '2023-12-29T12:59:20Z',
},
{
id: 'test-4-1',
version: 25,
elementType: 'log_pipelines',
active: false,
isValid: false,
disabled: false,
deployStatus: 'DIRTY',
deployResult: 'Deployment is dirty',
lastHash: 'log_pipelines:23',
lastConf: 'eivrounreovi',
createdBy: 'test-created-by',
createdByName: 'test-user',
createdAt: '2023-12-29T12:59:20Z',
},
{
id: 'test-4-2',
version: 26,
elementType: 'log_pipelines',
active: false,
isValid: false,
disabled: false,
deployStatus: 'FAILED',
deployResult: 'Deployment failed',
lastHash: 'log_pipelines:23',
lastConf: 'eivrounreovi',
createdBy: 'test-created-by',
createdByName: 'test-user',
createdAt: '2023-12-29T12:59:20Z',
},
{
id: 'test-4-3',
version: 27,
elementType: 'log_pipelines',
active: false,
isValid: false,
disabled: false,
deployStatus: 'UNKNOWN',
deployResult: '',
lastHash: 'log_pipelines:23',
lastConf: 'eivrounreovi',
createdBy: 'test-created-by',
createdByName: 'test-user',
createdAt: '2023-12-29T12:59:20Z',
},
];

View File

@@ -1,4 +1,5 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
@@ -8,8 +9,17 @@ import store from 'store';
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
const trackEventVar = jest.fn();
jest.mock('hooks/analytics/useAnalytics', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
trackEvent: trackEventVar,
trackPageView: jest.fn(),
})),
}));
describe('PipelinePage container test', () => {
it('should render CreatePipelineButton section', () => {
it('should render CreatePipelineButton section', async () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
@@ -26,4 +36,58 @@ describe('PipelinePage container test', () => {
);
expect(asFragment()).toMatchSnapshot();
});
it('CreatePipelineButton - edit mode & tracking', async () => {
const { getByText } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<CreatePipelineButton
setActionType={jest.fn()}
isActionMode="viewing-mode"
setActionMode={jest.fn()}
pipelineData={pipelineApiResponseMockData}
/>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
// enter_edit_mode click and track event data
const editButton = getByText('enter_edit_mode');
expect(editButton).toBeInTheDocument();
await userEvent.click(editButton);
expect(trackEventVar).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui',
});
});
it('CreatePipelineButton - add new mode & tracking', async () => {
const { getByText } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<CreatePipelineButton
setActionType={jest.fn()}
isActionMode="viewing-mode"
setActionMode={jest.fn()}
pipelineData={{ ...pipelineApiResponseMockData, pipelines: [] }}
/>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
// new_pipeline click and track event data
const editButton = getByText('new_pipeline');
expect(editButton).toBeInTheDocument();
await userEvent.click(editButton);
expect(trackEventVar).toBeCalledWith(
'Logs: Pipelines: Clicked Add New Pipeline',
{
source: 'signoz-ui',
},
);
});
});

View File

@@ -1,4 +1,5 @@
import { render } from '@testing-library/react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
@@ -20,4 +21,43 @@ describe('PipelinePage container test', () => {
);
expect(asFragment()).toMatchSnapshot();
});
it('should handle search', async () => {
const setPipelineValue = jest.fn();
const { getByPlaceholderText, container } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<PipelinesSearchSection setPipelineSearchValue={setPipelineValue} />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
const searchInput = getByPlaceholderText('search_pipeline_placeholder');
// Type into the search input
userEvent.type(searchInput, 'sample_pipeline');
jest.advanceTimersByTime(299);
expect(setPipelineValue).not.toHaveBeenCalled();
// Wait for the debounce delay to pass
await waitFor(() => {
// Expect the callback to be called after debounce delay
expect(setPipelineValue).toHaveBeenCalledWith('sample_pipeline');
});
// clear button
fireEvent.click(
container.querySelector(
'span[class*="ant-input-clear-icon"]',
) as HTMLElement,
);
// Wait for the debounce delay to pass
await waitFor(() => {
expect(setPipelineValue).toHaveBeenCalledWith('');
});
});
});

View File

@@ -105,8 +105,8 @@ export default function QBEntityOptions({
onQueryFunctionsUpdates && (
<QueryFunctions
query={query}
queryFunctions={query.functions}
key={query.functions.toString()}
queryFunctions={query.functions || []}
key={query.functions?.toString()}
onChange={onQueryFunctionsUpdates}
maxFunctions={isLogsDataSource ? 1 : 3}
/>

View File

@@ -0,0 +1,105 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import LeftToolbarActions from '../LeftToolbarActions';
import RightToolbarActions from '../RightToolbarActions';
describe('ToolbarActions', () => {
it('LeftToolbarActions - renders correctly with default props', async () => {
const handleChangeSelectedView = jest.fn();
const handleToggleShowHistogram = jest.fn();
const { queryByTestId } = render(
<LeftToolbarActions
items={{
search: {
name: 'search',
label: 'Search',
disabled: false,
show: true,
},
queryBuilder: {
name: 'query-builder',
label: 'Query Builder',
disabled: false,
show: true,
},
clickhouse: {
name: 'clickhouse',
label: 'Clickhouse',
disabled: false,
},
}}
selectedView={SELECTED_VIEWS.SEARCH}
onChangeSelectedView={handleChangeSelectedView}
onToggleHistrogramVisibility={handleToggleShowHistogram}
showHistogram
/>,
);
expect(screen.getByTestId('search-view')).toBeInTheDocument();
expect(screen.getByTestId('query-builder-view')).toBeInTheDocument();
// clickhouse should not be present as its show: false
expect(queryByTestId('clickhouse-view')).not.toBeInTheDocument();
await userEvent.click(screen.getByTestId('search-view'));
expect(handleChangeSelectedView).toBeCalled();
await userEvent.click(screen.getByTestId('query-builder-view'));
expect(handleChangeSelectedView).toBeCalled();
});
it('renders - clickhouse view and test histogram toggle', async () => {
const handleChangeSelectedView = jest.fn();
const handleToggleShowHistogram = jest.fn();
const { queryByTestId, getByRole } = render(
<LeftToolbarActions
items={{
search: {
name: 'search',
label: 'Search',
disabled: false,
show: false,
},
queryBuilder: {
name: 'query-builder',
label: 'Query Builder',
disabled: false,
show: true,
},
clickhouse: {
name: 'clickhouse',
label: 'Clickhouse',
disabled: false,
show: true,
},
}}
selectedView={SELECTED_VIEWS.QUERY_BUILDER}
onChangeSelectedView={handleChangeSelectedView}
onToggleHistrogramVisibility={handleToggleShowHistogram}
showHistogram
/>,
);
const clickHouseView = queryByTestId('clickhouse-view');
expect(clickHouseView).toBeInTheDocument();
await userEvent.click(clickHouseView as HTMLElement);
expect(handleChangeSelectedView).toBeCalled();
await userEvent.click(getByRole('switch'));
expect(handleToggleShowHistogram).toBeCalled();
});
it('RightToolbarActions - render correctly with props', async () => {
const onStageRunQuery = jest.fn();
const { queryByText } = render(
<RightToolbarActions onStageRunQuery={onStageRunQuery} />,
);
const stageNRunBtn = queryByText('Stage & Run Query');
expect(stageNRunBtn).toBeInTheDocument();
await userEvent.click(stageNRunBtn as HTMLElement);
expect(onStageRunQuery).toBeCalled();
});
});

View File

@@ -61,7 +61,6 @@
line-height: 18px;
background: transparent;
border-left: 2px solid transparent;
transition: 0.2s all linear;

View File

@@ -278,7 +278,7 @@ function SideNav({
}, [isCloudUserVal, isEnterprise, isFetching]);
useEffect(() => {
if (!isCloudUserVal) {
if (!(isCloudUserVal || isEECloudUser())) {
let updatedMenuItems = [...menuItems];
updatedMenuItems = updatedMenuItems.filter(
(item) => item.key !== ROUTES.INTEGRATIONS,

View File

@@ -13,11 +13,18 @@ function Events({
return <Typography>No events data in selected span</Typography>;
}
const sortedTraceEvents = events.sort((a, b) => {
// Handle undefined names by treating them as empty strings
const nameA = a.name || '';
const nameB = b.name || '';
return nameA.localeCompare(nameB);
});
return (
<ErrorTag
onToggleHandler={onToggleHandler}
setText={setText}
event={events}
event={sortedTraceEvents}
firstSpanStartTime={firstSpanStartTime}
/>
);

View File

@@ -41,8 +41,9 @@ function Tags({
setSearchText(value);
};
const filteredTags = tags.filter((tag) => tag.key.includes(searchText));
const filteredTags = tags
.filter((tag) => tag.key.includes(searchText))
.sort((a, b) => a.key.localeCompare(b.key));
if (tags.length === 0) {
return <Typography>No tags in selected span</Typography>;
}

View File

@@ -5,4 +5,5 @@ export const Container = styled.div`
align-items: center;
justify-content: flex-end;
gap: 0.3rem;
margin: 8px 0;
`;

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