Compare commits

...

63 Commits

Author SHA1 Message Date
Prashant Shahi
622e1765cf Merge branch 'develop' into release/v0.15.0 2023-01-31 16:21:38 +05:30
Amol Umbark
faaf0a6e73 fix: solved re-render issue when input fields were edited (#2149)
Co-authored-by: mindhash <mindhash@mindhashs-MacBook-Pro.local>
2023-01-31 14:46:03 +05:30
Prashant Shahi
4542a51531 Merge branch 'main' into release/v0.15.0 2023-01-31 00:56:31 +05:30
Prashant Shahi
191a538430 chore: 📌 pin versions: SigNoz 0.15.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-31 00:22:47 +05:30
Prashant Shahi
e6ce80213b Merge branch 'develop' into release/v0.15.0 2023-01-31 00:20:08 +05:30
Palash Gupta
3115b32dcd fix: interval is blocked for custom time selection (#2146)
* fix: interval is blocked for custom time selection

* fix: custom is updated

* chore: selectedTime is updated in hidden logic
2023-01-30 19:27:13 +05:30
Chintan Sudani
af272a368b fix: added lazy loading on dashboard (#2133)
* fix: Removed Strict mode to stop render twice

* fix: added lazy loading on dashboard

* fix: suggested changes

* fix: added react-intersection-observer changes

* fix: resolved multiple time api call issue

* chore: variable name is updated

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-30 18:32:05 +05:30
Palash Gupta
b336a6cb45 fix: widget options are now opening (#2141) 2023-01-30 18:06:49 +05:30
Fellipe Montes
b72815ca2f FIX: Exported dashboard include response of the queries #1981 (#2052)
* clear the queryData
* avoid creation of inline func and move logic to utils
* remove console.log
* fix
2023-01-30 16:07:23 +05:30
Amol Umbark
ed4a01dea6 fix: log issue remove field in query panel (#2130) 2023-01-27 13:27:59 +05:30
Vishal Sharma
1914c3b4a0 chore: update install message in install.sh script (#2131) 2023-01-27 12:53:23 +05:30
Prashant Shahi
3811e96e23 chore: 📌 pin versions: SigNoz OtelCollector 0.66.3 in standalone Docker
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-27 11:11:25 +05:30
Prashant Shahi
8d16493432 chore: 📌 pin versions: SigNoz OtelCollector 0.66.3
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-26 14:30:18 +05:30
Vishal Sharma
db2bfbb887 fix: tag filter query builder (#2125) 2023-01-26 01:18:19 +05:30
Chintan Sudani
213838a021 fix: Chart loaders on reload and change of time interval at dashboard (#2068)
* fix: Chart data logic on dashboard reloads

* fix: linting issues

* fix: added right side loader & css config

* fix: loader condition change

* fix: linting issues

* fix: error state of API

* fix: Resolved suggested changes

* fix: Error state for API Failed

* fix: Default loading state

* fix: linting issues

* fix: Suggested changes

* feat: Added common hook for previous value

* chore: usePrevious is made type safety

* chore: chart data set is updated

* chore: removed eslint rule

* fix: commitlint issue on commit

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2023-01-25 20:31:42 +05:30
Pranay Prateek
fd6f9a90e1 removing repostats workflow (#2053)
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Prashant Shahi <prashant@signoz.io>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-01-25 20:13:57 +05:30
Prashant Shahi
13f9922c53 chore(frontend): 🔧 support ARM and copy yarnrc in Dockerfile (#2119)
Signed-off-by: Prashant Shahi <prashant@signoz.io>

Signed-off-by: Prashant Shahi <prashant@signoz.io>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-25 19:58:44 +05:30
Chintan Sudani
a654baaa5b fix: graph flickering issue on trace page (#2120)
* fix: Removed Strict mode to stop render twice

* fix: graph flickering issue on trace page
2023-01-25 19:55:33 +05:30
Palash Gupta
f766435acc feat: popover is added in the trace tag search (#2118)
* feat: popover is updated

* chore: arrow is removed and padding is removed

* chore: width is updated
2023-01-25 18:56:15 +05:30
Marius Kimmina
d7a65ba689 chore: remove not needed code comments (#2054)
Signed-off-by: Marius Kimmina <mar.kimmina@gmail.com>

Signed-off-by: Marius Kimmina <mar.kimmina@gmail.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-25 16:10:22 +05:30
Vishal Sharma
05ce03e67d feat: tag filtering frontend changes (#2116)
feat: tag filtering frontend changes
2023-01-25 15:20:27 +05:30
Palash Gupta
ba6818f487 fix: total count is usage explorer (#2117)
* fix: total count is usage explorer

* chore: no spans found is also wrapped under typography
2023-01-25 14:55:39 +05:30
Srikanth Chekuri
ca53136cbf feat(ui): dashboard variable chaining (#2037)
* feat: dashboard variable chaining

* feat(ui): dashboard variable chaining

* chore: update vars loading

* chore: fix lint

* chore: better dependent vars

* chore: multi dependent variables

* chore: add more user friendly error

* chore: review comments

* chore: address review comments

* chore: remove string assertion

* chore: fix build by updating types

* chore: fix the variable data auto loading

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-01-25 13:22:57 +05:30
Vishal Sharma
c46bef321c feat: tag filter backend changes (#2115) 2023-01-25 12:35:44 +05:30
Palash Gupta
ba8f804b26 fix: yarnrc is added in the root of the frontend (#2114)
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2023-01-25 12:11:22 +05:30
Chintan Sudani
6cc7025e37 fix: Chart is not updating on change of variables (#2020)
* fix: Chart is not updating onchange of variables

* fix: Added useLocation hook for pathname

* fix: Lint issues resolved

* fix: Updated logic behind change of variables

* fix: Suggested changes of variable

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-25 10:54:36 +05:30
Palash Gupta
e62e541fc4 FE: added more eslint rule (#2090)
* chore: arrow-body-style func-style is added in the rule

* fix: linting issues fixed

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2023-01-24 18:53:04 +05:30
Palash Gupta
2f1ca93eda fix: tags is grabbed from the local state (#2106) 2023-01-24 17:42:48 +05:30
Priyanka Chakraborty
f1c7d72fc5 1375 overview querybuilder (#1983) 2023-01-24 09:30:26 +05:30
Chintan Sudani
a405307c96 fix: Redundant call on resize or rearrange layout on dashboard (#2099)
* fix: Removed Strict mode to stop render twice

* fix: Redundant call on resize or rearrange layout on dashboard

* fix: Resolved suggested changes

* fix: Resolved suggested changes

* chore: some of the refactoring is updated

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-23 20:21:24 +05:30
Ram S Gupta
c85d48d7fa remove no-shadow:off rules from eslint rule list (#2093)
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-23 17:15:18 +05:30
Chintan Sudani
75470f6bb9 fix: Removed Strict mode to stop render twice (#2097) 2023-01-23 17:01:45 +05:30
Palash Gupta
f75e688b32 feat: text is handled under light and dark mode (#2087) 2023-01-23 10:40:27 +05:30
Vishal Sharma
5f3ca045df fix: dockerfile clickhouse indentation issue (#2083) 2023-01-20 00:09:46 +05:30
Chintan Sudani
186632af69 fix: Changed Legends UI & Scrollable (#2078)
* fix: Changed Legends UI & Scrollable

* fix: Changed axis label color

* fix: Changed Legends UI & Scrollable

* chore: Removed other issues changes

* fix: linting issues

* fix: changed fontsize of legend

* fix: changed height of legend

* chore: px is updated to rem

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-18 19:53:45 +05:30
Chintan Sudani
fa652be926 fix: Changed axis label color (#2080)
* fix: Changed axis label color

* fix: linting issues

* chore: helpers is updated

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-18 19:40:15 +05:30
volodfast
1e39131c38 feat: drag select timeframe on charts (#2018)
* feat: add drag select functionality to chart

* fix: use redux stored values for time frame selection

* fix: ignore clicks on chart without dragging

* feat: add intersection cursor to chart

* refactor: update drag-select chart plugin

* fix: respond to drag-select mouseup outside of chart

* fix: remove unnecessary chart update

* feat: add drag-select to dashboard charts

* refactor: add util functions to create custom plugin options

* fix: enable custom chart plugins

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Ankit Nayan <ankit@signoz.io>
2023-01-17 17:00:34 +05:30
Ankit Nayan
153e859ac3 Fix/analytics (#2049)
* fix: incorrect calculation
* chore: adding nil check

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-17 15:28:58 +05:30
Fellipe Montes
d1cc29e118 Create: Widget Header in the Loading State #2042 (#2048)
* create a visual loading state with header

* updates loading with WidgetHeader component

* chore: onview and ondelete is updated

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-17 11:37:30 +05:30
Priyanka Chakraborty
972bf94dd0 refactor: tagFilteritems-refactored (#2056)
* refactor: tagFilteritems-refactored

* refactor: wrapper-over-getwidget

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-16 18:05:13 +05:30
Palash Gupta
3632208d45 Fix: live tail memory (#2033)
* feat: react is updated to v18

* feat: logs card is updated
2023-01-16 17:56:46 +05:30
Srikanth Chekuri
cd9768c738 feat: dashboard variable chaining (#2036) 2023-01-16 14:57:04 +05:30
Pranay Prateek
f01b9605db Update README.md 2023-01-16 13:03:15 +05:30
GitStart
eec236af50 Add visual feedback on Copy JSON in Log filter page (#2055)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: gitstart_bot <gitstart_bot@users.noreply.github.com>
2023-01-16 12:03:35 +05:30
Fellipe Montes
bbff2b459e Fix: Invite links do not work if name is not given when creating the invite #2008 (#2026) 2023-01-13 21:37:36 +05:30
Chintan Sudani
d9535e7a8d fix: Trigger Save layout only on title (#2039)
* fix: Trigger Save layout only on title

* chore: code improvement

* fix: Lint issues resolved

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-13 17:29:51 +05:30
volodfast
a82bbe1a72 chore: update chartjs to version 3.9.1 (#2041) 2023-01-13 17:07:28 +05:30
Fellipe Montes
6812f55152 change CSS and isEllipsed variable (#2035)
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-13 14:07:23 +05:30
Chintan Sudani
83163c17cd fix: Added Extra color code to stop repeat same color (#2015) 2023-01-13 13:50:11 +05:30
Palash Gupta
5ed7c9a46e feat: react is updated to v18 (#2030) 2023-01-13 12:01:46 +05:30
Ankit Nayan
2f323056d0 Merge pull request #2034 from SigNoz/release/v0.14.0
Release/v0.14.0
2023-01-12 18:55:14 +05:30
Prashant Shahi
51b583480b chore: 📌 pin versions: SigNoz 0.14.0, SigNoz OtelCollector 0.66.2
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-12 17:56:29 +05:30
Srikanth Chekuri
7b1e2c8b98 fix: use target arch amd64 (#2027) 2023-01-12 11:27:48 +05:30
Srikanth Chekuri
b87f3bdb50 fix: query builder formula fails to eval (#1999)
* fix: query builder formula fails to eval

* fix: result label set without reference

* chore: update tests

Co-authored-by: Prashant Shahi <prashant@signoz.io>
2023-01-11 16:12:47 +05:30
Palash Gupta
2f5908a3dd feat: antd is updated from v4 to v5 (#2012)
* feat: v5 is in progress

* feat: antdv5 is updated

* fix: build is fixed

* fix: default config is over written by custom one

* chore: onchange handler is updated

* chore: overflow is hidden in the layout

* Update index.tsx

* fix: import is fixed

* chore: un used import is fixed

* fix: dark mode is updated in service map

* fix: config dropdown is updated

* fix: logs types is updated

* fix: copy clipboard notification is updated

* chore: layout changes are updated

* chore: colors is updated

* chore: action width is updated

Co-authored-by: Pranay Prateek <pranay@signoz.io>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-01-11 14:39:06 +05:30
Yash Joshi
ca77820e9d refactor: use antd form in organization display name (#2006)
* refactor: use antd form in organization display name

* chore: interface is now named interface

Co-authored-by: Palash <palashgdev@gmail.com>
2023-01-11 00:59:45 +05:30
Marius Kimmina
a4346a2d93 fix(FE): show no No Data on default Dashboards (#2003)
* fix(FE): show no No Data on default Dashboards

Signed-off-by: Marius Kimmina <mar.kimmina@gmail.com>

* chore: removed un used styles

Signed-off-by: Marius Kimmina <mar.kimmina@gmail.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-01-10 23:54:14 +05:30
Srikanth Chekuri
44360ecacf Add support for histogram quantiles (#1533) 2023-01-10 21:42:44 +05:30
Srikanth Chekuri
b675c3cfec fix: add signoz.collector.id to spanmetrics dimensions (#2001)
* fix: add service.instance.id to spanmetrics dimensions

* chore: update description

* chore: update the resource key
2023-01-10 19:21:17 +05:30
Marius Kimmina
b23d8da96c style: use 'no data' for empty graphs (#2002)
* style: use 'No Data' for empty graphs

* style: use 'No data' for empty graphs

Signed-off-by: Marius Kimmina <mar.kimmina@gmail.com>

Signed-off-by: Marius Kimmina <mar.kimmina@gmail.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-01-10 10:44:59 +05:30
Ankit Nayan
215ea8d819 chore: different ticker interval for active user 2023-01-08 23:12:02 +05:30
Ankit Nayan
0c27d5acbc chore: better error handling 2023-01-08 22:49:11 +05:30
Ankit Nayan
435d74c37e Merge pull request #1996 from SigNoz/release/v0.13.1
Release/v0.13.1
2023-01-07 20:56:08 +05:30
265 changed files with 9983 additions and 7552 deletions

View File

@@ -32,6 +32,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
shell: bash
run: |
make test
- name: Build query-service image
shell: bash
run: |

View File

@@ -1,25 +0,0 @@
on:
schedule:
# Run this once per day, towards the end of the day for keeping the most
# recent data point most meaningful (hours are interpreted in UTC).
- cron: "0 8 * * *"
workflow_dispatch: # Allow for running this manually.
jobs:
j1:
name: repostats
runs-on: ubuntu-latest
steps:
- name: run-ghrs
uses: jgehrcke/github-repo-stats@v1.1.0
with:
# Define the stats repository (the repo to fetch
# stats for and to generate the report for).
# Remove the parameter when the stats repository
# and the data repository are the same.
repository: signoz/signoz
# Set a GitHub API token that can read the stats
# repository, and that can push to the data
# repository (which this workflow file lives in),
# to store data and the report files.
ghtoken: ${{ github.token }}

View File

@@ -54,7 +54,7 @@ build-push-frontend:
@echo "--> Building and pushing frontend docker image"
@echo "------------------"
@cd $(FRONTEND_DIRECTORY) && \
docker buildx build --file Dockerfile --progress plane --push --platform linux/amd64 \
docker buildx build --file Dockerfile --progress plane --push --platform linux/arm64,linux/amd64 \
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
# Steps to build and push docker image of query service
@@ -135,3 +135,6 @@ clear-standalone-data:
clear-swarm-data:
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*"
test:
go test ./pkg/query-service/app/metrics/...

View File

@@ -23,7 +23,7 @@
##
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. SigNoz uses distributed tracing to gain visibility into your software stack.
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. With SigNoz, you can:
👉 Visualise Metrics, Traces and Logs in a single pane of glass

View File

@@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.13.1
image: signoz/query-service:0.15.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.13.1
image: signoz/frontend:0.15.0
deploy:
restart_policy:
condition: on-failure
@@ -179,7 +179,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.66.1
image: signoz/signoz-otel-collector:0.66.3
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -207,7 +207,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.66.1
image: signoz/signoz-otel-collector:0.66.3
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@@ -86,6 +86,10 @@ processors:
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: 'signoz.collector.id'
# memory_limiter:
# # 80% of maximum memory up to 2G
# limit_mib: 1500

View File

@@ -905,7 +905,8 @@
<dictionaries_config>*_dictionary.xml</dictionaries_config>
<!-- Configuration of user defined executable functions -->
<user_defined_executable_functions_config>*_function.xml</user_defined_executable_functions_config>
<user_defined_executable_functions_config>*function.xml</user_defined_executable_functions_config>
<user_scripts_path>/var/lib/clickhouse/user_scripts/</user_scripts_path>
<!-- Uncomment if you want data to be compressed 30-100% better.
Don't do that if you just started using ClickHouse.

View File

@@ -0,0 +1,21 @@
<functions>
<function>
<type>executable</type>
<name>histogramQuantile</name>
<return_type>Float64</return_type>
<argument>
<type>Array(Float64)</type>
<name>buckets</name>
</argument>
<argument>
<type>Array(Float64)</type>
<name>counts</name>
</argument>
<argument>
<type>Float64</type>
<name>quantile</name>
</argument>
<format>CSV</format>
<command>./histogramQuantile</command>
</function>
</functions>

View File

@@ -41,7 +41,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: otel-collector
image: signoz/signoz-otel-collector:0.66.1
image: signoz/signoz-otel-collector:0.66.3
command: ["--config=/etc/otel-collector-config.yaml"]
# user: root # required for reading docker container logs
volumes:
@@ -67,7 +67,7 @@ services:
otel-collector-metrics:
container_name: otel-collector-metrics
image: signoz/signoz-otel-collector:0.66.1
image: signoz/signoz-otel-collector:0.66.3
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@@ -97,9 +97,11 @@ services:
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
- ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/
- ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-2:
# <<: *clickhouse-defaults
@@ -112,9 +114,12 @@ services:
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-2/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-3:
# <<: *clickhouse-defaults
@@ -127,9 +132,11 @@ services:
# volumes:
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./custom-function.xml:/etc/clickhouse-server/custom-function.xml
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-3/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.0-0.2}
@@ -146,7 +153,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.13.1}
image: signoz/query-service:${DOCKER_TAG:-0.15.0}
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@@ -174,7 +181,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.13.1}
image: signoz/frontend:${DOCKER_TAG:-0.15.0}
container_name: frontend
restart: on-failure
depends_on:
@@ -186,7 +193,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -211,7 +218,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@@ -83,6 +83,10 @@ processors:
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: 'signoz.collector.id'
# memory_limiter:
# # 80% of maximum memory up to 2G
# limit_mib: 1500

View File

@@ -0,0 +1,237 @@
package main
import (
"bufio"
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
)
// NOTE: executable must be built with target OS and architecture set to linux/amd64
// env GOOS=linux GOARCH=amd64 go build -o histogramQuantile histogramQuantile.go
// The following code is adapted from the following source:
// https://github.com/prometheus/prometheus/blob/main/promql/quantile.go
type bucket struct {
upperBound float64
count float64
}
// buckets implements sort.Interface.
type buckets []bucket
func (b buckets) Len() int { return len(b) }
func (b buckets) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b buckets) Less(i, j int) bool { return b[i].upperBound < b[j].upperBound }
// bucketQuantile calculates the quantile 'q' based on the given buckets. The
// buckets will be sorted by upperBound by this function (i.e. no sorting
// needed before calling this function). The quantile value is interpolated
// assuming a linear distribution within a bucket. However, if the quantile
// falls into the highest bucket, the upper bound of the 2nd highest bucket is
// returned. A natural lower bound of 0 is assumed if the upper bound of the
// lowest bucket is greater 0. In that case, interpolation in the lowest bucket
// happens linearly between 0 and the upper bound of the lowest bucket.
// However, if the lowest bucket has an upper bound less or equal 0, this upper
// bound is returned if the quantile falls into the lowest bucket.
//
// There are a number of special cases (once we have a way to report errors
// happening during evaluations of AST functions, we should report those
// explicitly):
//
// If 'buckets' has 0 observations, NaN is returned.
//
// If 'buckets' has fewer than 2 elements, NaN is returned.
//
// If the highest bucket is not +Inf, NaN is returned.
//
// If q==NaN, NaN is returned.
//
// If q<0, -Inf is returned.
//
// If q>1, +Inf is returned.
func bucketQuantile(q float64, buckets buckets) float64 {
if math.IsNaN(q) {
return math.NaN()
}
if q < 0 {
return math.Inf(-1)
}
if q > 1 {
return math.Inf(+1)
}
sort.Sort(buckets)
if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) {
return math.NaN()
}
buckets = coalesceBuckets(buckets)
ensureMonotonic(buckets)
if len(buckets) < 2 {
return math.NaN()
}
observations := buckets[len(buckets)-1].count
if observations == 0 {
return math.NaN()
}
rank := q * observations
b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })
if b == len(buckets)-1 {
return buckets[len(buckets)-2].upperBound
}
if b == 0 && buckets[0].upperBound <= 0 {
return buckets[0].upperBound
}
var (
bucketStart float64
bucketEnd = buckets[b].upperBound
count = buckets[b].count
)
if b > 0 {
bucketStart = buckets[b-1].upperBound
count -= buckets[b-1].count
rank -= buckets[b-1].count
}
return bucketStart + (bucketEnd-bucketStart)*(rank/count)
}
// coalesceBuckets merges buckets with the same upper bound.
//
// The input buckets must be sorted.
func coalesceBuckets(buckets buckets) buckets {
last := buckets[0]
i := 0
for _, b := range buckets[1:] {
if b.upperBound == last.upperBound {
last.count += b.count
} else {
buckets[i] = last
last = b
i++
}
}
buckets[i] = last
return buckets[:i+1]
}
// The assumption that bucket counts increase monotonically with increasing
// upperBound may be violated during:
//
// * Recording rule evaluation of histogram_quantile, especially when rate()
// has been applied to the underlying bucket timeseries.
// * Evaluation of histogram_quantile computed over federated bucket
// timeseries, especially when rate() has been applied.
//
// This is because scraped data is not made available to rule evaluation or
// federation atomically, so some buckets are computed with data from the
// most recent scrapes, but the other buckets are missing data from the most
// recent scrape.
//
// Monotonicity is usually guaranteed because if a bucket with upper bound
// u1 has count c1, then any bucket with a higher upper bound u > u1 must
// have counted all c1 observations and perhaps more, so that c >= c1.
//
// Randomly interspersed partial sampling breaks that guarantee, and rate()
// exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
// 4 samples but the bucket with le=2000 has a count of 7 from 3 samples. The
// monotonicity is broken. It is exacerbated by rate() because under normal
// operation, cumulative counting of buckets will cause the bucket counts to
// diverge such that small differences from missing samples are not a problem.
// rate() removes this divergence.)
//
// bucketQuantile depends on that monotonicity to do a binary search for the
// bucket with the φ-quantile count, so breaking the monotonicity
// guarantee causes bucketQuantile() to return undefined (nonsense) results.
//
// As a somewhat hacky solution until ingestion is atomic per scrape, we
// calculate the "envelope" of the histogram buckets, essentially removing
// any decreases in the count between successive buckets.
func ensureMonotonic(buckets buckets) {
max := buckets[0].count
for i := 1; i < len(buckets); i++ {
switch {
case buckets[i].count > max:
max = buckets[i].count
case buckets[i].count < max:
buckets[i].count = max
}
}
}
// End of copied code.
func readLines() []string {
r := bufio.NewReader(os.Stdin)
bytes := []byte{}
lines := []string{}
for {
line, isPrefix, err := r.ReadLine()
if err != nil {
break
}
bytes = append(bytes, line...)
if !isPrefix {
str := strings.TrimSpace(string(bytes))
if len(str) > 0 {
lines = append(lines, str)
bytes = []byte{}
}
}
}
if len(bytes) > 0 {
lines = append(lines, string(bytes))
}
return lines
}
func main() {
lines := readLines()
for _, text := range lines {
// Example input
// "[1, 2, 4, 8, 16]", "[1, 5, 8, 10, 14]", 0.9"
// bounds - counts - quantile
parts := strings.Split(text, "\",")
var bucketNumbers []float64
// Strip the ends with square brackets
text = parts[0][2 : len(parts[0])-1]
// Parse the bucket bounds
for _, num := range strings.Split(text, ",") {
num = strings.TrimSpace(num)
number, err := strconv.ParseFloat(num, 64)
if err == nil {
bucketNumbers = append(bucketNumbers, number)
}
}
var bucketCounts []float64
// Strip the ends with square brackets
text = parts[1][2 : len(parts[1])-1]
// Parse the bucket counts
for _, num := range strings.Split(text, ",") {
num = strings.TrimSpace(num)
number, err := strconv.ParseFloat(num, 64)
if err == nil {
bucketCounts = append(bucketCounts, number)
}
}
// Parse the quantile
q, err := strconv.ParseFloat(parts[2], 64)
var b buckets
if err == nil {
for i := 0; i < len(bucketNumbers); i++ {
b = append(b, bucket{upperBound: bucketNumbers[i], count: bucketCounts[i]})
}
}
fmt.Println(bucketQuantile(q, b))
}
}

View File

@@ -511,13 +511,15 @@ else
echo ""
echo -e "🟢 Your frontend is running on http://localhost:3301"
echo ""
echo " By default, retention period is set to 7 days for logs and traces, and 30 days for metrics."
echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n"
echo " To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
echo ""
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
echo ""
echo "👉 Need help Getting Started?"
echo "👉 Need help in Getting Started?"
echo -e "Join us on Slack https://signoz.io/slack"
echo ""
echo -e "\n📨 Please share your email to receive support & updates about SigNoz!"

View File

@@ -30,6 +30,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/healthcheck"
basealm "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
"go.signoz.io/signoz/pkg/query-service/model"
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
rules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/telemetry"
@@ -271,42 +272,42 @@ func (lrw *loggingResponseWriter) Flush() {
func extractDashboardMetaData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFrom := "/api/v2/metrics/query_range"
var requestBody map[string]interface{}
data := map[string]interface{}{}
var postData *model.QueryRangeParamsV2
if path == pathToExtractBodyFrom && (r.Method == "POST") {
bodyBytes, _ := ioutil.ReadAll(r.Body)
r.Body.Close() // must close
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
if r.Body != nil {
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, false
}
r.Body.Close() // must close
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
json.Unmarshal(bodyBytes, &postData)
json.Unmarshal(bodyBytes, &requestBody)
} else {
return nil, false
}
} else {
return nil, false
}
compositeMetricQuery, compositeMetricQueryExists := requestBody["compositeMetricQuery"]
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
signozMetricFound := false
signozMetricNotFound := false
if compositeMetricQueryExists {
signozMetricFound = telemetry.GetInstance().CheckSigNozMetrics(compositeMetricQueryMap)
queryType, queryTypeExists := compositeMetricQueryMap["queryType"]
if queryTypeExists {
data["queryType"] = queryType
}
panelType, panelTypeExists := compositeMetricQueryMap["panelType"]
if panelTypeExists {
data["panelType"] = panelType
if postData != nil {
signozMetricNotFound = telemetry.GetInstance().CheckSigNozMetricsV2(postData.CompositeMetricQuery)
if postData.CompositeMetricQuery != nil {
data["queryType"] = postData.CompositeMetricQuery.QueryType
data["panelType"] = postData.CompositeMetricQuery.PanelType
}
data["datasource"] = postData.DataSource
}
datasource, datasourceExists := requestBody["dataSource"]
if datasourceExists {
data["datasource"] = datasource
}
if !signozMetricFound {
if signozMetricNotFound {
telemetry.GetInstance().AddActiveMetricsUser()
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_DASHBOARDS_METADATA, data, true)
}

View File

@@ -102,9 +102,10 @@ module.exports = {
},
],
'@typescript-eslint/no-unused-vars': 'error',
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
'arrow-body-style': ['error', 'as-needed'],
// eslint rules need to remove
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'off',
'import/no-cycle': 'off',

View File

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

1
frontend/.yarnrc Normal file
View File

@@ -0,0 +1 @@
network-timeout 600000

View File

@@ -9,8 +9,9 @@ ARG TARGETARCH
WORKDIR /frontend
# Copy the package.json to install dependencies
# Copy the package.json and .yarnrc files prior to install dependencies
COPY package.json ./
COPY .yarnrc ./
# Install the dependencies and make the folder
RUN CI=1 yarn install

View File

@@ -27,8 +27,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.6.2",
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@grafana/data": "^8.4.3",
"@monaco-editor/react": "^4.3.1",
"@testing-library/jest-dom": "^5.11.4",
@@ -36,7 +36,7 @@
"@testing-library/user-event": "^12.1.10",
"@welldone-software/why-did-you-render": "^6.2.1",
"@xstate/react": "^3.0.0",
"antd": "4.19.2",
"antd": "5.0.5",
"axios": "^0.21.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.0",
@@ -44,7 +44,7 @@
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-minify": "^0.5.1",
"babel-preset-react-app": "^10.0.0",
"chart.js": "^3.4.0",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"color": "^4.2.1",
@@ -70,16 +70,18 @@
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"mini-css-extract-plugin": "2.4.5",
"react": "17.0.0",
"react-dom": "17.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.1",
"react-intersection-observer": "9.4.1",
"react-query": "^3.34.19",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"react-vis": "^1.11.7",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
@@ -132,8 +134,8 @@
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
@@ -186,7 +188,7 @@
]
},
"resolutions": {
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0"
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,8 @@
import { ConfigProvider } from 'antd';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import AppLayout from 'container/AppLayout';
import { useThemeConfig } from 'hooks/useDarkMode';
import history from 'lib/history';
import React, { Suspense } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
@@ -9,29 +11,31 @@ import PrivateRoute from './Private';
import routes from './routes';
function App(): JSX.Element {
const themeConfig = useThemeConfig();
return (
<Router history={history}>
<PrivateRoute>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => {
return (
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<PrivateRoute>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
);
})}
))}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</PrivateRoute>
</Router>
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</PrivateRoute>
</Router>
</ConfigProvider>
);
}

View File

@@ -1,4 +1,4 @@
import axios from 'api';
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -8,9 +8,7 @@ const query = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/variables/query?query=${encodeURIComponent(props.query)}`,
);
const response = await axios.post(`/variables/query`, props);
return {
statusCode: 200,

View File

@@ -12,7 +12,9 @@ const getSpans = async (
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
Values: e.Values,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
}));
const exclude: string[] = [];

View File

@@ -30,7 +30,9 @@ const getSpanAggregate = async (
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
Values: e.Values,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
}));
const other = Object.fromEntries(props.selectedFilter);

View File

@@ -11,9 +11,11 @@ const getTagValue = async (
const response = await axios.post<PayloadProps>(`/getTagValues`, {
start: props.start.toString(),
end: props.end.toString(),
tagKey: props.tagKey,
tagKey: {
Key: props.tagKey.Key,
Type: props.tagKey.Type,
},
});
return {
statusCode: 200,
error: null,

View File

@@ -1,9 +0,0 @@
import generatePicker from 'antd/es/date-picker/generatePicker';
import { Dayjs } from 'dayjs';
// included in antd
// eslint-disable-next-line import/no-extraneous-dependencies
import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs';
const DatePicker = generatePicker<Dayjs>(dayjsGenerateConfig);
export default DatePicker;

View File

@@ -1,8 +1,6 @@
import MEditor, { EditorProps } from '@monaco-editor/react';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
function Editor({
value,
@@ -12,7 +10,7 @@ function Editor({
height,
options,
}: MEditorProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
return (
<MEditor
theme={isDarkMode ? 'vs-dark' : 'vs-light'}

View File

@@ -0,0 +1,321 @@
import { Chart, ChartTypeRegistry, Plugin } from 'chart.js';
import * as ChartHelpers from 'chart.js/helpers';
// utils
import { ChartEventHandler, mergeDefaultOptions } from './utils';
export const dragSelectPluginId = 'drag-select-plugin';
type ChartDragHandlers = {
mousedown: ChartEventHandler;
mousemove: ChartEventHandler;
mouseup: ChartEventHandler;
globalMouseup: () => void;
};
export type DragSelectPluginOptions = {
color?: string;
onSelect?: (startValueX: number, endValueX: number) => void;
};
const defaultDragSelectPluginOptions: Required<DragSelectPluginOptions> = {
color: 'rgba(0, 0, 0, 0.5)',
onSelect: () => {},
};
export function createDragSelectPluginOptions(
isEnabled: boolean,
onSelect?: (start: number, end: number) => void,
color?: string,
): DragSelectPluginOptions | false {
if (!isEnabled) {
return false;
}
return {
onSelect,
color,
};
}
function createMousedownHandler(
chart: Chart,
dragData: DragSelectData,
): ChartEventHandler {
return (ev): void => {
const { left, right } = chart.chartArea;
let { x: startDragPositionX } = ChartHelpers.getRelativePosition(ev, chart);
if (left > startDragPositionX) {
startDragPositionX = left;
}
if (right < startDragPositionX) {
startDragPositionX = right;
}
const startValuePositionX = chart.scales.x.getValueForPixel(
startDragPositionX,
);
dragData.onDragStart(startDragPositionX, startValuePositionX);
};
}
function createMousemoveHandler(
chart: Chart,
dragData: DragSelectData,
): ChartEventHandler {
return (ev): void => {
if (!dragData.isMouseDown) {
return;
}
const { left, right } = chart.chartArea;
let { x: dragPositionX } = ChartHelpers.getRelativePosition(ev, chart);
if (left > dragPositionX) {
dragPositionX = left;
}
if (right < dragPositionX) {
dragPositionX = right;
}
const valuePositionX = chart.scales.x.getValueForPixel(dragPositionX);
dragData.onDrag(dragPositionX, valuePositionX);
chart.update('none');
};
}
function createMouseupHandler(
chart: Chart,
options: DragSelectPluginOptions,
dragData: DragSelectData,
): ChartEventHandler {
return (ev): void => {
const { left, right } = chart.chartArea;
let { x: endRelativePostionX } = ChartHelpers.getRelativePosition(ev, chart);
if (left > endRelativePostionX) {
endRelativePostionX = left;
}
if (right < endRelativePostionX) {
endRelativePostionX = right;
}
const endValuePositionX = chart.scales.x.getValueForPixel(
endRelativePostionX,
);
dragData.onDragEnd(endRelativePostionX, endValuePositionX);
chart.update('none');
if (
typeof options.onSelect === 'function' &&
typeof dragData.startValuePositionX === 'number' &&
typeof dragData.endValuePositionX === 'number'
) {
const start = Math.min(
dragData.startValuePositionX,
dragData.endValuePositionX,
);
const end = Math.max(
dragData.startValuePositionX,
dragData.endValuePositionX,
);
options.onSelect(start, end);
}
};
}
function createGlobalMouseupHandler(
options: DragSelectPluginOptions,
dragData: DragSelectData,
): () => void {
return (): void => {
const { isDragging, endRelativePixelPositionX, endValuePositionX } = dragData;
if (!isDragging) {
return;
}
dragData.onDragEnd(
endRelativePixelPositionX as number,
endValuePositionX as number,
);
if (
typeof options.onSelect === 'function' &&
typeof dragData.startValuePositionX === 'number' &&
typeof dragData.endValuePositionX === 'number'
) {
const start = Math.min(
dragData.startValuePositionX,
dragData.endValuePositionX,
);
const end = Math.max(
dragData.startValuePositionX,
dragData.endValuePositionX,
);
options.onSelect(start, end);
}
};
}
class DragSelectData {
public isDragging = false;
public isMouseDown = false;
public startRelativePixelPositionX: number | null = null;
public startValuePositionX: number | null | undefined = null;
public endRelativePixelPositionX: number | null = null;
public endValuePositionX: number | null | undefined = null;
public initialize(): void {
this.isDragging = false;
this.isMouseDown = false;
this.startRelativePixelPositionX = null;
this.startValuePositionX = null;
this.endRelativePixelPositionX = null;
this.endValuePositionX = null;
}
public onDragStart(
startRelativePixelPositionX: number,
startValuePositionX: number | undefined,
): void {
this.isDragging = false;
this.isMouseDown = true;
this.startRelativePixelPositionX = startRelativePixelPositionX;
this.startValuePositionX = startValuePositionX;
this.endRelativePixelPositionX = null;
this.endValuePositionX = null;
}
public onDrag(
endRelativePixelPositionX: number,
endValuePositionX: number | undefined,
): void {
this.isDragging = true;
this.endRelativePixelPositionX = endRelativePixelPositionX;
this.endValuePositionX = endValuePositionX;
}
public onDragEnd(
endRelativePixelPositionX: number,
endValuePositionX: number | undefined,
): void {
if (!this.isDragging) {
this.initialize();
return;
}
this.isDragging = false;
this.isMouseDown = false;
this.endRelativePixelPositionX = endRelativePixelPositionX;
this.endValuePositionX = endValuePositionX;
}
}
export const createDragSelectPlugin = (): Plugin<
keyof ChartTypeRegistry,
DragSelectPluginOptions
> => {
const dragData = new DragSelectData();
let pluginOptions: Required<DragSelectPluginOptions>;
const handlers: ChartDragHandlers = {
mousedown: () => {},
mousemove: () => {},
mouseup: () => {},
globalMouseup: () => {},
};
const dragSelectPlugin: Plugin<
keyof ChartTypeRegistry,
DragSelectPluginOptions
> = {
id: dragSelectPluginId,
start: (chart: Chart, _, passedOptions) => {
pluginOptions = mergeDefaultOptions(
passedOptions,
defaultDragSelectPluginOptions,
);
const { canvas } = chart;
dragData.initialize();
const mousedownHandler = createMousedownHandler(chart, dragData);
const mousemoveHandler = createMousemoveHandler(chart, dragData);
const mouseupHandler = createMouseupHandler(chart, pluginOptions, dragData);
const globalMouseupHandler = createGlobalMouseupHandler(
pluginOptions,
dragData,
);
canvas.addEventListener('mousedown', mousedownHandler, { passive: true });
canvas.addEventListener('mousemove', mousemoveHandler, { passive: true });
canvas.addEventListener('mouseup', mouseupHandler, { passive: true });
document.addEventListener('mouseup', globalMouseupHandler, {
passive: true,
});
handlers.mousedown = mousedownHandler;
handlers.mousemove = mousemoveHandler;
handlers.mouseup = mouseupHandler;
handlers.globalMouseup = globalMouseupHandler;
},
beforeDestroy: (chart: Chart) => {
const { canvas } = chart;
if (!canvas) {
return;
}
canvas.removeEventListener('mousedown', handlers.mousedown);
canvas.removeEventListener('mousemove', handlers.mousemove);
canvas.removeEventListener('mouseup', handlers.mouseup);
document.removeEventListener('mouseup', handlers.globalMouseup);
},
afterDatasetsDraw: (chart: Chart) => {
const {
startRelativePixelPositionX,
endRelativePixelPositionX,
isDragging,
} = dragData;
if (startRelativePixelPositionX && endRelativePixelPositionX && isDragging) {
const left = Math.min(
startRelativePixelPositionX,
endRelativePixelPositionX,
);
const right = Math.max(
startRelativePixelPositionX,
endRelativePixelPositionX,
);
const top = chart.chartArea.top - 5;
const bottom = chart.chartArea.bottom + 5;
/* eslint-disable-next-line no-param-reassign */
chart.ctx.fillStyle = pluginOptions.color;
chart.ctx.fillRect(left, top, right - left, bottom - top);
}
},
};
return dragSelectPlugin;
};

View File

@@ -11,7 +11,7 @@ export const emptyGraph = {
ctx.textBaseline = 'middle';
ctx.font = '1.5rem sans-serif';
ctx.fillStyle = `${grey.primary}`;
ctx.fillText('No data to display', width / 2, height / 2);
ctx.fillText('No data', width / 2, height / 2);
ctx.restore();
},
};

View File

@@ -0,0 +1,164 @@
import { Chart, ChartEvent, ChartTypeRegistry, Plugin } from 'chart.js';
import * as ChartHelpers from 'chart.js/helpers';
// utils
import { ChartEventHandler, mergeDefaultOptions } from './utils';
export const intersectionCursorPluginId = 'intersection-cursor-plugin';
export type IntersectionCursorPluginOptions = {
color?: string;
dashSize?: number;
gapSize?: number;
};
export const defaultIntersectionCursorPluginOptions: Required<IntersectionCursorPluginOptions> = {
color: 'white',
dashSize: 3,
gapSize: 3,
};
export function createIntersectionCursorPluginOptions(
isEnabled: boolean,
color?: string,
dashSize?: number,
gapSize?: number,
): IntersectionCursorPluginOptions | false {
if (!isEnabled) {
return false;
}
return {
color,
dashSize,
gapSize,
};
}
function createMousemoveHandler(
chart: Chart,
cursorData: IntersectionCursorData,
): ChartEventHandler {
return (ev: ChartEvent | MouseEvent): void => {
const { left, right, top, bottom } = chart.chartArea;
let { x, y } = ChartHelpers.getRelativePosition(ev, chart);
if (left > x) {
x = left;
}
if (right < x) {
x = right;
}
if (y < top) {
y = top;
}
if (y > bottom) {
y = bottom;
}
cursorData.onMouseMove(x, y);
};
}
function createMouseoutHandler(
cursorData: IntersectionCursorData,
): ChartEventHandler {
return (): void => {
cursorData.onMouseOut();
};
}
class IntersectionCursorData {
public positionX: number | null | undefined;
public positionY: number | null | undefined;
public initialize(): void {
this.positionX = null;
this.positionY = null;
}
public onMouseMove(x: number | undefined, y: number | undefined): void {
this.positionX = x;
this.positionY = y;
}
public onMouseOut(): void {
this.positionX = null;
this.positionY = null;
}
}
export const createIntersectionCursorPlugin = (): Plugin<
keyof ChartTypeRegistry,
IntersectionCursorPluginOptions
> => {
const cursorData = new IntersectionCursorData();
let pluginOptions: Required<IntersectionCursorPluginOptions>;
let mousemoveHandler: (ev: ChartEvent | MouseEvent) => void;
let mouseoutHandler: (ev: ChartEvent | MouseEvent) => void;
const intersectionCursorPlugin: Plugin<
keyof ChartTypeRegistry,
IntersectionCursorPluginOptions
> = {
id: intersectionCursorPluginId,
start: (chart: Chart, _, passedOptions) => {
const { canvas } = chart;
cursorData.initialize();
pluginOptions = mergeDefaultOptions(
passedOptions,
defaultIntersectionCursorPluginOptions,
);
mousemoveHandler = createMousemoveHandler(chart, cursorData);
mouseoutHandler = createMouseoutHandler(cursorData);
canvas.addEventListener('mousemove', mousemoveHandler, { passive: true });
canvas.addEventListener('mouseout', mouseoutHandler, { passive: true });
},
beforeDestroy: (chart: Chart) => {
const { canvas } = chart;
if (!canvas) {
return;
}
canvas.removeEventListener('mousemove', mousemoveHandler);
canvas.removeEventListener('mouseout', mouseoutHandler);
},
afterDatasetsDraw: (chart: Chart) => {
const { positionX, positionY } = cursorData;
const lineDashData = [pluginOptions.dashSize, pluginOptions.gapSize];
if (typeof positionX === 'number' && typeof positionY === 'number') {
const { top, bottom, left, right } = chart.chartArea;
chart.ctx.beginPath();
/* eslint-disable-next-line no-param-reassign */
chart.ctx.strokeStyle = pluginOptions.color;
chart.ctx.setLineDash(lineDashData);
chart.ctx.moveTo(left, positionY);
chart.ctx.lineTo(right, positionY);
chart.ctx.stroke();
chart.ctx.beginPath();
chart.ctx.setLineDash(lineDashData);
/* eslint-disable-next-line no-param-reassign */
chart.ctx.strokeStyle = pluginOptions.color;
chart.ctx.moveTo(positionX, top);
chart.ctx.lineTo(positionX, bottom);
chart.ctx.stroke();
}
},
};
return intersectionCursorPlugin;
};

View File

@@ -22,87 +22,86 @@ const getOrCreateLegendList = (
listContainer.style.height = '100%';
listContainer.style.flexWrap = 'wrap';
listContainer.style.justifyContent = 'center';
listContainer.style.fontSize = '0.75rem';
legendContainer?.appendChild(listContainer);
}
return listContainer;
};
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => {
return {
id: 'htmlLegend',
afterUpdate(chart): void {
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
id: 'htmlLegend',
afterUpdate(chart): void {
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
// Reuse the built-in legendItems generator
const items = get(chart, [
'options',
'plugins',
'legend',
'labels',
'generateLabels',
])
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
chart,
)
: null;
// Reuse the built-in legendItems generator
const items = get(chart, [
'options',
'plugins',
'legend',
'labels',
'generateLabels',
])
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
chart,
)
: null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items?.forEach((item: Record<any, any>, index: number) => {
const li = document.createElement('li');
li.style.alignItems = 'center';
li.style.cursor = 'pointer';
li.style.display = 'flex';
li.style.marginLeft = '10px';
li.style.marginTop = '5px';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items?.forEach((item: Record<any, any>, index: number) => {
const li = document.createElement('li');
li.style.alignItems = 'center';
li.style.cursor = 'pointer';
li.style.display = 'flex';
li.style.marginLeft = '10px';
// li.style.marginTop = '5px';
li.onclick = (): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { type } = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
chart.toggleDataVisibility(index);
} else {
chart.setDatasetVisibility(
item.datasetIndex,
!chart.isDatasetVisible(item.datasetIndex),
);
}
chart.update();
};
// Color box
const boxSpan = document.createElement('span');
boxSpan.style.background = `${item.strokeStyle}` || `${colors[0]}`;
boxSpan.style.borderColor = `${item?.strokeStyle}`;
boxSpan.style.borderWidth = `${item.lineWidth}px`;
boxSpan.style.display = 'inline-block';
boxSpan.style.minHeight = '20px';
boxSpan.style.marginRight = '10px';
boxSpan.style.minWidth = '20px';
boxSpan.style.borderRadius = '50%';
if (item.text) {
// Text
const textContainer = document.createElement('span');
textContainer.style.margin = '0';
textContainer.style.padding = '0';
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
const text = document.createTextNode(item.text);
textContainer.appendChild(text);
li.appendChild(boxSpan);
li.appendChild(textContainer);
ul.appendChild(li);
li.onclick = (): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { type } = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
chart.toggleDataVisibility(index);
} else {
chart.setDatasetVisibility(
item.datasetIndex,
!chart.isDatasetVisible(item.datasetIndex),
);
}
});
},
};
};
chart.update();
};
// Color box
const boxSpan = document.createElement('span');
boxSpan.style.background = `${item.strokeStyle}` || `${colors[0]}`;
boxSpan.style.borderColor = `${item?.strokeStyle}`;
boxSpan.style.borderWidth = `${item.lineWidth}px`;
boxSpan.style.display = 'inline-block';
boxSpan.style.minHeight = '0.75rem';
boxSpan.style.marginRight = '0.5rem';
boxSpan.style.minWidth = '0.75rem';
boxSpan.style.borderRadius = '50%';
if (item.text) {
// Text
const textContainer = document.createElement('span');
textContainer.style.margin = '0';
textContainer.style.padding = '0';
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
const text = document.createTextNode(item.text);
textContainer.appendChild(text);
li.appendChild(boxSpan);
li.appendChild(textContainer);
ul.appendChild(li);
}
});
},
});

View File

@@ -0,0 +1,20 @@
import { ChartEvent } from 'chart.js';
export type ChartEventHandler = (ev: ChartEvent | MouseEvent) => void;
export function mergeDefaultOptions<T extends Record<string, unknown>>(
options: T,
defaultOptions: Required<T>,
): Required<T> {
const sanitizedOptions = { ...options };
Object.keys(options).forEach((key) => {
if (sanitizedOptions[key as keyof T] === undefined) {
delete sanitizedOptions[key as keyof T];
}
});
return {
...defaultOptions,
...sanitizedOptions,
};
}

View File

@@ -0,0 +1,8 @@
import { themeColors } from 'constants/theme';
export const getAxisLabelColor = (currentTheme: string): string => {
if (currentTheme === 'light') {
return themeColors.black;
}
return themeColors.whiteCream;
};

View File

@@ -23,14 +23,25 @@ import {
} from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useCallback, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { hasData } from './hasData';
import { getAxisLabelColor } from './helpers';
import { legend } from './Plugin';
import {
createDragSelectPlugin,
createDragSelectPluginOptions,
dragSelectPluginId,
DragSelectPluginOptions,
} from './Plugin/DragSelect';
import { emptyGraph } from './Plugin/EmptyGraph';
import {
createIntersectionCursorPlugin,
createIntersectionCursorPluginOptions,
intersectionCursorPluginId,
IntersectionCursorPluginOptions,
} from './Plugin/IntersectionCursor';
import { LegendsContainer } from './styles';
import { useXAxisTimeUnit } from './xAxisConfig';
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
@@ -66,9 +77,12 @@ function Graph({
forceReRender,
staticLine,
containerHeight,
onDragSelect,
dragSelectColor,
}: GraphProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
const currentTheme = isDarkMode ? 'dark' : 'light';
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
@@ -92,7 +106,7 @@ function Graph({
}
if (chartRef.current !== null) {
const options: ChartOptions = {
const options: CustomChartOptions = {
animation: {
duration: animate ? 200 : 0,
},
@@ -149,6 +163,15 @@ function Graph({
},
},
},
[dragSelectPluginId]: createDragSelectPluginOptions(
!!onDragSelect,
onDragSelect,
dragSelectColor,
),
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
!!onDragSelect,
currentTheme === 'dark' ? 'white' : 'black',
),
},
layout: {
padding: 0,
@@ -178,6 +201,7 @@ function Graph({
},
},
type: 'time',
ticks: { color: getAxisLabelColor(currentTheme) },
},
y: {
display: true,
@@ -186,6 +210,7 @@ function Graph({
color: getGridColor(),
},
ticks: {
color: getAxisLabelColor(currentTheme),
// Include a dollar sign in the ticks
callback(value) {
return getYAxisFormattedValue(value.toString(), yAxisUnit);
@@ -212,7 +237,13 @@ function Graph({
const chartHasData = hasData(data);
const chartPlugins = [];
if (!chartHasData) chartPlugins.push(emptyGraph);
if (chartHasData) {
chartPlugins.push(createIntersectionCursorPlugin());
chartPlugins.push(createDragSelectPlugin());
} else {
chartPlugins.push(emptyGraph);
}
chartPlugins.push(legend(name, data.datasets.length > 3));
lineChartRef.current = new Chart(chartRef.current, {
@@ -235,6 +266,9 @@ function Graph({
yAxisUnit,
onClickHandler,
staticLine,
onDragSelect,
dragSelectColor,
currentTheme,
]);
useEffect(() => {
@@ -249,6 +283,13 @@ function Graph({
);
}
type CustomChartOptions = ChartOptions & {
plugins: {
[dragSelectPluginId]: DragSelectPluginOptions | false;
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
};
};
interface GraphProps {
animate?: boolean;
type: ChartType;
@@ -261,6 +302,8 @@ interface GraphProps {
forceReRender?: boolean | null | number;
staticLine?: StaticLineProps | undefined;
containerHeight?: string | number;
onDragSelect?: (start: number, end: number) => void;
dragSelectColor?: string;
}
export interface StaticLineProps {
@@ -287,6 +330,8 @@ Graph.defaultProps = {
yAxisUnit: undefined,
forceReRender: undefined,
staticLine: undefined,
containerHeight: '85%',
containerHeight: '90%',
onDragSelect: undefined,
dragSelectColor: undefined,
};
export default Graph;

View File

@@ -1,14 +1,22 @@
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const LegendsContainer = styled.div`
height: 15%;
height: 10%;
* {
::-webkit-scrollbar {
display: none !important;
width: 0.5rem;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: ${themeColors.royalGrey};
border-radius: 0.625rem;
}
::-webkit-scrollbar-thumb:hover {
background: ${themeColors.matterhornGrey};
}
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
`;

View File

@@ -1,29 +1,27 @@
import { Popover } from 'antd';
import React from 'react';
import { notification, Popover } from 'antd';
import React, { useCallback, useEffect } from 'react';
import { useCopyToClipboard } from 'react-use';
interface CopyClipboardHOCProps {
textToCopy: string;
children: React.ReactNode;
}
function CopyClipboardHOC({
textToCopy,
children,
}: CopyClipboardHOCProps): JSX.Element {
const [, setCopy] = useCopyToClipboard();
const [value, setCopy] = useCopyToClipboard();
useEffect(() => {
if (value.value) {
notification.success({
message: 'Copied to clipboard',
});
}
}, [value]);
const onClick = useCallback((): void => {
setCopy(textToCopy);
}, [setCopy, textToCopy]);
return (
<span
style={{
margin: 0,
padding: 0,
cursor: 'pointer',
}}
onClick={(): void => setCopy(textToCopy)}
onKeyDown={(): void => setCopy(textToCopy)}
role="button"
tabIndex={0}
>
<span onClick={onClick} onKeyDown={onClick} role="button" tabIndex={0}>
<Popover
placement="top"
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
@@ -34,4 +32,9 @@ function CopyClipboardHOC({
);
}
interface CopyClipboardHOCProps {
textToCopy: string;
children: React.ReactNode;
}
export default CopyClipboardHOC;

View File

@@ -1,6 +1,6 @@
import { blue, grey, orange } from '@ant-design/colors';
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
import { Button, Divider, Row, Typography } from 'antd';
import { Button, Divider, notification, Row, Typography } from 'antd';
import { map } from 'd3';
import dayjs from 'dayjs';
import { FlatLogData } from 'lib/logs/flatLogData';
@@ -14,7 +14,7 @@ import { ILogsReducer } from 'types/reducer/logs';
import AddToQueryHOC from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC';
import { Container } from './styles';
import { Container, LogContainer, Text, TextContainer } from './styles';
import { isValidLogField } from './util';
interface LogFieldProps {
@@ -23,21 +23,17 @@ interface LogFieldProps {
}
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
return (
<div
style={{
display: 'flex',
overflow: 'hidden',
width: '100%',
}}
>
<Typography.Text type="secondary">{fieldKey}</Typography.Text>
<TextContainer>
<Text ellipsis type="secondary">
{fieldKey}
</Text>
<CopyClipboardHOC textToCopy={fieldValue}>
<Typography.Text ellipsis>
{': '}
{fieldValue}
</Typography.Text>
</CopyClipboardHOC>
</div>
</TextContainer>
);
}
function LogSelectedField({
@@ -93,40 +89,46 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
const handleCopyJSON = (): void => {
setCopy(JSON.stringify(logData, null, 2));
notification.success({
message: 'Copied to clipboard',
});
};
return (
<Container>
<div style={{ maxWidth: '100%' }}>
<div>
<div>
{'{'}
<div style={{ marginLeft: '0.5rem' }}>
<LogGeneralField
fieldKey="log"
fieldValue={flattenLogData.body as never}
/>
{flattenLogData.stream && (
<LogContainer>
<>
<LogGeneralField
fieldKey="stream"
fieldValue={flattenLogData.stream as never}
fieldKey="log"
fieldValue={flattenLogData.body as never}
/>
)}
<LogGeneralField
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
/>
</div>
{flattenLogData.stream && (
<LogGeneralField
fieldKey="stream"
fieldValue={flattenLogData.stream as never}
/>
)}
<LogGeneralField
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
/>
</>
</LogContainer>
{'}'}
</div>
<div>
{map(selected, (field) => {
return isValidLogField(flattenLogData[field.name] as never) ? (
{map(selected, (field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
<LogSelectedField
key={field.name}
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
/>
) : null;
})}
) : null,
)}
</div>
</div>
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />

View File

@@ -1,4 +1,4 @@
import { Card } from 'antd';
import { Card, Typography } from 'antd';
import styled, { keyframes } from 'styled-components';
const fadeInAnimation = keyframes`
@@ -16,3 +16,20 @@ export const Container = styled(Card)`
animation-duration: 0.2s;
animation-timing-function: ease-in;
`;
export const Text = styled(Typography.Text)`
&&& {
min-width: 1.5rem;
white-space: nowrap;
}
`;
export const TextContainer = styled.div`
display: flex;
overflow: hidden;
width: 100%;
`;
export const LogContainer = styled.div`
margin-left: 0.5rem;
`;

View File

@@ -11,7 +11,7 @@ function CustomModal({
return (
<Modal
title={title}
visible={isModalVisible}
open={isModalVisible}
footer={footer}
closable={closable}
>

View File

@@ -48,9 +48,9 @@ function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
(state) => state.app,
);
const c = allComponentMap.find((item) => {
return item.match(path, currentVersion, userFlags);
});
const c = allComponentMap.find((item) =>
item.match(path, currentVersion, userFlags),
);
if (!c) {
return null;

View File

@@ -4,9 +4,9 @@ import React from 'react';
import { SpinerStyle } from './styles';
function Spinner({ size, tip, height }: SpinnerProps): JSX.Element {
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
return (
<SpinerStyle height={height}>
<SpinerStyle height={height} style={style}>
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
</SpinerStyle>
);
@@ -16,11 +16,13 @@ interface SpinnerProps {
size?: SpinProps['size'];
tip?: SpinProps['tip'];
height?: React.CSSProperties['height'];
style?: React.CSSProperties;
}
Spinner.defaultProps = {
size: undefined,
tip: undefined,
height: undefined,
style: {},
};
export default Spinner;

View File

@@ -6,18 +6,16 @@ import React from 'react';
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
return (
<Tooltip
overlay={(): JSX.Element => {
return (
<div>
{`${text} `}
{url && (
<a href={url} rel="noopener noreferrer" target="_blank">
here
</a>
)}
</div>
);
}}
overlay={(): JSX.Element => (
<div>
{`${text} `}
{url && (
<a href={url} rel="noopener noreferrer" target="_blank">
here
</a>
)}
</div>
)}
>
<QuestionCircleFilled style={{ fontSize: '1.3125rem' }} />
</Tooltip>

View File

@@ -8,7 +8,6 @@ export const TextContainer = styled.div<TextContainerProps>`
display: flex;
> button {
margin-left: ${({ noButtonMargin }): string => {
return noButtonMargin ? '0' : '0.5rem';
}}
margin-left: ${({ noButtonMargin }): string =>
noButtonMargin ? '0' : '0.5rem'}
`;

View File

@@ -3,4 +3,5 @@ export enum LOCALSTORAGE {
IS_LOGGED_IN = 'IS_LOGGED_IN',
AUTH_TOKEN = 'AUTH_TOKEN',
REFRESH_AUTH_TOKEN = 'REFRESH_AUTH_TOKEN',
THEME = 'THEME',
}

View File

@@ -8,11 +8,11 @@ export const OperatorConversions: Array<{
{
label: 'IN',
metricValue: '=~',
traceValue: 'in',
traceValue: 'In',
},
{
label: 'Not IN',
metricValue: '!~',
traceValue: 'not in',
traceValue: 'NotIn',
},
];

View File

@@ -0,0 +1,42 @@
const themeColors = {
chartcolors: {
dodgerBlue: '#2F80ED',
mediumOrchid: '#BB6BD9',
seaBuckthorn: '#F2994A',
seaGreen: '#219653',
turquoiseBlue: '#56CCF2',
festivalOrange: '#F2C94C',
silver: '#BDBDBD',
outrageousOrange: '#FF6633',
roseBud: '#FFB399',
magentaPink: '#FF33FF',
canary: '#FFFF99',
deepSkyBlue: '#00B3E6',
goldTips: '#E6B333',
royalBlue: '#3366E6',
avocado: '#999966',
mintGreen: '#99FF99',
chestnut: '#B34D4D',
lima: '#80B300',
olive: '#809900',
beautyBush: '#E6B3B3',
danube: '#6680B3',
oliveDrab: '#66991A',
lavenderRose: '#FF99E6',
electricLime: '#CCFF1A',
radicalRed: '#FF1A66',
harleyOrange: '#E6331A',
turquoise: '#33FFCC',
gladeGreen: '#66994D',
hemlock: '#66664D',
vidaLoca: '#4D8000',
rust: '#B33300',
},
errorColor: '#d32f2f',
royalGrey: '#888888',
matterhornGrey: '#555555',
whiteCream: '#ffffffd5',
black: '#000000',
};
export { themeColors };

View File

@@ -1,5 +1,5 @@
import { Button } from 'antd';
import { NotificationInstance } from 'antd/lib/notification';
import { NotificationInstance } from 'antd/es/notification/interface';
import deleteChannel from 'api/channels/delete';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -10,7 +10,8 @@ import {
Tooltip,
Typography,
} from 'antd';
import { ColumnType } from 'antd/es/table';
import { ColumnType, TablePaginationConfig } from 'antd/es/table';
import { FilterValue, SorterResult } from 'antd/es/table/interface';
import { ColumnsType } from 'antd/lib/table';
import { FilterConfirmProps } from 'antd/lib/table/interface';
import getAll from 'api/errors/getAll';
@@ -30,6 +31,7 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { Exception, PayloadProps } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FilterDropdownExtendsProps } from './types';
import {
extractFilterValues,
getDefaultFilterValue,
@@ -176,41 +178,45 @@ function AllErrors(): JSX.Element {
);
const filterDropdownWrapper = useCallback(
({ setSelectedKeys, selectedKeys, confirm, placeholder, filterKey }) => {
return (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder={placeholder}
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
allowClear
defaultValue={getDefaultFilterValue(
filterKey,
getUpdatedServiceName,
getUpdatedExceptionType,
)}
onPressEnter={handleSearch(confirm, selectedKeys[0], filterKey)}
/>
<Button
type="primary"
onClick={handleSearch(confirm, selectedKeys[0], filterKey)}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
);
},
({
setSelectedKeys,
selectedKeys,
confirm,
placeholder,
filterKey,
}: FilterDropdownExtendsProps) => (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder={placeholder}
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
allowClear
defaultValue={getDefaultFilterValue(
filterKey,
getUpdatedServiceName,
getUpdatedExceptionType,
)}
onPressEnter={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
/>
<Button
type="primary"
onClick={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
),
[getUpdatedExceptionType, getUpdatedServiceName, handleSearch],
);
const onExceptionTypeFilter = useCallback(
(value, record: Exception): boolean => {
const onExceptionTypeFilter: ColumnType<Exception>['onFilter'] = useCallback(
(value: unknown, record: Exception): boolean => {
if (record.exceptionType && typeof value === 'string') {
return record.exceptionType.toLowerCase().includes(value.toLowerCase());
}
@@ -220,7 +226,7 @@ function AllErrors(): JSX.Element {
);
const onApplicationTypeFilter = useCallback(
(value, record: Exception): boolean => {
(value: unknown, record: Exception): boolean => {
if (record.serviceName && typeof value === 'string') {
return record.serviceName.toLowerCase().includes(value.toLowerCase());
}
@@ -343,7 +349,11 @@ function AllErrors(): JSX.Element {
];
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
(paginations, filters, sorter) => {
(
paginations: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<Exception>[] | SorterResult<Exception>,
) => {
if (!Array.isArray(sorter)) {
const { pageSize = 0, current = 0 } = paginations;
const { columnKey = '', order } = sorter;

View File

@@ -0,0 +1,9 @@
import { FilterDropdownProps } from 'antd/es/table/interface';
export interface FilterDropdownExtendsProps {
placeholder: string;
filterKey: string;
confirm: FilterDropdownProps['confirm'];
setSelectedKeys: FilterDropdownProps['setSelectedKeys'];
selectedKeys: FilterDropdownProps['selectedKeys'];
}

View File

@@ -20,15 +20,14 @@ export const urlKey = {
serviceName: 'serviceName',
};
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
return !!(
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy =>
!!(
orderBy === 'serviceName' ||
orderBy === 'exceptionCount' ||
orderBy === 'lastSeen' ||
orderBy === 'firstSeen' ||
orderBy === 'exceptionType'
);
};
export const getOrder = (order: string | null): Order => {
if (isOrder(order)) {
@@ -82,12 +81,9 @@ export const getDefaultOrder = (
return undefined;
};
export const getNanoSeconds = (date: string): string => {
return (
Math.floor(new Date(date).getTime() / 1e3).toString() +
String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0')
);
};
export const getNanoSeconds = (date: string): string =>
Math.floor(new Date(date).getTime() / 1e3).toString() +
String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0');
export const getUpdatePageSize = (pageSize: string | null): number => {
if (pageSize) {

View File

@@ -3,9 +3,10 @@ import styled from 'styled-components';
export const Layout = styled(LayoutComponent)`
&&& {
min-height: 92vh;
display: flex;
position: relative;
min-height: calc(100vh - 4rem);
overflow: hidden;
}
`;

View File

@@ -1,13 +1,12 @@
import { Menu, Space } from 'antd';
import Spinner from 'components/Spinner';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { Suspense, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
import AppReducer from 'types/reducer/app';
import ErrorLink from './ErrorLink';
import LinkContainer from './Link';
import { MenuItem } from './styles';
function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
const sortedConfig = useMemo(
@@ -15,10 +14,10 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
[config.components],
);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
return (
<Menu.ItemGroup>
<Menu>
{sortedConfig.map((item) => {
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
@@ -28,19 +27,19 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
return (
<ErrorLink key={item.text + item.href}>
<Suspense fallback={<Spinner height="5vh" />}>
<Menu.Item>
<MenuItem>
<LinkContainer href={item.href}>
<Space size="small" align="start">
<Component />
{item.text}
</Space>
</LinkContainer>
</Menu.Item>
</MenuItem>
</Suspense>
</ErrorLink>
);
})}
</Menu.ItemGroup>
</Menu>
);
}

View File

@@ -0,0 +1,10 @@
import { Menu } from 'antd';
import styled from 'styled-components';
export const MenuItem = styled(Menu.Item)`
&&& {
height: 1.75rem;
display: flex;
align-items: center;
}
`;

View File

@@ -5,6 +5,7 @@ import {
QuestionCircleOutlined,
} from '@ant-design/icons';
import { Dropdown, Menu, Space } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -15,9 +16,8 @@ import HelpToolTip from './Config';
function DynamicConfigDropdown({
frontendId,
}: DynamicConfigDropdownProps): JSX.Element {
const { configs, isDarkMode } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const { configs } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
const config = useMemo(

View File

@@ -78,16 +78,17 @@ function CreateAlertChannels({
[type, selectedConfig],
);
const prepareSlackRequest = useCallback(() => {
return {
const prepareSlackRequest = useCallback(
() => ({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
};
}, [selectedConfig]);
}),
[selectedConfig],
);
const onSlackHandler = useCallback(async () => {
setSavingState(true);

View File

@@ -47,8 +47,8 @@ function EditAlertChannels({
setType(value as ChannelType);
}, []);
const prepareSlackRequest = useCallback(() => {
return {
const prepareSlackRequest = useCallback(
() => ({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
@@ -56,8 +56,9 @@ function EditAlertChannels({
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,
};
}, [id, selectedConfig]);
}),
[id, selectedConfig],
);
const onSlackEditHandler = useCallback(async () => {
setSavingState(true);
@@ -143,8 +144,8 @@ function EditAlertChannels({
setSavingState(false);
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
const preparePagerRequest = useCallback(() => {
return {
const preparePagerRequest = useCallback(
() => ({
name: selectedConfig.name || '',
routing_key: selectedConfig.routing_key,
client: selectedConfig.client,
@@ -157,8 +158,9 @@ function EditAlertChannels({
details: selectedConfig.details,
detailsArray: JSON.parse(selectedConfig.details || '{}'),
id,
};
}, [id, selectedConfig]);
}),
[id, selectedConfig],
);
const onPagerEditHandler = useCallback(async () => {
setSavingState(true);

View File

@@ -1,5 +1,4 @@
import { Select } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Form, Select } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertDef, Labels } from 'types/api/alerts/def';
@@ -31,7 +30,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>
<FormContainer>
<FormItem
<Form.Item
label={t('field_severity')}
labelAlign="left"
name={['labels', 'severity']}
@@ -54,9 +53,9 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
<Option value="warning">{t('option_warning')}</Option>
<Option value="info">{t('option_info')}</Option>
</SeveritySelect>
</FormItem>
</Form.Item>
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
<Form.Item label={t('field_alert_name')} labelAlign="left" name="alert">
<InputSmall
onChange={(e): void => {
setAlertDef({
@@ -65,8 +64,8 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
});
}}
/>
</FormItem>
<FormItem
</Form.Item>
<Form.Item
label={t('field_alert_desc')}
labelAlign="left"
name={['annotations', 'description']}
@@ -82,7 +81,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
});
}}
/>
</FormItem>
</Form.Item>
<FormItemMedium label={t('field_labels')}>
<LabelSelect
onSetLabels={(l: Labels): void => {

View File

@@ -32,6 +32,8 @@ export const rawQueryToIChQuery = (
// ClickHouseQueryBuilder format. The main difference is
// use of rawQuery (in ClickHouseQueryBuilder)
// and query (in alert builder)
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => {
return { ...src, name: 'A', rawQuery: src.query };
};
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => ({
...src,
name: 'A',
rawQuery: src.query,
});

View File

@@ -211,78 +211,70 @@ function QuerySection({
setFormulaQueries({ ...formulas });
}, [formulaQueries, setFormulaQueries]);
const renderPromqlUI = (): JSX.Element => {
return (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
};
const renderPromqlUI = (): JSX.Element => (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
const renderChQueryUI = (): JSX.Element => {
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
};
const renderChQueryUI = (): JSX.Element => (
<ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />
);
const renderFormulaButton = (): JSX.Element => {
return (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
};
const renderFormulaButton = (): JSX.Element => (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
const renderQueryButton = (): JSX.Element => {
return (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
};
const renderQueryButton = (): JSX.Element => (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
const renderMetricUI = (): JSX.Element => {
return (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
const renderMetricUI = (): JSX.Element => (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = metricQueries[key];
current.name = key;
return (
<MetricsBuilder
key={key}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
/>
);
})}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = metricQueries[key];
const current = formulaQueries[key];
current.name = key;
return (
<MetricsBuilder
<MetricsBuilderFormula
key={key}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
/>
);
})}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
</div>
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
</div>
);
};
</div>
);
const handleRunQuery = (): void => {
runQuery();

View File

@@ -38,106 +38,94 @@ function RuleOptions({
});
};
const renderCompareOps = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
onChange={(value: string | unknown): void => {
const newOp = (value as string) || '';
const renderCompareOps = (): JSX.Element => (
<InlineSelect
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
onChange={(value: string | unknown): void => {
const newOp = (value as string) || '';
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
op: newOp,
},
});
}}
>
<Option value="1">{t('option_above')}</Option>
<Option value="2">{t('option_below')}</Option>
<Option value="3">{t('option_equal')}</Option>
<Option value="4">{t('option_notequal')}</Option>
</InlineSelect>
);
};
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
op: newOp,
},
});
}}
>
<Option value="1">{t('option_above')}</Option>
<Option value="2">{t('option_below')}</Option>
<Option value="3">{t('option_equal')}</Option>
<Option value="4">{t('option_notequal')}</Option>
</InlineSelect>
);
const renderThresholdMatchOpts = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
<Option value="2">{t('option_allthetimes')}</Option>
<Option value="3">{t('option_onaverage')}</Option>
<Option value="4">{t('option_intotal')}</Option>
</InlineSelect>
);
};
const renderThresholdMatchOpts = (): JSX.Element => (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
<Option value="2">{t('option_allthetimes')}</Option>
<Option value="3">{t('option_onaverage')}</Option>
<Option value="4">{t('option_intotal')}</Option>
</InlineSelect>
);
const renderPromMatchOpts = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
</InlineSelect>
);
};
const renderPromMatchOpts = (): JSX.Element => (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
</InlineSelect>
);
const renderEvalWindows = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultEvalWindow}
style={{ minWidth: '120px' }}
value={alertDef.evalWindow}
onChange={(value: string | unknown): void => {
const ew = (value as string) || alertDef.evalWindow;
setAlertDef({
...alertDef,
evalWindow: ew,
});
}}
>
{' '}
<Option value="5m0s">{t('option_5min')}</Option>
<Option value="10m0s">{t('option_10min')}</Option>
<Option value="15m0s">{t('option_15min')}</Option>
<Option value="60m0s">{t('option_60min')}</Option>
<Option value="4h0m0s">{t('option_4hours')}</Option>
<Option value="24h0m0s">{t('option_24hours')}</Option>
</InlineSelect>
);
};
const renderEvalWindows = (): JSX.Element => (
<InlineSelect
defaultValue={defaultEvalWindow}
style={{ minWidth: '120px' }}
value={alertDef.evalWindow}
onChange={(value: string | unknown): void => {
const ew = (value as string) || alertDef.evalWindow;
setAlertDef({
...alertDef,
evalWindow: ew,
});
}}
>
{' '}
<Option value="5m0s">{t('option_5min')}</Option>
<Option value="10m0s">{t('option_10min')}</Option>
<Option value="15m0s">{t('option_15min')}</Option>
<Option value="60m0s">{t('option_60min')}</Option>
<Option value="4h0m0s">{t('option_4hours')}</Option>
<Option value="24h0m0s">{t('option_24hours')}</Option>
</InlineSelect>
);
const renderThresholdRuleOpts = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
</Typography.Text>
</FormItem>
);
};
const renderPromRuleOptions = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderPromMatchOpts()}
</Typography.Text>
</FormItem>
);
};
const renderThresholdRuleOpts = (): JSX.Element => (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
</Typography.Text>
</FormItem>
);
const renderPromRuleOptions = (): JSX.Element => (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderPromMatchOpts()}
</Typography.Text>
</FormItem>
);
return (
<>

View File

@@ -15,154 +15,130 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const renderStep1QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep1QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
</StyledList>
</>
);
const renderStep2QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3QB = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderGuideForQB = (): JSX.Element => {
return (
<>
{renderStep1QB()}
{renderStep2QB()}
{renderStep3QB()}
</>
);
};
const renderStep1PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForQB = (): JSX.Element => (
<>
{renderStep1QB()}
{renderStep2QB()}
{renderStep3QB()}
</>
);
const renderStep1PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
</StyledList>
</>
);
const renderStep2PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3PQL = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderGuideForPQL = (): JSX.Element => {
return (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
};
const renderGuideForPQL = (): JSX.Element => (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
const renderStep1CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
<StyledList>
<StyledListItem>
<Trans
i18nKey="user_guide_ch_step1a"
t={t}
components={[
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
<a
key={1}
target="_blank"
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
/>,
]}
/>
</StyledListItem>
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep1CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
<StyledList>
<StyledListItem>
<Trans
i18nKey="user_guide_ch_step1a"
t={t}
components={[
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
<a
key={1}
target="_blank"
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
/>,
]}
/>
</StyledListItem>
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
</StyledList>
</>
);
const renderStep2CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
</StyledList>
</>
);
const renderStep3CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3CH = (): JSX.Element => (
<>
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
</StyledList>
</>
);
const renderGuideForCH = (): JSX.Element => {
return (
<>
{renderStep1CH()}
{renderStep2CH()}
{renderStep3CH()}
</>
);
};
const renderGuideForCH = (): JSX.Element => (
<>
{renderStep1CH()}
{renderStep2CH()}
{renderStep3CH()}
</>
);
return (
<StyledMainContainer>
<Row>

View File

@@ -1,5 +1,5 @@
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { FormInstance, Modal, notification, Typography } from 'antd';
import { Col, FormInstance, Modal, notification, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import ROUTES from 'constants/routes';
@@ -34,7 +34,6 @@ import {
MainFormContainer,
PanelContainer,
StyledLeftContainer,
StyledRightContainer,
} from './styles';
import useDebounce from './useDebounce';
import UserGuide from './UserGuide';
@@ -437,41 +436,35 @@ function FormAlertRules({
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
);
const renderQBChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name=""
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
};
const renderQBChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name=""
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
const renderPromChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
/>
);
};
const renderPromChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
/>
);
const renderChQueryChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={manualStagedQuery}
userQueryKey={runQueryId}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
};
const renderChQueryChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={manualStagedQuery}
userQueryKey={runQueryId}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
return (
<>
{Element}
@@ -535,9 +528,9 @@ function FormAlertRules({
</ButtonContainer>
</MainFormContainer>
</StyledLeftContainer>
<StyledRightContainer flex="1 1 300px">
<Col flex="1 1 300px">
<UserGuide queryType={queryCategory} />
</StyledRightContainer>
</Col>
</PanelContainer>
</>
);

View File

@@ -4,13 +4,11 @@ import {
} from '@ant-design/icons';
import { useMachine } from '@xstate/react';
import { Button, Input, message, Modal } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { map } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Labels } from 'types/api/alerts/def';
import AppReducer from 'types/reducer/app';
import { v4 as uuid } from 'uuid';
import { ResourceAttributesFilterMachine } from './Labels.machine';
@@ -29,7 +27,8 @@ function LabelSelect({
initialValues,
}: LabelSelectProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const [currentVal, setCurrentVal] = useState('');
const [staging, setStaging] = useState<string[]>([]);
const [queries, setQueries] = useState<ILabelRecord[]>(
@@ -120,17 +119,15 @@ function LabelSelect({
{queries.length > 0 &&
map(
queries,
(query): JSX.Element => {
return (
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
);
},
(query): JSX.Element => (
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
),
)}
</div>
<div>
{map(staging, (item) => {
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
})}
{map(staging, (item) => (
<QueryChipItem key={uuid()}>{item}</QueryChipItem>
))}
</div>
<div style={{ display: 'flex', width: '100%' }}>

View File

@@ -9,19 +9,15 @@ import {
Select,
Typography,
} from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import TextArea from 'antd/lib/input/TextArea';
import styled from 'styled-components';
const { TextArea } = Input;
const { Item } = Form;
export const PanelContainer = styled(Row)`
flex-wrap: nowrap;
`;
export const StyledRightContainer = styled(Col)`
&&& {
}
`;
export const StyledLeftContainer = styled(Col)`
&&& {
margin-right: 1rem;
@@ -111,7 +107,7 @@ export const TextareaMedium = styled(TextArea)`
width: 70%;
`;
export const FormItemMedium = styled(FormItem)`
export const FormItemMedium = styled(Item)`
width: 70%;
`;

View File

@@ -42,17 +42,15 @@ export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
export const toIMetricsBuilderQuery = (
q: IMetricQuery,
): IMetricsBuilderQuery => {
return {
name: q.name,
metricName: q.metricName,
tagFilters: q.tagFilters,
groupBy: q.groupBy,
aggregateOperator: q.aggregateOperator,
disabled: q.disabled,
legend: q.legend,
};
};
): IMetricsBuilderQuery => ({
name: q.name,
metricName: q.metricName,
tagFilters: q.tagFilters,
groupBy: q.groupBy,
aggregateOperator: q.aggregateOperator,
disabled: q.disabled,
legend: q.legend,
});
export const prepareBuilderQueries = (
m: IMetricQueries,

View File

@@ -1,5 +1,5 @@
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React from 'react';
import { toFixed } from 'utils/toFixed';
@@ -14,7 +14,7 @@ interface SpanLengthProps {
function SpanLength(props: SpanLengthProps): JSX.Element {
const { width, leftOffset, bgColor, inMsCount } = props;
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
return (
<SpanWrapper>

View File

@@ -1,10 +1,10 @@
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
import { Col } from 'antd';
import { Col, Typography } from 'antd';
import { StyledCol, StyledRow } from 'components/Styled';
import { IIntervalUnit } from 'container/TraceDetail/utils';
import useThemeMode from 'hooks/useThemeMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { ITraceMetaData } from '..';
@@ -19,6 +19,7 @@ import {
styles,
Wrapper,
} from './styles';
import { getIconStyles } from './utils';
function Trace(props: TraceProps): JSX.Element {
const {
@@ -42,7 +43,8 @@ function Trace(props: TraceProps): JSX.Element {
isMissing,
} = props;
const { isDarkMode } = useThemeMode();
const isDarkMode = useIsDarkMode();
const [isOpen, setOpen] = useState<boolean>(activeSpanPath[level] === id);
const localTreeExpandInteraction = useRef<boolean | 0>(0); // Boolean is for the state of the expansion whereas the number i.e. 0 is for skipping the user interaction.
@@ -111,6 +113,18 @@ function Trace(props: TraceProps): JSX.Element {
const width = (value * 1e2) / (globalSpread * 1e6);
const panelWidth = SPAN_DETAILS_LEFT_COL_WIDTH - level * (16 + 1) - 48;
const iconStyles = useMemo(() => getIconStyles(isDarkMode), [isDarkMode]);
const icon = useMemo(
() =>
isOpen ? (
<CaretDownFilled style={iconStyles} />
) : (
<CaretRightFilled style={iconStyles} />
),
[isOpen, iconStyles],
);
return (
<Wrapper
onMouseEnter={onMouseEnterHandler}
@@ -136,10 +150,8 @@ function Trace(props: TraceProps): JSX.Element {
isDarkMode={isDarkMode}
onClick={onClickTreeExpansion}
>
{totalSpans}
<CaretContainer>
{isOpen ? <CaretDownFilled /> : <CaretRightFilled />}
</CaretContainer>
<Typography>{totalSpans}</Typography>
<CaretContainer>{icon}</CaretContainer>
</CardComponent>
)}
</Col>

View File

@@ -0,0 +1,3 @@
export const getIconStyles = (isDarkMode: boolean): Record<string, string> => ({
color: isDarkMode ? 'white' : 'black',
});

View File

@@ -112,21 +112,19 @@ export const getNodeById = (
const getSpanWithoutChildren = (
span: ITraceTree,
): Omit<ITraceTree, 'children'> => {
return {
id: span.id,
name: span.name,
parent: span.parent,
serviceColour: span.serviceColour,
serviceName: span.serviceName,
startTime: span.startTime,
tags: span.tags,
time: span.time,
value: span.value,
event: span.event,
hasError: span.hasError,
};
};
): Omit<ITraceTree, 'children'> => ({
id: span.id,
name: span.name,
parent: span.parent,
serviceColour: span.serviceColour,
serviceName: span.serviceName,
startTime: span.startTime,
tags: span.tags,
time: span.time,
value: span.value,
event: span.event,
hasError: span.hasError,
});
export const isSpanPresentInSearchString = (
searchedString: string,

View File

@@ -572,7 +572,7 @@ function GeneralSettings({
onOkHandler(category.name.toLowerCase() as TTTLType)
}
centered
visible={category.save.modal}
open={category.save.modal}
confirmLoading={category.save.apiLoading}
>
<Typography>
@@ -590,20 +590,22 @@ function GeneralSettings({
});
return (
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
<>
{Element}
<ErrorTextContainer>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
<ErrorTextContainer>
<TextToolTip
{...{
text: `More details on how to set retention period`,
url: 'https://signoz.io/docs/userguide/retention-period/',
}}
/>
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
<Row justify="start">{renderConfig}</Row>
</Col>
<Row justify="start">{renderConfig}</Row>
</Col>
</>
);
}

View File

@@ -19,6 +19,7 @@ function GridGraphComponent({
name,
yAxisUnit,
staticLine,
onDragSelect,
}: GridGraphComponentProps): JSX.Element | null {
const location = history.location.pathname;
@@ -38,6 +39,7 @@ function GridGraphComponent({
name,
yAxisUnit,
staticLine,
onDragSelect,
}}
/>
);
@@ -85,6 +87,7 @@ export interface GridGraphComponentProps {
name: string;
yAxisUnit?: string;
staticLine?: StaticLineProps;
onDragSelect?: (start: number, end: number) => void;
}
GridGraphComponent.defaultProps = {
@@ -94,6 +97,7 @@ GridGraphComponent.defaultProps = {
onClickHandler: undefined,
yAxisUnit: undefined,
staticLine: undefined,
onDragSelect: undefined,
};
export default GridGraphComponent;

View File

@@ -1,4 +1,4 @@
import { Button, Typography } from 'antd';
import { Button } from 'antd';
import { GraphOnClickHandler } from 'components/Graph';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
@@ -9,7 +9,7 @@ import {
} from 'container/NewWidget/RightContainer/timeItems';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
@@ -19,7 +19,7 @@ import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { NotFoundContainer, TimeContainer } from './styles';
import { TimeContainer } from './styles';
function FullView({
widget,
@@ -27,6 +27,7 @@ function FullView({
onClickHandler,
name,
yAxisUnit,
onDragSelect,
}: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
@@ -57,20 +58,23 @@ function FullView({
}),
);
const isError = response?.error;
const chartDataSet = useMemo(
() =>
getChartData({
queryData: [
{
queryData: response?.data?.payload?.data?.result || [],
},
],
}),
[response],
);
const isLoading = response.isLoading === true;
const errorMessage = isError instanceof Error ? isError?.message : '';
if (isLoading) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
if (isError || !response?.data?.payload?.data?.result) {
return (
<NotFoundContainer>
<Typography>{errorMessage}</Typography>
</NotFoundContainer>
);
}
return (
<>
@@ -94,24 +98,15 @@ function FullView({
)}
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: getChartData({
queryData: [
{
queryData: response.data?.payload?.data?.result
? response.data?.payload?.data?.result
: [],
},
],
}),
isStacked: widget.isStacked,
opacity: widget.opacity,
title: widget.title,
onClickHandler,
name,
yAxisUnit,
}}
GRAPH_TYPES={widget.panelTypes}
data={chartDataSet}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={widget.title}
onClickHandler={onClickHandler}
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
/>
</>
);
@@ -123,12 +118,14 @@ interface FullViewProps {
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
onDragSelect?: (start: number, end: number) => void;
}
FullView.defaultProps = {
fullViewOptions: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
onDragSelect: undefined,
};
export default FullView;

View File

@@ -15,7 +15,7 @@ import GetMaxMinTime from 'lib/getMaxMinTime';
import GetMinMax from 'lib/getMinMax';
import getStartAndEndTime from 'lib/getStartAndEndTime';
import getStep from 'lib/getStep';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -30,6 +30,7 @@ function FullView({
onClickHandler,
name,
yAxisUnit,
onDragSelect,
}: FullViewProps): JSX.Element {
const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector<
AppState,
@@ -80,25 +81,22 @@ function FullView({
const queryLength = widget.query.filter((e) => e.query.length !== 0);
const response = useQueries(
queryLength.map((query) => {
return {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
queryFn: () => {
return getQueryResult({
end: queryMinMax.max.toString(),
query: query.query,
start: queryMinMax.min.toString(),
step: `${getStep({
start: queryMinMax.min,
end: queryMinMax.max,
inputFormat: 's',
})}`,
});
},
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
retryOnMount: false,
};
}),
queryLength.map((query) => ({
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
queryFn: () =>
getQueryResult({
end: queryMinMax.max.toString(),
query: query.query,
start: queryMinMax.min.toString(),
step: `${getStep({
start: queryMinMax.min,
end: queryMinMax.max,
inputFormat: 's',
})}`,
}),
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
retryOnMount: false,
})),
);
const isError =
@@ -117,6 +115,18 @@ function FullView({
})),
);
const chartDataSet = useMemo(
() =>
getChartData({
queryData: data.map((e) => ({
query: e?.map((e) => e.query).join(' ') || '',
queryData: e?.map((e) => e.queryData) || [],
legend: e?.map((e) => e.legend).join('') || '',
})),
}),
[data],
);
if (isLoading) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
@@ -151,22 +161,15 @@ function FullView({
)}
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: getChartData({
queryData: data.map((e) => ({
query: e?.map((e) => e.query).join(' ') || '',
queryData: e?.map((e) => e.queryData) || [],
legend: e?.map((e) => e.legend).join('') || '',
})),
}),
isStacked: widget.isStacked,
opacity: widget.opacity,
title: widget.title,
onClickHandler,
name,
yAxisUnit,
}}
GRAPH_TYPES={widget.panelTypes}
data={chartDataSet}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={widget.title}
onClickHandler={onClickHandler}
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
/>
</>
);
@@ -178,12 +181,14 @@ interface FullViewProps {
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
onDragSelect?: (start: number, end: number) => void;
}
FullView.defaultProps = {
fullViewOptions: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
onDragSelect: undefined,
};
export default FullView;

View File

@@ -1,14 +1,5 @@
import styled from 'styled-components';
export const GraphContainer = styled.div`
min-height: 70vh;
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
width: 100%;
`;
export const NotFoundContainer = styled.div`
display: flex;
justify-content: center;

View File

@@ -1,13 +1,15 @@
import { Typography } from 'antd';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import usePreviousValue from 'hooks/usePreviousValue';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import getChartData from 'lib/getChartData';
import isEmpty from 'lodash-es/isEmpty';
import React, { memo, useCallback, useEffect, useState } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useInView } from 'react-intersection-observer';
import { useQuery } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@@ -20,13 +22,14 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll';
import DashboardReducer from 'types/reducer/dashboards';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LayoutProps } from '..';
import EmptyWidget from '../EmptyWidget';
import WidgetHeader from '../WidgetHeader';
import FullView from './FullView/index.metricsBuilder';
import { ErrorContainer, FullViewContainer, Modal } from './styles';
import { FullViewContainer, Modal } from './styles';
function GridCardGraph({
widget,
@@ -35,13 +38,14 @@ function GridCardGraph({
yAxisUnit,
layout = [],
setLayout,
onDragSelect,
}: GridCardGraphProps): JSX.Element {
const [state, setState] = useState<GridCardGraphState>({
loading: true,
errorMessage: '',
error: false,
payload: undefined,
const { ref: graphRef, inView: isGraphVisible } = useInView({
threshold: 0,
triggerOnce: true,
});
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
const [hovered, setHovered] = useState(false);
const [modal, setModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
@@ -53,113 +57,57 @@ function GridCardGraph({
AppState,
GlobalReducer
>((state) => state.globalTime);
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboard] = dashboards;
const selectedData = selectedDashboard?.data;
const { variables } = selectedData;
// const getMaxMinTime = GetMaxMinTime({
// graphType: widget?.panelTypes,
// maxTime,
// minTime,
// });
// const { start, end } = GetStartAndEndTime({
// type: widget?.timePreferance,
// maxTime: getMaxMinTime.maxTime,
// minTime: getMaxMinTime.minTime,
// });
// const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
// const response = useQueries(
// queryLength?.map((query) => {
// return {
// // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
// queryFn: () => {
// return getQueryResult({
// end,
// query: query?.query,
// start,
// step: '60',
// });
// },
// queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
// retryOnMount: false,
// };
// }),
// );
// const isError =
// response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
// response.some((e) => e.isError === true);
// const isLoading = response.some((e) => e.isLoading === true);
// const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
// const data = response.map((responseOfQuery) =>
// responseOfQuery?.data?.payload?.result.map((e, index) => ({
// query: queryLength[index]?.query,
// queryData: e,
// legend: queryLength[index]?.legend,
// })),
// );
useEffect(() => {
(async (): Promise<void> => {
try {
setState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
const response = await GetMetricQueryRange({
selectedTime: widget.timePreferance,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval,
variables: getDashboardVariables(),
});
const isError = response.error;
if (isError != null) {
setState((state) => ({
...state,
error: true,
errorMessage: isError || 'Something went wrong',
loading: false,
}));
} else {
const chartDataSet = getChartData({
queryData: [
{
queryData: response.payload?.data?.result
? response.payload?.data?.result
: [],
},
],
});
setState((state) => ({
...state,
loading: false,
payload: chartDataSet,
}));
const queryResponse = useQuery(
[
`GetMetricsQueryRange-${widget.timePreferance}-${globalSelectedInterval}-${widget.id}`,
{
widget,
maxTime,
minTime,
globalSelectedInterval,
variables,
},
],
() =>
GetMetricQueryRange({
selectedTime: widget.timePreferance,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval,
variables: getDashboardVariables(),
}),
{
keepPreviousData: true,
enabled: isGraphVisible,
refetchOnMount: false,
onError: (error) => {
if (error instanceof Error) {
setErrorMessage(error.message);
}
} catch (error) {
setState((state) => ({
...state,
error: true,
errorMessage: (error as AxiosError).toString(),
loading: false,
}));
} finally {
setState((state) => ({
...state,
loading: false,
}));
}
})();
}, [widget, maxTime, minTime, globalSelectedInterval]);
},
},
);
const chartData = useMemo(
() =>
getChartData({
queryData: [
{
queryData: queryResponse?.data?.payload?.data?.result || [],
},
],
}),
[queryResponse],
);
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
const onToggleModal = useCallback(
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
@@ -177,70 +125,114 @@ function GridCardGraph({
onToggleModal(setDeleteModal);
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
const getModals = (): JSX.Element => {
return (
<>
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeleteModal)}
visible={deleteModal}
title="Delete"
height="10vh"
onOk={onDeleteHandler}
centered
>
<Typography>Are you sure you want to delete this widget</Typography>
</Modal>
const getModals = (): JSX.Element => (
<>
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeleteModal)}
open={deleteModal}
title="Delete"
height="10vh"
onOk={onDeleteHandler}
centered
>
<Typography>Are you sure you want to delete this widget</Typography>
</Modal>
<Modal
title="View"
footer={[]}
centered
visible={modal}
onCancel={(): void => onToggleModal(setModal)}
width="85%"
destroyOnClose
>
<FullViewContainer>
<FullView
name={`${name}expanded`}
widget={widget}
yAxisUnit={yAxisUnit}
/>
</FullViewContainer>
</Modal>
</>
);
<Modal
title="View"
footer={[]}
centered
open={modal}
onCancel={(): void => onToggleModal(setModal)}
width="85%"
destroyOnClose
>
<FullViewContainer>
<FullView name={`${name}expanded`} widget={widget} yAxisUnit={yAxisUnit} />
</FullViewContainer>
</Modal>
</>
);
const handleOnView = (): void => {
onToggleModal(setModal);
};
const handleOnDelete = (): void => {
onToggleModal(setDeleteModal);
};
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
if (state.error && !isEmptyLayout) {
if (queryResponse.isError && !isEmptyLayout) {
return (
<>
<span ref={graphRef}>
{getModals()}
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
<ErrorContainer>{state.errorMessage}</ErrorContainer>
</>
{!isEmpty(widget) && prevChartDataSetRef && (
<>
<div className="drag-handle">
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={handleOnView}
onDelete={handleOnDelete}
queryResponse={queryResponse}
errorMessage={errorMessage}
/>
</div>
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
data={prevChartDataSetRef}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={' '}
name={name}
yAxisUnit={yAxisUnit}
/>
</>
)}
</span>
);
}
if (
(state.loading === true || state.payload === undefined) &&
!isEmptyLayout
) {
return <Spinner height="20vh" tip="Loading..." />;
if (prevChartDataSetRef?.labels === undefined && queryResponse.isLoading) {
return (
<span ref={graphRef}>
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
<>
<div className="drag-handle">
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={handleOnView}
onDelete={handleOnDelete}
queryResponse={queryResponse}
errorMessage={errorMessage}
/>
</div>
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
data={prevChartDataSetRef}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={' '}
name={name}
yAxisUnit={yAxisUnit}
/>
</>
) : (
<Spinner height="20vh" tip="Loading..." />
)}
</span>
);
}
return (
<span
ref={graphRef}
onMouseOver={(): void => {
setHovered(true);
}}
@@ -255,28 +247,31 @@ function GridCardGraph({
}}
>
{!isEmptyLayout && (
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
<div className="drag-handle">
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={handleOnView}
onDelete={handleOnDelete}
queryResponse={queryResponse}
errorMessage={errorMessage}
/>
</div>
)}
{!isEmptyLayout && getModals()}
{!isEmpty(widget) && !!state.payload && (
{!isEmpty(widget) && !!queryResponse.data?.payload && (
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
name,
yAxisUnit,
}}
GRAPH_TYPES={widget.panelTypes}
data={chartData}
isStacked={widget.isStacked}
opacity={widget.opacity}
title={' '} // `empty title to accommodate absolutely positioned widget header
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
/>
)}
@@ -285,13 +280,6 @@ function GridCardGraph({
);
}
interface GridCardGraphState {
loading: boolean;
error: boolean;
errorMessage: string;
payload: ChartData | undefined;
}
interface DispatchProps {
deleteWidget: ({
widgetId,
@@ -306,8 +294,13 @@ interface GridCardGraphProps extends DispatchProps {
layout?: Layout[];
// eslint-disable-next-line react/require-default-props
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
onDragSelect?: (start: number, end: number) => void;
}
GridCardGraph.defaultProps = {
onDragSelect: undefined,
};
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({

View File

@@ -1,5 +1,6 @@
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React from 'react';
import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux';
@@ -27,7 +28,7 @@ function GraphLayout({
setLayout,
}: GraphLayoutProps): JSX.Element {
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
['save_layout', 'add_panel'],
@@ -71,6 +72,7 @@ function GraphLayout({
useCSSTransforms
allowOverlap={false}
onLayoutChange={onLayoutChangeHandler}
draggableHandle=".drag-handle"
>
{layouts.map(({ Component, ...rest }) => {
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);

View File

@@ -0,0 +1,22 @@
import { themeColors } from 'constants/theme';
const positionCss: React.CSSProperties['position'] = 'fixed';
export const spinnerStyles = { position: positionCss, right: '0.5rem' };
export const tooltipStyles = {
fontSize: '1rem',
top: '0.313rem',
position: positionCss,
right: '0.313rem',
color: themeColors.errorColor,
};
export const errorTooltipPosition = 'top';
export const overlayStyles: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
};

View File

@@ -2,22 +2,33 @@ import {
DeleteOutlined,
DownOutlined,
EditFilled,
ExclamationCircleOutlined,
FullscreenOutlined,
} from '@ant-design/icons';
import { Dropdown, Menu, Typography } from 'antd';
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
import Spinner from 'components/Spinner';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import React, { useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import AppReducer from 'types/reducer/app';
import {
errorTooltipPosition,
overlayStyles,
spinnerStyles,
tooltipStyles,
} from './config';
import {
ArrowContainer,
HeaderContainer,
HeaderContentContainer,
MenuItemContainer,
} from './styles';
type TWidgetOptions = 'view' | 'edit' | 'delete' | string;
@@ -27,6 +38,10 @@ interface IWidgetHeaderProps {
onView: VoidFunction;
onDelete: VoidFunction;
parentHover: boolean;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
>;
errorMessage: string | undefined;
}
function WidgetHeader({
title,
@@ -34,35 +49,49 @@ function WidgetHeader({
onView,
onDelete,
parentHover,
queryResponse,
errorMessage,
}: IWidgetHeaderProps): JSX.Element {
const [localHover, setLocalHover] = useState(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
const onEditHandler = (): void => {
const onEditHandler = useCallback((): void => {
const widgetId = widget.id;
history.push(
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`,
);
};
}, [widget.id, widget.panelTypes]);
const keyMethodMapping: {
[K in TWidgetOptions]: { key: TWidgetOptions; method: VoidFunction };
} = {
view: {
key: 'view',
method: onView,
} = useMemo(
() => ({
view: {
key: 'view',
method: onView,
},
edit: {
key: 'edit',
method: onEditHandler,
},
delete: {
key: 'delete',
method: onDelete,
},
}),
[onDelete, onEditHandler, onView],
);
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
({ key }: { key: TWidgetOptions }): void => {
const functionToCall = keyMethodMapping[key]?.method;
if (functionToCall) {
functionToCall();
setIsOpen(false);
}
},
edit: {
key: 'edit',
method: onEditHandler,
},
delete: {
key: 'delete',
method: onDelete,
},
};
const onMenuItemSelectHandler = ({ key }: { key: TWidgetOptions }): void => {
keyMethodMapping[key]?.method();
};
[keyMethodMapping],
);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [deleteWidget, editWidget] = useComponentPermission(
@@ -70,57 +99,85 @@ function WidgetHeader({
role,
);
const menu = (
<Menu onClick={onMenuItemSelectHandler}>
<Menu.Item key={keyMethodMapping.view.key}>
<MenuItemContainer>
<span>View</span> <FullscreenOutlined />
</MenuItemContainer>
</Menu.Item>
const menuList: MenuItemType[] = useMemo(
() => [
{
key: keyMethodMapping.view.key,
icon: <FullscreenOutlined />,
disabled: queryResponse.isLoading,
label: 'View',
},
{
key: keyMethodMapping.edit.key,
icon: <EditFilled />,
disabled: !editWidget,
label: 'Edit',
},
{
key: keyMethodMapping.delete.key,
icon: <DeleteOutlined />,
disabled: !deleteWidget,
danger: true,
label: 'Delete',
},
],
[
deleteWidget,
editWidget,
keyMethodMapping.delete.key,
keyMethodMapping.edit.key,
keyMethodMapping.view.key,
queryResponse.isLoading,
],
);
{editWidget && (
<Menu.Item key={keyMethodMapping.edit.key}>
<MenuItemContainer>
<span>Edit</span> <EditFilled />
</MenuItemContainer>
</Menu.Item>
)}
const onClickHandler = useCallback(() => {
setIsOpen((open) => !open);
}, []);
{deleteWidget && (
<>
<Menu.Divider />
<Menu.Item key={keyMethodMapping.delete.key} danger>
<MenuItemContainer>
<span>Delete</span> <DeleteOutlined />
</MenuItemContainer>
</Menu.Item>
</>
)}
</Menu>
const menu = useMemo(
() => ({
items: menuList,
onClick: onMenuItemSelectHandler,
}),
[menuList, onMenuItemSelectHandler],
);
return (
<Dropdown
overlay={menu}
trigger={['click']}
overlayStyle={{ minWidth: 100 }}
placement="bottom"
>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}
onMouseOut={(): void => setLocalHover(false)}
hover={localHover}
<div>
<Dropdown
destroyPopupOnHide
open={isOpen}
onOpenChange={setIsOpen}
menu={menu}
trigger={['click']}
overlayStyle={overlayStyles}
>
<HeaderContentContainer onClick={(e): void => e.preventDefault()}>
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
{title}
</Typography.Text>
<ArrowContainer hover={parentHover}>
<DownOutlined />
</ArrowContainer>
</HeaderContentContainer>
</HeaderContainer>
</Dropdown>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}
onMouseOut={(): void => setLocalHover(false)}
hover={localHover}
onClick={onClickHandler}
>
<HeaderContentContainer>
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
{title}
</Typography.Text>
<ArrowContainer hover={parentHover}>
<DownOutlined />
</ArrowContainer>
</HeaderContentContainer>
</HeaderContainer>
</Dropdown>
{queryResponse.isFetching && !queryResponse.isError && (
<Spinner height="5vh" style={spinnerStyles} />
)}
{queryResponse.isError && (
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
<ExclamationCircleOutlined style={tooltipStyles} />
</Tooltip>
)}
</div>
);
}

View File

@@ -1,12 +1,6 @@
import { grey } from '@ant-design/colors';
import styled from 'styled-components';
export const MenuItemContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
export const HeaderContainer = styled.div<{ hover: boolean }>`
width: 100%;
text-align: center;

View File

@@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { AppDispatch } from 'store';
import { UpdateTimeInterval } from 'store/actions';
import {
ToggleAddWidget,
ToggleAddWidgetProps,
@@ -63,12 +65,22 @@ function GridGraph(props: Props): JSX.Element {
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
const { widgets } = data;
const dispatch = useDispatch<Dispatch<AppActions>>();
const dispatch: AppDispatch = useDispatch<Dispatch<AppActions>>();
const [layouts, setLayout] = useState<LayoutProps[]>(
getPreLayouts(widgets, selectedDashboard.data.layout || []),
);
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
},
[dispatch],
);
useEffect(() => {
(async (): Promise<void> => {
if (!isAddWidget) {
@@ -182,13 +194,14 @@ function GridGraph(props: Props): JSX.Element {
yAxisUnit={currentWidget?.yAxisUnit}
layout={layout}
setLayout={setLayout}
onDragSelect={onDragSelect}
/>
),
};
}),
);
},
[widgets],
[widgets, onDragSelect],
);
const onEmptyWidgetHandler = useCallback(async () => {

View File

@@ -3,67 +3,39 @@ import {
CaretUpFilled,
LogoutOutlined,
} from '@ant-design/icons';
import {
Avatar,
Divider,
Dropdown,
Layout,
Menu,
Space,
Typography,
} from 'antd';
import { Divider, Dropdown, Menu, Space, Typography } from 'antd';
import { Logout } from 'api/utils';
import ROUTES from 'constants/routes';
import Config from 'container/ConfigDropdown';
import setTheme, { AppMode } from 'lib/theme/setTheme';
import { useIsDarkMode, useThemeMode } from 'hooks/useDarkMode';
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { ToggleDarkMode } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import AppReducer from 'types/reducer/app';
import CurrentOrganization from './CurrentOrganization';
import ManageLicense from './ManageLicense';
import SignedInAS from './SignedInAs';
import {
AvatarWrapper,
Container,
Header,
IconContainer,
LogoutContainer,
NavLinkWrapper,
ToggleButton,
} from './styles';
function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
const { isDarkMode, user, currentVersion } = useSelector<AppState, AppReducer>(
function HeaderContainer(): JSX.Element {
const { user, currentVersion } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const isDarkMode = useIsDarkMode();
const { toggleTheme } = useThemeMode();
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
const onToggleThemeHandler = useCallback(() => {
const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode';
setTheme(preMode);
const id: AppMode = preMode;
const { head } = document;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = !isDarkMode ? '/css/antd.dark.min.css' : '/css/antd.min.css';
link.media = 'all';
link.id = id;
head.appendChild(link);
link.onload = (): void => {
toggleDarkMode();
const prevNode = document.getElementById('appMode');
prevNode?.remove();
};
}, [toggleDarkMode, isDarkMode]);
const onToggleHandler = useCallback(
(functionToExecute: Dispatch<SetStateAction<boolean>>) => (): void => {
functionToExecute((state) => !state);
@@ -100,15 +72,18 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
);
return (
<Layout.Header>
<Header>
<Container>
<NavLink to={ROUTES.APPLICATION}>
<Space align="center" direction="horizontal">
<NavLinkWrapper>
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
<Typography.Title style={{ margin: 0, color: '#DBDBDB' }} level={4}>
<Typography.Title
style={{ margin: 0, color: 'rgb(219, 219, 219)' }}
level={4}
>
SigNoz
</Typography.Title>
</Space>
</NavLinkWrapper>
</NavLink>
<Space style={{ height: '100%' }} align="center">
@@ -116,7 +91,7 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
<ToggleButton
checked={isDarkMode}
onChange={onToggleThemeHandler}
onChange={toggleTheme}
defaultChecked={isDarkMode}
checkedChildren="🌜"
unCheckedChildren="🌞"
@@ -129,7 +104,7 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
visible={isUserDropDownOpen}
>
<Space>
<Avatar shape="circle">{user?.name[0]}</Avatar>
<AvatarWrapper shape="circle">{user?.name[0]}</AvatarWrapper>
<IconContainer>
{!isUserDropDownOpen ? <CaretDownFilled /> : <CaretUpFilled />}
</IconContainer>
@@ -137,20 +112,8 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
</Dropdown>
</Space>
</Container>
</Layout.Header>
</Header>
);
}
interface DispatchProps {
toggleDarkMode: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
toggleDarkMode: bindActionCreators(ToggleDarkMode, dispatch),
});
type Props = DispatchProps;
export default connect(null, mapDispatchToProps)(HeaderContainer);
export default HeaderContainer;

View File

@@ -1,10 +1,14 @@
import { Switch, Typography } from 'antd';
import { Avatar, Layout, Switch, Typography } from 'antd';
import styled from 'styled-components';
export const Header = styled(Layout.Header)`
background: #1f1f1f !important;
`;
export const Container = styled.div`
display: flex;
justify-content: space-between;
height: 100%;
height: 4rem;
`;
export const AvatarContainer = styled.div`
@@ -66,3 +70,15 @@ export const ToggleButton = styled(Switch)<DarkModeProps>`
export const IconContainer = styled.div`
color: white;
`;
export const NavLinkWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
gap: 0.5rem;
`;
export const AvatarWrapper = styled(Avatar)`
background-color: rgba(255, 255, 255, 0.25);
`;

View File

@@ -1,4 +1,4 @@
import { NotificationInstance } from 'antd/lib/notification/index';
import { NotificationInstance } from 'antd/es/notification/interface';
import deleteAlerts from 'api/alerts/delete';
import { State } from 'hooks/useFetch';
import React, { useState } from 'react';

View File

@@ -1,7 +1,7 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import { notification, Typography } from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import { notification, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
@@ -110,13 +110,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return (
<>
{withOutSeverityKeys.map((e) => {
return (
<StyledTag key={e} color="magenta">
{e}: {value[e]}
</StyledTag>
);
})}
{withOutSeverityKeys.map((e) => (
<StyledTag key={e} color="magenta">
{e}: {value[e]}
</StyledTag>
))}
</>
);
},
@@ -128,22 +126,20 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
title: 'Action',
dataIndex: 'id',
key: 'action',
render: (id: GettableAlert['id'], record): JSX.Element => {
return (
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
render: (id: GettableAlert['id'], record): JSX.Element => (
<>
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
<ColumnButton
onClick={(): void => onEditHandler(id.toString())}
type="link"
>
Edit
</ColumnButton>
<ColumnButton
onClick={(): void => onEditHandler(id.toString())}
type="link"
>
Edit
</ColumnButton>
<DeleteAlert notifications={notifications} setData={setData} id={id} />
</>
);
},
<DeleteAlert notifications={notifications} setData={setData} id={id} />
</>
),
});
}

View File

@@ -40,8 +40,8 @@ function ToggleAlertState({
});
if (response.statusCode === 200) {
setData((state) => {
return state.map((alert) => {
setData((state) =>
state.map((alert) => {
if (alert.id === id) {
return {
...alert,
@@ -50,8 +50,8 @@ function ToggleAlertState({
};
}
return alert;
});
});
}),
);
setAPIStatus((state) => ({
...state,

View File

@@ -131,7 +131,7 @@ function ImportJSON({
return (
<Modal
visible={isImportJSONModalVisible}
open={isImportJSONModalVisible}
centered
maskClosable
destroyOnClose

View File

@@ -5,10 +5,7 @@ import { RefSelectProps } from 'antd/lib/select';
import history from 'lib/history';
import { filter, map } from 'lodash-es';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { v4 as uuidv4 } from 'uuid';
import { DashboardSearchAndFilter } from './Dashboard.machine';
@@ -30,7 +27,6 @@ function SearchFilter({
searchData: Dashboard[];
filterDashboards: (filteredDashboards: Dashboard[]) => void;
}): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const [category, setCategory] = useState<TCategory>();
const [optionsData, setOptionsData] = useState<IOptionsData>(
OptionsSchemas.attribute,
@@ -154,14 +150,8 @@ function SearchFilter({
};
return (
<SearchContainer isDarkMode={isDarkMode}>
<div
style={{
maxWidth: '70%',
display: 'flex',
overflowX: 'auto',
}}
>
<SearchContainer>
<div>
{map(queries, (query) => (
<QueryChip key={query.id} queryData={query} onRemove={removeQueryById} />
))}
@@ -194,16 +184,14 @@ function SearchFilter({
{optionsData.options &&
Array.isArray(optionsData.options) &&
optionsData.options.map(
(optionItem): JSX.Element => {
return (
<Select.Option
key={(optionItem.value as string) || (optionItem.name as string)}
value={optionItem.value || optionItem.name}
>
{optionItem.name}
</Select.Option>
);
},
(optionItem): JSX.Element => (
<Select.Option
key={(optionItem.value as string) || (optionItem.name as string)}
value={optionItem.value || optionItem.name}
>
{optionItem.name}
</Select.Option>
),
)}
</Select>
)}

View File

@@ -2,10 +2,7 @@ import { grey } from '@ant-design/colors';
import { Tag } from 'antd';
import styled from 'styled-components';
export const SearchContainer = styled.div<{
isDarkMode: boolean;
}>`
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
export const SearchContainer = styled.div`
width: 100%;
display: flex;
align-items: center;

View File

@@ -19,9 +19,7 @@ export const convertQueriesToURLQuery = (
export const convertURLQueryStringToQuery = (
queryString: string,
): IQueryStructure[] => {
return JSON.parse(decode(queryString));
};
): IQueryStructure[] => JSON.parse(decode(queryString));
export const resolveOperator = (
result: unknown,

View File

@@ -30,13 +30,11 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
flattenLogData !== null &&
Object.keys(flattenLogData)
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
.map((key) => {
return {
key,
field: key,
value: JSON.stringify(flattenLogData[key]),
};
});
.map((key) => ({
key,
field: key,
value: JSON.stringify(flattenLogData[key]),
}));
if (!dataSource) {
return null;
@@ -45,7 +43,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
const columns = [
{
title: 'Action',
width: 75,
width: 100,
render: (fieldData: Record<string, string>): JSX.Element | null => {
const fieldKey = fieldData.field.split('.').slice(-1);
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {

View File

@@ -1,20 +1,22 @@
import { Drawer, Tabs } from 'antd';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
import JSONView from './JsonView';
import TableView from './TableView';
const { TabPane } = Tabs;
function LogDetailedView(): JSX.Element {
const { detailedLog } = useSelector<AppState, ILogsReducer>(
(state) => state.logs,
);
const dispatch = useDispatch();
const dispatch = useDispatch<Dispatch<AppActions>>();
const onDrawerClose = (): void => {
dispatch({
type: SET_DETAILED_LOG_DATA,
@@ -23,30 +25,25 @@ function LogDetailedView(): JSX.Element {
};
return (
<div style={{}}>
<Drawer
width="60%"
title="Log Details"
placement="right"
closable
mask={false}
onClose={onDrawerClose}
visible={detailedLog !== null}
getContainer={false}
style={{ overscrollBehavior: 'contain' }}
>
{detailedLog && (
<Tabs defaultActiveKey="1">
<TabPane tab="Table" key="1">
<TableView logData={detailedLog} />
</TabPane>
<TabPane tab="JSON" key="2">
<JSONView logData={detailedLog} />
</TabPane>
</Tabs>
)}
</Drawer>
</div>
<Drawer
width="60%"
title="Log Details"
placement="right"
closable
onClose={onDrawerClose}
open={detailedLog !== null}
style={{ overscrollBehavior: 'contain' }}
destroyOnClose
>
<Tabs defaultActiveKey="1">
<Tabs.TabPane tab="Table" key="1">
{detailedLog && <TableView logData={detailedLog} />}
</Tabs.TabPane>
<Tabs.TabPane tab="JSON" key="2">
{detailedLog && <JSONView logData={detailedLog} />}
</Tabs.TabPane>
</Tabs>
</Drawer>
);
}

View File

@@ -7,6 +7,7 @@ import {
import { Button, Popover, Select, Space } from 'antd';
import { LiveTail } from 'api/logs/livetail';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { throttle } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
@@ -19,7 +20,6 @@ import {
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
import AppReducer from 'types/reducer/app';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
@@ -35,7 +35,8 @@ function LogLiveTail(): JSX.Element {
liveTailStartRange,
logs,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@@ -54,22 +55,25 @@ function LogLiveTail(): JSX.Element {
const batchedEventsRef = useRef<Record<string, unknown>[]>([]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const pushLiveLog = useCallback(
throttle(() => {
dispatch({
type: PUSH_LIVE_TAIL_EVENT,
payload: batchedEventsRef.current.reverse(),
});
batchedEventsRef.current = [];
}, 1500),
[],
);
const pushLiveLog = useCallback(() => {
dispatch({
type: PUSH_LIVE_TAIL_EVENT,
payload: batchedEventsRef.current.reverse(),
});
batchedEventsRef.current = [];
}, [dispatch]);
const batchLiveLog = (e: { data: string }): void => {
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
pushLiveLog();
};
const pushLiveLogThrottled = useMemo(() => throttle(pushLiveLog, 1000), [
pushLiveLog,
]);
const batchLiveLog = useCallback(
(e: { data: string }): void => {
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
pushLiveLogThrottled();
},
[pushLiveLogThrottled],
);
// This ref depicts thats whether the live tail is played from paused state or not.
const liveTailSourceRef = useRef<EventSource | null>(null);

View File

@@ -157,18 +157,16 @@ function Login({
}
};
const renderSAMLAction = (): JSX.Element => {
return (
<Button
type="primary"
loading={isLoading}
disabled={isLoading}
href={precheckResult.ssoUrl}
>
{t('login_with_sso')}
</Button>
);
};
const renderSAMLAction = (): JSX.Element => (
<Button
type="primary"
loading={isLoading}
disabled={isLoading}
href={precheckResult.ssoUrl}
>
{t('login_with_sso')}
</Button>
);
const renderOnSsoError = (): JSX.Element | null => {
if (!ssoerror) {

View File

@@ -77,8 +77,8 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getLogsAggregate, maxTime, minTime, liveTail]);
const graphData = useMemo(() => {
return {
const graphData = useMemo(
() => ({
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
@@ -86,8 +86,9 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
backgroundColor: blue[4],
},
],
};
}, [logsAggregate]);
}),
[logsAggregate],
);
return (
<Container>

View File

@@ -1,9 +1,7 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Popover, Spin } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { Field } from './styles';
@@ -26,7 +24,7 @@ export function FieldItem({
iconHoverText,
}: FieldItemProps): JSX.Element {
const [isHovered, setIsHovered] = useState(false);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
return (
<Field
onMouseEnter={(): void => {

View File

@@ -25,9 +25,7 @@ export const Field = styled.div<{ isDarkMode: boolean }>`
justify-content: space-between;
align-items: center;
&:hover {
background: ${({ isDarkMode }): string => {
return isDarkMode ? grey[7] : '#ddd';
}};
background: ${({ isDarkMode }): string => (isDarkMode ? grey[7] : '#ddd')};
}
`;

View File

@@ -175,6 +175,7 @@ export interface QueryBuilderProps {
onDropDownToggleHandler: (value: boolean) => VoidFunction;
fieldsQuery: QueryFields[][];
setFieldsQuery: (q: QueryFields[][]) => void;
syncKeyPrefix: () => void;
}
function QueryBuilder({
@@ -182,6 +183,7 @@ function QueryBuilder({
fieldsQuery,
setFieldsQuery,
onDropDownToggleHandler,
syncKeyPrefix,
}: QueryBuilderProps): JSX.Element {
const handleUpdate = (query: Query, queryIndex: number): void => {
const updated = [...fieldsQuery];
@@ -195,6 +197,9 @@ function QueryBuilder({
else updated.splice(queryIndex, 2);
setFieldsQuery(updated);
// initiate re-render query panel
syncKeyPrefix();
};
const QueryUI = (

View File

@@ -47,6 +47,14 @@ function SearchFields({
}
}, [parsedQuery]);
// syncKeyPrefix initiates re-render. useful in situations like
// delete field (in search panel). this method allows condiitonally
// setting keyPrefix as doing it on every update of query initiates
// a re-render. this is a problem for text fields where input focus goes away.
const syncKeyPrefix = (): void => {
keyPrefixRef.current = hashCode(JSON.stringify(fieldsQuery));
};
const addSuggestedField = useCallback(
(name: string): void => {
if (!name) {
@@ -98,6 +106,7 @@ function SearchFields({
onDropDownToggleHandler={onDropDownToggleHandler}
fieldsQuery={fieldsQuery}
setFieldsQuery={setFieldsQuery}
syncKeyPrefix={syncKeyPrefix}
/>
<SearchFieldsActionBar
applyUpdate={applyUpdate}

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