Compare commits
345 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d59f4c7e5 | ||
|
|
59025238b3 | ||
|
|
b62b3591af | ||
|
|
97207f8e6d | ||
|
|
745626f516 | ||
|
|
826cbe0803 | ||
|
|
342e94d093 | ||
|
|
6614cd31c1 | ||
|
|
7086f7eafa | ||
|
|
edf7e9821f | ||
|
|
a52c104562 | ||
|
|
a3c917cca0 | ||
|
|
e73432df83 | ||
|
|
d2cdf401b8 | ||
|
|
9091b9b82c | ||
|
|
c69e2e0d50 | ||
|
|
ad5a9dcd6a | ||
|
|
7fd27ec09d | ||
|
|
26a806a7fe | ||
|
|
bc5862646d | ||
|
|
eb1c5c4565 | ||
|
|
fdbb8b652e | ||
|
|
7f9c226175 | ||
|
|
b9c87c1395 | ||
|
|
708f6dd03f | ||
|
|
2f27908434 | ||
|
|
234a69de8c | ||
|
|
fda0441686 | ||
|
|
b034c60897 | ||
|
|
5a81f5f90b | ||
|
|
818a984af3 | ||
|
|
1ded475b37 | ||
|
|
6e8be3fcc3 | ||
|
|
182ba3596d | ||
|
|
ef6d847c15 | ||
|
|
6e2ceb9efb | ||
|
|
02035ebd82 | ||
|
|
cf95d9c76f | ||
|
|
9ff055015f | ||
|
|
e2ce1eb88b | ||
|
|
5a00fbd1d2 | ||
|
|
a047801014 | ||
|
|
72452dc946 | ||
|
|
604d98be05 | ||
|
|
e7f5adc8a9 | ||
|
|
fb10d7d81f | ||
|
|
d67f709b8a | ||
|
|
81291c996f | ||
|
|
18fc1a2761 | ||
|
|
679eb39256 | ||
|
|
357e422eca | ||
|
|
ca3ff04f7d | ||
|
|
a0c320e47e | ||
|
|
5637188e72 | ||
|
|
7cb2399c4c | ||
|
|
93c9138fe1 | ||
|
|
d1a256a6d5 | ||
|
|
b6a455d264 | ||
|
|
c32b8638a4 | ||
|
|
e21f23874d | ||
|
|
43c05c9605 | ||
|
|
ec8e505647 | ||
|
|
c7f09354f7 | ||
|
|
df0502726d | ||
|
|
e8f2176566 | ||
|
|
9da399023b | ||
|
|
76331001b7 | ||
|
|
10e47b5bff | ||
|
|
25398d9d35 | ||
|
|
38bfc41190 | ||
|
|
8679f2c37a | ||
|
|
f7cd0d4934 | ||
|
|
12349d79a9 | ||
|
|
c5991b50bc | ||
|
|
8dbd1c65e9 | ||
|
|
2089c51f63 | ||
|
|
a021386cb8 | ||
|
|
8c2f33c95a | ||
|
|
bbda684e65 | ||
|
|
37ff9480e1 | ||
|
|
fe314a8ddd | ||
|
|
8bddee75a3 | ||
|
|
a8eec1b7ab | ||
|
|
93220ba6c2 | ||
|
|
f45ac7855e | ||
|
|
33ac5b79be | ||
|
|
d5e112a9bc | ||
|
|
428b10f78c | ||
|
|
8824916880 | ||
|
|
b24fadaf86 | ||
|
|
c149181924 | ||
|
|
5ad367a0fc | ||
|
|
bd248c46b2 | ||
|
|
dcad77746a | ||
|
|
b27bdac1f6 | ||
|
|
17438ca823 | ||
|
|
684eeace93 | ||
|
|
bbffac1603 | ||
|
|
37493b49e5 | ||
|
|
3e97d2ffa3 | ||
|
|
e2df2e7c41 | ||
|
|
6322578842 | ||
|
|
21c6d3ba99 | ||
|
|
975d57cade | ||
|
|
efe34d2582 | ||
|
|
d63a35e937 | ||
|
|
c49bb0696b | ||
|
|
9a58cc652c | ||
|
|
5ee0bb57cc | ||
|
|
6949c659af | ||
|
|
6c11c6d4da | ||
|
|
9557cb2f70 | ||
|
|
2e4f0cfc33 | ||
|
|
ea6ee6a6ef | ||
|
|
dd25ad95c7 | ||
|
|
63570c847a | ||
|
|
60b78e94d8 | ||
|
|
51f1d0fd05 | ||
|
|
502b8b1ba8 | ||
|
|
041d347d50 | ||
|
|
9aff047da4 | ||
|
|
0bc44c6fd9 | ||
|
|
23081996c4 | ||
|
|
2c206e8bf4 | ||
|
|
1726469aaa | ||
|
|
fb1e823e6b | ||
|
|
813eeb6d5a | ||
|
|
a3bc2ff24e | ||
|
|
0c2574cef8 | ||
|
|
7c952fd9cd | ||
|
|
9ac2308b89 | ||
|
|
81beaffa3d | ||
|
|
4db109cbad | ||
|
|
5b72919a55 | ||
|
|
d09290528f | ||
|
|
7dd09129aa | ||
|
|
5f73a82d9f | ||
|
|
d4bfe3a096 | ||
|
|
c92493904b | ||
|
|
c74896b213 | ||
|
|
0b3e8d797b | ||
|
|
c8f3e9024c | ||
|
|
8f6178f0a9 | ||
|
|
5ff9172103 | ||
|
|
67ba46abde | ||
|
|
20b1f96c19 | ||
|
|
0e8f09632f | ||
|
|
61a1d04252 | ||
|
|
80171eddea | ||
|
|
28684423d1 | ||
|
|
a5e4336e18 | ||
|
|
037559537b | ||
|
|
c9db4c9051 | ||
|
|
31a89bfdb3 | ||
|
|
776aa3471f | ||
|
|
36610c809e | ||
|
|
1a0c76a43b | ||
|
|
c1d00c1155 | ||
|
|
3f96325ad8 | ||
|
|
99ed314fc9 | ||
|
|
12e56932ee | ||
|
|
c4944370ce | ||
|
|
192d3881a1 | ||
|
|
d6152510c7 | ||
|
|
3e37a8b364 | ||
|
|
b9b63a0ac4 | ||
|
|
9d20c2f787 | ||
|
|
8ea0f72178 | ||
|
|
167050b4b5 | ||
|
|
fe640aae39 | ||
|
|
6c2faa21f4 | ||
|
|
c617784d7c | ||
|
|
1e7280136a | ||
|
|
17a5bc8cc3 | ||
|
|
c3763032df | ||
|
|
da4cbf6c2f | ||
|
|
97bfee48e1 | ||
|
|
da23d9e087 | ||
|
|
27db1b9080 | ||
|
|
55d7285c9a | ||
|
|
27c48674d4 | ||
|
|
d29dfa0751 | ||
|
|
d951483597 | ||
|
|
481792d4ca | ||
|
|
0fa20445d8 | ||
|
|
eb4ac18162 | ||
|
|
1ddda19c8e | ||
|
|
91c3abae37 | ||
|
|
b5debe6ea2 | ||
|
|
7367f8dd4b | ||
|
|
bac717e9e6 | ||
|
|
e1219ea942 | ||
|
|
1c867d3b4c | ||
|
|
65c2a0bf6a | ||
|
|
755d64061e | ||
|
|
500ab02c47 | ||
|
|
dfef41913f | ||
|
|
210c5fd7f2 | ||
|
|
c4b052c51e | ||
|
|
da79f93495 | ||
|
|
83e3e3c3ed | ||
|
|
7508c9148f | ||
|
|
b15463fd38 | ||
|
|
66b2e17bba | ||
|
|
9af991e424 | ||
|
|
59497ed53c | ||
|
|
7f04a4407b | ||
|
|
2a03291171 | ||
|
|
53bfc33075 | ||
|
|
c821e8bb75 | ||
|
|
eff87f2666 | ||
|
|
3f5171dc69 | ||
|
|
c5d7d9d134 | ||
|
|
6defa0ac8b | ||
|
|
e3fee332c7 | ||
|
|
2c7cefcc74 | ||
|
|
080a53a9b4 | ||
|
|
2a5cb78964 | ||
|
|
b99d7009a1 | ||
|
|
e46b7e41e5 | ||
|
|
50270281e3 | ||
|
|
5e5e81d81d | ||
|
|
eb2fe20025 | ||
|
|
df7f276f03 | ||
|
|
b0f62daa24 | ||
|
|
797352583a | ||
|
|
96267e2e3a | ||
|
|
388ef9453c | ||
|
|
995e45713c | ||
|
|
b0d5b15330 | ||
|
|
80cd317b3b | ||
|
|
51721f97c7 | ||
|
|
e7e0f5b96a | ||
|
|
a26ebb742a | ||
|
|
9d1305f174 | ||
|
|
ab514cc0f2 | ||
|
|
1f44f089e0 | ||
|
|
174fc107c2 | ||
|
|
06a55ccdd6 | ||
|
|
a3731e4c4e | ||
|
|
e183cace75 | ||
|
|
23490ca7f8 | ||
|
|
9f71e732c7 | ||
|
|
23d6287594 | ||
|
|
2624ce4007 | ||
|
|
3d5134b43c | ||
|
|
c657f96032 | ||
|
|
c18fff6ae8 | ||
|
|
28142764af | ||
|
|
2fa265ff2e | ||
|
|
dca0b11acd | ||
|
|
bad80def90 | ||
|
|
8965b9b503 | ||
|
|
05076968c9 | ||
|
|
7c8afc2e1c | ||
|
|
c8a1a8600e | ||
|
|
45cb1eb38f | ||
|
|
8ebb76bd0c | ||
|
|
309ffa4989 | ||
|
|
d787298600 | ||
|
|
7998d474e2 | ||
|
|
7b8ff5a285 | ||
|
|
cb22aef36f | ||
|
|
cf93712286 | ||
|
|
a906f94b8a | ||
|
|
93b6749920 | ||
|
|
8ab527b174 | ||
|
|
ad163c2b61 | ||
|
|
21f909f4c0 | ||
|
|
b67206dd65 | ||
|
|
ce5afd31fd | ||
|
|
9a184f5740 | ||
|
|
be14f1c32c | ||
|
|
ae37a608f8 | ||
|
|
aaeb579d0d | ||
|
|
1151e8521e | ||
|
|
d779b83715 | ||
|
|
47a41473df | ||
|
|
de370d7f0c | ||
|
|
8a5b26cefe | ||
|
|
2a20b6fc86 | ||
|
|
02ef1744b4 | ||
|
|
2c973adf0b | ||
|
|
f7ff491d35 | ||
|
|
6cd341a887 | ||
|
|
0832bce955 | ||
|
|
62b2462e03 | ||
|
|
152846f554 | ||
|
|
846da08cbd | ||
|
|
17f32e9765 | ||
|
|
48659a2957 | ||
|
|
a2a8a32d1c | ||
|
|
28f2ee2627 | ||
|
|
3b01bb2614 | ||
|
|
622e1765cf | ||
|
|
faaf0a6e73 | ||
|
|
4542a51531 | ||
|
|
191a538430 | ||
|
|
e6ce80213b | ||
|
|
3115b32dcd | ||
|
|
af272a368b | ||
|
|
b336a6cb45 | ||
|
|
b72815ca2f | ||
|
|
ed4a01dea6 | ||
|
|
1914c3b4a0 | ||
|
|
3811e96e23 | ||
|
|
8d16493432 | ||
|
|
db2bfbb887 | ||
|
|
213838a021 | ||
|
|
fd6f9a90e1 | ||
|
|
13f9922c53 | ||
|
|
a654baaa5b | ||
|
|
f766435acc | ||
|
|
d7a65ba689 | ||
|
|
05ce03e67d | ||
|
|
ba6818f487 | ||
|
|
ca53136cbf | ||
|
|
c46bef321c | ||
|
|
ba8f804b26 | ||
|
|
6cc7025e37 | ||
|
|
e62e541fc4 | ||
|
|
2f1ca93eda | ||
|
|
f1c7d72fc5 | ||
|
|
a405307c96 | ||
|
|
c85d48d7fa | ||
|
|
75470f6bb9 | ||
|
|
f75e688b32 | ||
|
|
5f3ca045df | ||
|
|
186632af69 | ||
|
|
fa652be926 | ||
|
|
1e39131c38 | ||
|
|
153e859ac3 | ||
|
|
d1cc29e118 | ||
|
|
972bf94dd0 | ||
|
|
3632208d45 | ||
|
|
cd9768c738 | ||
|
|
f01b9605db | ||
|
|
eec236af50 | ||
|
|
bbff2b459e | ||
|
|
d9535e7a8d | ||
|
|
a82bbe1a72 | ||
|
|
6812f55152 | ||
|
|
83163c17cd | ||
|
|
5ed7c9a46e | ||
|
|
2f323056d0 |
10
.github/CODEOWNERS
vendored
10
.github/CODEOWNERS
vendored
@@ -2,6 +2,12 @@
|
||||
# Owners are automatically requested for review for PRs that changes code
|
||||
# that they own.
|
||||
* @ankitnayan
|
||||
/frontend/ @palashgdev @pranshuchittora
|
||||
|
||||
/frontend/ @palashgdev
|
||||
/deploy/ @prashant-shahi
|
||||
/pkg/query-service/ @srikanthccv
|
||||
/sample-apps/ @prashant-shahi
|
||||
**/query-service/ @srikanthccv
|
||||
Makefile @srikanthccv
|
||||
go.* @srikanthccv
|
||||
.git* @srikanthccv
|
||||
.github @prashant-shahi
|
||||
|
||||
2
.github/config.yml
vendored
2
.github/config.yml
vendored
@@ -17,7 +17,7 @@ newPRWelcomeComment: >
|
||||
# Comment to be posted to on pull requests merged by a first time user
|
||||
firstPRMergeComment: >
|
||||
Congrats on merging your first pull request!
|
||||
|
||||
|
||||

|
||||
|
||||
We here at SigNoz are proud of you! 🥳
|
||||
|
||||
9
.github/workflows/commitlint.yml
vendored
9
.github/workflows/commitlint.yml
vendored
@@ -7,12 +7,7 @@ jobs:
|
||||
lint-commits:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.1
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# we actually need "github.event.pull_request.commits + 1" commit
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v2.1.0
|
||||
# or just "yarn" if you depend on "@commitlint/cli" already
|
||||
- run: yarn add @commitlint/cli
|
||||
- run: yarn add @commitlint/config-conventional
|
||||
- run: yarn run commitlint --config ./node_modules/@commitlint/config-conventional/index.js --from HEAD~${{ github.event.pull_request.commits }} --to HEAD
|
||||
- uses: wagoid/commitlint-github-action@v5
|
||||
|
||||
2
.github/workflows/e2e-k3s.yaml
vendored
2
.github/workflows/e2e-k3s.yaml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
--set frontend.service.type=LoadBalancer \
|
||||
--set queryService.image.tag=$DOCKER_TAG \
|
||||
--set frontend.image.tag=$DOCKER_TAG
|
||||
|
||||
|
||||
# get pods, services and the container images
|
||||
kubectl get pods -n platform
|
||||
kubectl get svc -n platform
|
||||
|
||||
5
.github/workflows/pr_verify_linked_issue.yml
vendored
5
.github/workflows/pr_verify_linked_issue.yml
vendored
@@ -5,7 +5,7 @@ name: VerifyIssue
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [edited, synchronize, opened, reopened]
|
||||
types: [edited, opened]
|
||||
check_run:
|
||||
|
||||
jobs:
|
||||
@@ -14,7 +14,6 @@ jobs:
|
||||
name: Ensure Pull Request has a linked issue.
|
||||
steps:
|
||||
- name: Verify Linked Issue
|
||||
uses: hattan/verify-linked-issue-action@v1.1.0
|
||||
uses: srikanthccv/verify-linked-issue-action@v0.70
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
25
.github/workflows/repo-stats.yml
vendored
25
.github/workflows/repo-stats.yml
vendored
@@ -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 }}
|
||||
1
.github/workflows/sonar.yml
vendored
1
.github/workflows/sonar.yml
vendored
@@ -24,4 +24,3 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
6
.github/workflows/staging-deployment.yaml
vendored
6
.github/workflows/staging-deployment.yaml
vendored
@@ -11,21 +11,23 @@ jobs:
|
||||
environment: staging
|
||||
steps:
|
||||
- name: Executing remote ssh commands using ssh key
|
||||
uses: appleboy/ssh-action@v0.1.6
|
||||
uses: appleboy/ssh-action@v0.1.8
|
||||
env:
|
||||
GITHUB_BRANCH: develop
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
with:
|
||||
host: ${{ secrets.HOST_DNS }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.EC2_SSH_KEY }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
envs: GITHUB_BRANCH,GITHUB_SHA
|
||||
command_timeout: 60m
|
||||
script: |
|
||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
export OTELCOL_TAG="main"
|
||||
docker system prune --force
|
||||
docker pull signoz/signoz-otel-collector:main
|
||||
cd ~/signoz
|
||||
git status
|
||||
git add .
|
||||
|
||||
4
.github/workflows/testing-deployment.yaml
vendored
4
.github/workflows/testing-deployment.yaml
vendored
@@ -11,14 +11,14 @@ jobs:
|
||||
if: ${{ github.event.label.name == 'testing-deploy' }}
|
||||
steps:
|
||||
- name: Executing remote ssh commands using ssh key
|
||||
uses: appleboy/ssh-action@v0.1.6
|
||||
uses: appleboy/ssh-action@v0.1.8
|
||||
env:
|
||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
with:
|
||||
host: ${{ secrets.HOST_DNS }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.EC2_SSH_KEY }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
envs: GITHUB_BRANCH,GITHUB_SHA
|
||||
command_timeout: 60m
|
||||
script: |
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,5 @@
|
||||
|
||||
node_modules
|
||||
yarn.lock
|
||||
package.json
|
||||
|
||||
deploy/docker/environment_tiny/common_test
|
||||
frontend/node_modules
|
||||
@@ -52,4 +50,6 @@ ee/query-service/tests/test-deploy/data/
|
||||
*.db
|
||||
/deploy/docker/clickhouse-setup/data/
|
||||
/deploy/docker-swarm/clickhouse-setup/data/
|
||||
bin/
|
||||
bin/
|
||||
|
||||
*/query-service/queries.active
|
||||
|
||||
@@ -80,7 +80,7 @@ Before sending us a pull request, please ensure that,
|
||||
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
|
||||
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
|
||||
|
||||
**Note:** Unless your change is small, **please** consider submitting different Pull Rrequest(s):
|
||||
**Note:** Unless your change is small, **please** consider submitting different Pull Request(s):
|
||||
|
||||
* 1️⃣ First PR should include the overall structure of the new component:
|
||||
* Readme, configuration, interfaces or base classes, etc...
|
||||
|
||||
4
Makefile
4
Makefile
@@ -138,3 +138,7 @@ clear-swarm-data:
|
||||
|
||||
test:
|
||||
go test ./pkg/query-service/app/metrics/...
|
||||
go test ./pkg/query-service/cache/...
|
||||
go test ./pkg/query-service/app/...
|
||||
go test ./pkg/query-service/converter/...
|
||||
go test ./pkg/query-service/formatter/...
|
||||
|
||||
@@ -85,9 +85,9 @@ Hier findest du die vollständige Liste von unterstützten Programmiersprachen -
|
||||
|
||||
### Bereitstellung mit Docker
|
||||
|
||||
Bitte folge den [hier](https://signoz.io/docs/deployment/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
|
||||
Bitte folge den [hier](https://signoz.io/docs/install/docker/) aufgelisteten Schritten um deine Anwendung mit Docker bereitzustellen.
|
||||
|
||||
Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/deployment/troubleshooting) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
|
||||
Die [Anleitungen zur Fehlerbehebung](https://signoz.io/docs/install/troubleshooting/) könnten hilfreich sein, falls du auf irgendwelche Schwierigkeiten stößt.
|
||||
|
||||
<p>  </p>
|
||||
|
||||
|
||||
59
README.md
59
README.md
@@ -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
|
||||
|
||||
@@ -35,19 +35,41 @@ SigNoz helps developers monitor applications and troubleshoot problems in their
|
||||
|
||||
👉 Filter and query logs, build dashboards and alerts based on attributes in logs
|
||||
|
||||

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

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

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

|
||||
👉 Record exceptions automatically in Python, Java, Ruby, and Javascript
|
||||
|
||||
👉 Easy to set alerts with DIY query builder
|
||||
|
||||
|
||||
### Application Metrics
|
||||
|
||||

|
||||
|
||||
|
||||
### Distributed Tracing
|
||||
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
|
||||
|
||||
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
|
||||
|
||||
### Logs Management
|
||||
|
||||
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
|
||||
|
||||
### Infrastructure Monitoring
|
||||
|
||||
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
|
||||
|
||||
### Exceptions Monitoring
|
||||
|
||||

|
||||
|
||||
|
||||
### Alerts
|
||||
|
||||
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
|
||||
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
|
||||
|
||||
## Join our Slack community
|
||||
|
||||
@@ -55,7 +77,6 @@ Come say Hi to us on [Slack](https://signoz.io/slack) 👋
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Features.svg" width="50px" />
|
||||
|
||||
## Features:
|
||||
|
||||
@@ -65,10 +86,13 @@ Come say Hi to us on [Slack](https://signoz.io/slack) 👋
|
||||
- See exact request trace to figure out issues in downstream services, slow DB queries, call to 3rd party services like payment gateways, etc
|
||||
- Filter traces by service name, operation, latency, error, tags/annotations.
|
||||
- Run aggregates on trace data (events/spans) to get business relevant metrics. e.g. You can get error rate and 99th percentile latency of `customer_type: gold` or `deployment_version: v2` or `external_call: paypal`
|
||||
- Native support for OpenTelemetry Logs, advanced log query builder, and automatic log collection from k8s cluster
|
||||
- Lightning quick log analytics ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
|
||||
- End-to-End visibility into infrastructure performance, ingest metrics from all kinds of host environments
|
||||
- Easy to set alerts with DIY query builder
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/WhatsCool.svg" width="50px" />
|
||||
|
||||
## Why SigNoz?
|
||||
|
||||
@@ -97,15 +121,14 @@ You can find the complete list of languages here - https://opentelemetry.io/docs
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Philosophy.svg" width="50px" />
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Deploy using Docker
|
||||
|
||||
Please follow the steps listed [here](https://signoz.io/docs/deployment/docker/) to install using docker
|
||||
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
|
||||
|
||||
The [troubleshooting instructions](https://signoz.io/docs/deployment/troubleshooting) may be helpful if you face any issues.
|
||||
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
|
||||
|
||||
<p>  </p>
|
||||
|
||||
@@ -116,7 +139,6 @@ Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_cha
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/UseSigNoz.svg" width="50px" />
|
||||
|
||||
## Comparisons to Familiar Tools
|
||||
|
||||
@@ -144,6 +166,8 @@ Moreover, SigNoz has few more advanced features wrt Jaeger:
|
||||
- SigNoz Logs management are based on ClickHouse, a columnar OLAP datastore which makes aggregate log analytics queries much more efficient
|
||||
- 50% lower resource requirement compared to Elastic during ingestion
|
||||
|
||||
We have published benchmarks comparing Elastic with SigNoz. Check it out [here](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<p>  </p>
|
||||
|
||||
### SigNoz vs Loki
|
||||
@@ -152,9 +176,10 @@ Moreover, SigNoz has few more advanced features wrt Jaeger:
|
||||
- SigNoz supports indexes over high cardinality data and has no limitations on the number of indexes, while Loki reaches max streams with a few indexes added to it.
|
||||
- Searching over a huge volume of data is difficult and slow in Loki compared to SigNoz
|
||||
|
||||
We have published benchmarks comparing Loki with SigNoz. Check it out [here](https://signoz.io/blog/logs-performance-benchmark/?utm_source=github-readme&utm_medium=logs-benchmark)
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributors.svg" width="50px" />
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -181,7 +206,6 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/DevelopingLocally.svg" width="50px" />
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -189,7 +213,6 @@ You can find docs at https://signoz.io/docs/. If you need any clarification or f
|
||||
|
||||
<br /><br />
|
||||
|
||||
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Contributing.svg" width="50px" />
|
||||
|
||||
## Community
|
||||
|
||||
|
||||
@@ -84,9 +84,9 @@ Você pode encontrar a lista completa de linguagens aqui - https://opentelemetry
|
||||
|
||||
### Implantar usando Docker
|
||||
|
||||
Siga as etapas listadas [aqui](https://signoz.io/docs/deployment/docker/) para instalar usando o Docker.
|
||||
Siga as etapas listadas [aqui](https://signoz.io/docs/install/docker/) para instalar usando o Docker.
|
||||
|
||||
Esse [guia para solução de problemas](https://signoz.io/docs/deployment/troubleshooting) pode ser útil se você enfrentar quaisquer problemas.
|
||||
Esse [guia para solução de problemas](https://signoz.io/docs/install/troubleshooting/) pode ser útil se você enfrentar quaisquer problemas.
|
||||
|
||||
<p>  </p>
|
||||
|
||||
|
||||
@@ -80,9 +80,9 @@ SigNoz帮助开发人员监控应用并排查已部署应用中的问题。SigNo
|
||||
|
||||
### 使用Docker部署
|
||||
|
||||
请按照[这里](https://signoz.io/docs/deployment/docker/)列出的步骤使用Docker来安装
|
||||
请按照[这里](https://signoz.io/docs/install/docker/)列出的步骤使用Docker来安装
|
||||
|
||||
如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/deployment/troubleshooting)会对你有帮助。
|
||||
如果你遇到任何问题,这个[排查指南](https://signoz.io/docs/install/troubleshooting/)会对你有帮助。
|
||||
|
||||
<p>  </p>
|
||||
|
||||
|
||||
@@ -27,12 +27,6 @@ For x86 chip (amd):
|
||||
docker-compose -f docker/clickhouse-setup/docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
For Mac with Apple chip (arm):
|
||||
|
||||
```sh
|
||||
docker-compose -f docker/clickhouse-setup/docker-compose.arm.yaml up -d
|
||||
```
|
||||
|
||||
Open http://localhost:3301 in your favourite browser. In couple of minutes, you should see
|
||||
the data generated from hotrod in SigNoz UI.
|
||||
|
||||
|
||||
@@ -7,9 +7,21 @@
|
||||
</default>
|
||||
<s3>
|
||||
<type>s3</type>
|
||||
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
|
||||
<!-- For S3 cold storage,
|
||||
if region is us-east-1, endpoint can be https://<bucket-name>.s3.amazonaws.com
|
||||
if region is not us-east-1, endpoint should be https://<bucket-name>.s3-<region>.amazonaws.com
|
||||
For GCS cold storage,
|
||||
endpoint should be https://storage.googleapis.com/<bucket-name>/data/
|
||||
-->
|
||||
<endpoint>https://BUCKET-NAME.s3-REGION-NAME.amazonaws.com/data/</endpoint>
|
||||
<access_key_id>ACCESS-KEY-ID</access_key_id>
|
||||
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
|
||||
<!-- In case of S3, uncomment the below configuration in case you want to read
|
||||
AWS credentials from the Environment variables if they exist. -->
|
||||
<!-- <use_environment_credentials>true</use_environment_credentials> -->
|
||||
<!-- In case of GCS, uncomment the below configuration, since GCS does
|
||||
not support batch deletion and result in error messages in logs. -->
|
||||
<!-- <support_batch_delete>false</support_batch_delete> -->
|
||||
</s3>
|
||||
</disks>
|
||||
<policies>
|
||||
|
||||
@@ -34,7 +34,7 @@ x-clickhouse-depend: &clickhouse-depend
|
||||
|
||||
services:
|
||||
zookeeper-1:
|
||||
image: bitnami/zookeeper:3.7.0
|
||||
image: bitnami/zookeeper:3.7.1
|
||||
hostname: zookeeper-1
|
||||
user: root
|
||||
ports:
|
||||
@@ -124,7 +124,7 @@ services:
|
||||
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:0.23.0-0.2
|
||||
image: signoz/alertmanager:0.23.1
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
command:
|
||||
@@ -137,7 +137,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.14.0
|
||||
image: signoz/query-service:0.20.1
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@@ -156,7 +156,7 @@ services:
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-swarm
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -166,7 +166,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.14.0
|
||||
image: signoz/frontend:0.20.1
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -179,8 +179,8 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.66.2
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
image: signoz/signoz-otel-collector:0.76.1
|
||||
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
@@ -188,6 +188,7 @@ services:
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
@@ -207,8 +208,8 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.66.2
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
image: signoz/signoz-otel-collector:0.76.1
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
|
||||
@@ -75,7 +75,7 @@ processors:
|
||||
timeout: 10s
|
||||
resourcedetection:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
|
||||
detectors: [env, system] # include ec2 for AWS, gcp for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
signozspanmetrics/prometheus:
|
||||
metrics_exporter: prometheus
|
||||
@@ -110,6 +110,7 @@ exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
|
||||
@@ -7,9 +7,21 @@
|
||||
</default>
|
||||
<s3>
|
||||
<type>s3</type>
|
||||
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
|
||||
<!-- For S3 cold storage,
|
||||
if region is us-east-1, endpoint can be https://<bucket-name>.s3.amazonaws.com
|
||||
if region is not us-east-1, endpoint should be https://<bucket-name>.s3-<region>.amazonaws.com
|
||||
For GCS cold storage,
|
||||
endpoint should be https://storage.googleapis.com/<bucket-name>/data/
|
||||
-->
|
||||
<endpoint>https://BUCKET-NAME.s3-REGION-NAME.amazonaws.com/data/</endpoint>
|
||||
<access_key_id>ACCESS-KEY-ID</access_key_id>
|
||||
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
|
||||
<!-- In case of S3, uncomment the below configuration in case you want to read
|
||||
AWS credentials from the Environment variables if they exist. -->
|
||||
<!-- <use_environment_credentials>true</use_environment_credentials> -->
|
||||
<!-- In case of GCS, uncomment the below configuration, since GCS does
|
||||
not support batch deletion and result in error messages in logs. -->
|
||||
<!-- <support_batch_delete>false</support_batch_delete> -->
|
||||
</s3>
|
||||
</disks>
|
||||
<policies>
|
||||
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
|
||||
alertmanager:
|
||||
container_name: alertmanager
|
||||
image: signoz/alertmanager:0.23.0-0.2
|
||||
image: signoz/alertmanager:0.23.1
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
depends_on:
|
||||
@@ -41,8 +41,8 @@ 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.2
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
image: signoz/signoz-otel-collector:0.76.1
|
||||
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
# user: root # required for reading docker container logs
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
@@ -67,8 +67,8 @@ services:
|
||||
|
||||
otel-collector-metrics:
|
||||
container_name: otel-collector-metrics
|
||||
image: signoz/signoz-otel-collector:0.66.2
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
image: signoz/signoz-otel-collector:0.76.1
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
- "8080:8080"
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
@@ -34,9 +34,9 @@ x-clickhouse-depend: &clickhouse-depend
|
||||
# condition: service_healthy
|
||||
|
||||
services:
|
||||
|
||||
|
||||
zookeeper-1:
|
||||
image: bitnami/zookeeper:3.7.0
|
||||
image: bitnami/zookeeper:3.7.1
|
||||
container_name: zookeeper-1
|
||||
hostname: zookeeper-1
|
||||
user: root
|
||||
@@ -114,13 +114,13 @@ 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
|
||||
# - ./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
|
||||
# container_name: clickhouse-3
|
||||
@@ -132,14 +132,14 @@ 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
|
||||
# - ./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}
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.1}
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
depends_on:
|
||||
@@ -150,10 +150,10 @@ services:
|
||||
- --queryService.url=http://query-service:8085
|
||||
- --storage.path=/data
|
||||
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
# 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.14.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.20.1}
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
@@ -174,14 +174,14 @@ services:
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.14.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.20.1}
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -193,8 +193,8 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1}
|
||||
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
@@ -202,6 +202,7 @@ services:
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
- DOCKER_MULTI_NODE_CLUSTER=false
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
@@ -218,8 +219,8 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.2}
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.76.1}
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
@@ -231,15 +232,15 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
hotrod:
|
||||
image: jaegertracing/example-hotrod:1.30
|
||||
container_name: hotrod
|
||||
logging:
|
||||
options:
|
||||
max-size: 50m
|
||||
max-file: "3"
|
||||
command: ["all"]
|
||||
environment:
|
||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||
image: jaegertracing/example-hotrod:1.30
|
||||
container_name: hotrod
|
||||
logging:
|
||||
options:
|
||||
max-size: 50m
|
||||
max-file: "3"
|
||||
command: ["all"]
|
||||
environment:
|
||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||
|
||||
load-hotrod:
|
||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
||||
|
||||
@@ -70,6 +70,40 @@ receivers:
|
||||
|
||||
|
||||
processors:
|
||||
logstransform/internal:
|
||||
operators:
|
||||
- type: trace_parser
|
||||
if: '"trace_id" in attributes or "span_id" in attributes'
|
||||
trace_id:
|
||||
parse_from: attributes.trace_id
|
||||
span_id:
|
||||
parse_from: attributes.span_id
|
||||
output: remove_trace_id
|
||||
- type: trace_parser
|
||||
if: '"traceId" in attributes or "spanId" in attributes'
|
||||
trace_id:
|
||||
parse_from: attributes.traceId
|
||||
span_id:
|
||||
parse_from: attributes.spanId
|
||||
output: remove_traceId
|
||||
- id: remove_traceId
|
||||
type: remove
|
||||
if: '"traceId" in attributes'
|
||||
field: attributes.traceId
|
||||
output: remove_spanId
|
||||
- id: remove_spanId
|
||||
type: remove
|
||||
if: '"spanId" in attributes'
|
||||
field: attributes.spanId
|
||||
- id: remove_trace_id
|
||||
type: remove
|
||||
if: '"trace_id" in attributes'
|
||||
field: attributes.trace_id
|
||||
output: remove_span_id
|
||||
- id: remove_span_id
|
||||
type: remove
|
||||
if: '"span_id" in attributes'
|
||||
field: attributes.span_id
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
send_batch_max_size: 11000
|
||||
@@ -104,7 +138,7 @@ processors:
|
||||
# retry_on_failure: true
|
||||
resourcedetection:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
|
||||
detectors: [env, system] # include ec2 for AWS, gcp for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
|
||||
extensions:
|
||||
@@ -119,6 +153,7 @@ exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
@@ -171,5 +206,5 @@ service:
|
||||
exporters: [prometheus]
|
||||
logs:
|
||||
receivers: [otlp, filelog/dockercontainers]
|
||||
processors: [batch]
|
||||
processors: [logstransform/internal, batch]
|
||||
exporters: [clickhouselogsexporter]
|
||||
@@ -51,7 +51,7 @@ check_os() {
|
||||
os_name="$(cat /etc/*-release | awk -F= '$1 == "NAME" { gsub(/"/, ""); print $2; exit }')"
|
||||
|
||||
case "$os_name" in
|
||||
Ubuntu*)
|
||||
Ubuntu*|Pop!_OS)
|
||||
desired_os=1
|
||||
os="ubuntu"
|
||||
package_manager="apt-get"
|
||||
@@ -81,6 +81,11 @@ check_os() {
|
||||
os="centos"
|
||||
package_manager="yum"
|
||||
;;
|
||||
Rocky*)
|
||||
desired_os=1
|
||||
os="centos"
|
||||
package_manager="yum"
|
||||
;;
|
||||
SLES*)
|
||||
desired_os=1
|
||||
os="sles"
|
||||
@@ -120,7 +125,7 @@ check_ports_occupied() {
|
||||
|
||||
echo "+++++++++++ ERROR ++++++++++++++++++++++"
|
||||
echo "SigNoz requires ports 3301 & 4317 to be open. Please shut down any other service(s) that may be running on these ports."
|
||||
echo "You can run SigNoz on another port following this guide https://signoz.io/docs/deployment/docker#troubleshooting"
|
||||
echo "You can run SigNoz on another port following this guide https://signoz.io/docs/install/troubleshooting/"
|
||||
echo "++++++++++++++++++++++++++++++++++++++++"
|
||||
echo ""
|
||||
exit 1
|
||||
@@ -223,7 +228,7 @@ wait_for_containers_start() {
|
||||
|
||||
# The while loop is important because for-loops don't work for dynamic values
|
||||
while [[ $timeout -gt 0 ]]; do
|
||||
status_code="$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3301/api/v1/services/list || true)"
|
||||
status_code="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3301/api/v1/health?live=1" || true)"
|
||||
if [[ status_code -eq 200 ]]; then
|
||||
break
|
||||
else
|
||||
@@ -244,7 +249,7 @@ bye() { # Prints a friendly good bye message and exits the script.
|
||||
echo ""
|
||||
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
|
||||
|
||||
# echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker#troubleshooting"
|
||||
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
|
||||
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
|
||||
echo "++++++++++++++++++++++++++++++++++++++++"
|
||||
|
||||
@@ -272,7 +277,7 @@ request_sudo() {
|
||||
echo -e "\n\n🙇 We will need sudo access to complete the installation."
|
||||
if (( $EUID != 0 )); then
|
||||
sudo_cmd="sudo"
|
||||
echo -e "Please enter your sudo password, if prompt."
|
||||
echo -e "Please enter your sudo password, if prompted."
|
||||
# $sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null
|
||||
# if [[ $? -ne 0 ]] && ! $sudo_cmd -v; then
|
||||
# echo "Need sudo privileges to proceed with the installation."
|
||||
@@ -495,7 +500,7 @@ if [[ $status_code -ne 200 ]]; then
|
||||
|
||||
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
|
||||
|
||||
echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker/#troubleshooting-of-common-issues"
|
||||
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
|
||||
echo "or reach us on SigNoz for support https://signoz.io/slack"
|
||||
echo "++++++++++++++++++++++++++++++++++++++++"
|
||||
|
||||
@@ -511,13 +516,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!"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.17-buster AS builder
|
||||
FROM golang:1.18-buster AS builder
|
||||
|
||||
# LD_FLAGS is passed as argument from Makefile. It will be empty, if no argument passed
|
||||
ARG LD_FLAGS
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/license"
|
||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
rules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||
"go.signoz.io/signoz/pkg/query-service/version"
|
||||
)
|
||||
@@ -68,64 +69,75 @@ func (ah *APIHandler) CheckFeature(f string) bool {
|
||||
}
|
||||
|
||||
// RegisterRoutes registers routes for this handler on the given router
|
||||
func (ah *APIHandler) RegisterRoutes(router *mux.Router) {
|
||||
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddleware) {
|
||||
// note: add ee override methods first
|
||||
|
||||
// routes available only in ee version
|
||||
router.HandleFunc("/api/v1/licenses",
|
||||
baseapp.AdminAccess(ah.listLicenses)).
|
||||
am.AdminAccess(ah.listLicenses)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/licenses",
|
||||
baseapp.AdminAccess(ah.applyLicense)).
|
||||
am.AdminAccess(ah.applyLicense)).
|
||||
Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/featureFlags",
|
||||
baseapp.OpenAccess(ah.getFeatureFlags)).
|
||||
am.OpenAccess(ah.getFeatureFlags)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/loginPrecheck",
|
||||
baseapp.OpenAccess(ah.precheckLogin)).
|
||||
am.OpenAccess(ah.precheckLogin)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
// paid plans specific routes
|
||||
router.HandleFunc("/api/v1/complete/saml",
|
||||
baseapp.OpenAccess(ah.receiveSAML)).
|
||||
am.OpenAccess(ah.receiveSAML)).
|
||||
Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/complete/google",
|
||||
baseapp.OpenAccess(ah.receiveGoogleAuth)).
|
||||
am.OpenAccess(ah.receiveGoogleAuth)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
|
||||
router.HandleFunc("/api/v1/orgs/{orgId}/domains",
|
||||
baseapp.AdminAccess(ah.listDomainsByOrg)).
|
||||
am.AdminAccess(ah.listDomainsByOrg)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/domains",
|
||||
baseapp.AdminAccess(ah.postDomain)).
|
||||
am.AdminAccess(ah.postDomain)).
|
||||
Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/domains/{id}",
|
||||
baseapp.AdminAccess(ah.putDomain)).
|
||||
am.AdminAccess(ah.putDomain)).
|
||||
Methods(http.MethodPut)
|
||||
|
||||
router.HandleFunc("/api/v1/domains/{id}",
|
||||
baseapp.AdminAccess(ah.deleteDomain)).
|
||||
am.AdminAccess(ah.deleteDomain)).
|
||||
Methods(http.MethodDelete)
|
||||
|
||||
// base overrides
|
||||
router.HandleFunc("/api/v1/version", baseapp.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/{token}", baseapp.OpenAccess(ah.getInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/register", baseapp.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/login", baseapp.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/traces/{traceId}", baseapp.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/metrics/query_range", baseapp.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.getInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
|
||||
|
||||
ah.APIHandler.RegisterRoutes(router)
|
||||
// PAT APIs
|
||||
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.getPATs)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/pat/{id}", am.OpenAccess(ah.deletePAT)).Methods(http.MethodDelete)
|
||||
|
||||
ah.APIHandler.RegisterRoutes(router, am)
|
||||
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
|
||||
version := version.GetVersion()
|
||||
ah.WriteJSON(w, r, map[string]string{"version": version, "ee": "Y"})
|
||||
versionResponse := basemodel.GetVersionResponse{
|
||||
Version: version,
|
||||
EE: "Y",
|
||||
SetupCompleted: ah.SetupCompleted,
|
||||
}
|
||||
|
||||
ah.WriteJSON(w, r, versionResponse)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/ee/query-service/constants"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
@@ -87,9 +88,16 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// get invite object
|
||||
invite, err := baseauth.ValidateInvite(ctx, req)
|
||||
if err != nil || invite == nil {
|
||||
if err != nil {
|
||||
zap.S().Errorf("failed to validate invite token", err)
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if invite == nil {
|
||||
zap.S().Errorf("failed to validate invite token: it is either empty or invalid", err)
|
||||
RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// get auth domain from email domain
|
||||
@@ -190,7 +198,7 @@ func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string)
|
||||
}
|
||||
|
||||
// receiveGoogleAuth completes google OAuth response and forwards a request
|
||||
// to front-end to sign user in
|
||||
// to front-end to sign user in
|
||||
func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
redirectUri := constants.GetDefaultSiteURL()
|
||||
ctx := context.Background()
|
||||
@@ -221,15 +229,15 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
|
||||
// upgrade redirect url from the relay state for better accuracy
|
||||
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
||||
|
||||
// fetch domain by parsing relay state.
|
||||
// fetch domain by parsing relay state.
|
||||
domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
|
||||
if err != nil {
|
||||
handleSsoError(w, r, redirectUri)
|
||||
return
|
||||
}
|
||||
|
||||
// now that we have domain, use domain to fetch sso settings.
|
||||
// prepare google callback handler using parsedState -
|
||||
// now that we have domain, use domain to fetch sso settings.
|
||||
// prepare google callback handler using parsedState -
|
||||
// which contains redirect URL (front-end endpoint)
|
||||
callbackHandler, err := domain.PrepareGoogleOAuthProvider(parsedState)
|
||||
|
||||
@@ -239,7 +247,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
|
||||
handleSsoError(w, r, redirectUri)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
|
||||
if err != nil {
|
||||
zap.S().Errorf("[receiveGoogleAuth] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err))
|
||||
@@ -250,15 +258,12 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
|
||||
http.Redirect(w, r, nextPage, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// receiveSAML completes a SAML request and gets user logged in
|
||||
func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
||||
// this is the source url that initiated the login request
|
||||
redirectUri := constants.GetDefaultSiteURL()
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
if !ah.CheckFeature(model.SSO) {
|
||||
zap.S().Errorf("[receiveSAML] sso requested but feature unavailable %s in org domain %s", model.SSO)
|
||||
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
|
||||
@@ -287,13 +292,13 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
||||
// upgrade redirect url from the relay state for better accuracy
|
||||
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
||||
|
||||
// fetch domain by parsing relay state.
|
||||
// fetch domain by parsing relay state.
|
||||
domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
|
||||
if err != nil {
|
||||
handleSsoError(w, r, redirectUri)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
sp, err := domain.PrepareSamlRequest(parsedState)
|
||||
if err != nil {
|
||||
zap.S().Errorf("[receiveSAML] failed to prepare saml request for domain (%s): %v", domain.String(), err)
|
||||
@@ -327,6 +332,6 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
||||
handleSsoError(w, r, redirectUri)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
http.Redirect(w, r, nextPage, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import (
|
||||
)
|
||||
|
||||
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
featureSet := ah.FF().GetFeatureFlags()
|
||||
featureSet, err := ah.FF().GetFeatureFlags()
|
||||
if err != nil {
|
||||
ah.HandleError(w, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ah.Respond(w, featureSet)
|
||||
}
|
||||
|
||||
107
ee/query-service/app/api/pat.go
Normal file
107
ee/query-service/app/api/pat.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func generatePATToken() string {
|
||||
// Generate a 32-byte random token.
|
||||
token := make([]byte, 32)
|
||||
rand.Read(token)
|
||||
// Encode the token in base64.
|
||||
encodedToken := base64.StdEncoding.EncodeToString(token)
|
||||
return encodedToken
|
||||
}
|
||||
|
||||
func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
|
||||
req := model.PAT{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
user, err := auth.GetUserFromRequest(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorUnauthorized,
|
||||
Err: err,
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// All the PATs are associated with the user creating the PAT. Hence, the permissions
|
||||
// associated with the PAT is also equivalent to that of the user.
|
||||
req.UserID = user.Id
|
||||
req.CreatedAt = time.Now().Unix()
|
||||
req.Token = generatePATToken()
|
||||
|
||||
zap.S().Debugf("Got PAT request: %+v", req)
|
||||
if apierr := ah.AppDao().CreatePAT(ctx, &req); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, &req)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
user, err := auth.GetUserFromRequest(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorUnauthorized,
|
||||
Err: err,
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
zap.S().Infof("Get PATs for user: %+v", user.Id)
|
||||
pats, apierr := ah.AppDao().ListPATs(ctx, user.Id)
|
||||
if apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
ah.Respond(w, pats)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) deletePAT(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
id := mux.Vars(r)["id"]
|
||||
user, err := auth.GetUserFromRequest(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorUnauthorized,
|
||||
Err: err,
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
pat, apierr := ah.AppDao().GetPATByID(ctx, id)
|
||||
if apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
if pat.UserID != user.Id {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorUnauthorized,
|
||||
Err: fmt.Errorf("unauthorized PAT delete request"),
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
zap.S().Debugf("Delete PAT with id: %+v", id)
|
||||
if apierr := ah.AppDao().DeletePAT(ctx, id); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
ah.Respond(w, map[string]string{"data": "pat deleted successfully"})
|
||||
}
|
||||
@@ -22,14 +22,23 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/app/db"
|
||||
"go.signoz.io/signoz/ee/query-service/dao"
|
||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||
baseInterface "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
|
||||
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
||||
"go.signoz.io/signoz/ee/query-service/usage"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/agentConf"
|
||||
baseapp "go.signoz.io/signoz/pkg/query-service/app"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
||||
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"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"
|
||||
basemodel "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"
|
||||
@@ -37,6 +46,8 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const AppDbEngine = "sqlite"
|
||||
|
||||
type ServerOptions struct {
|
||||
PromConfigPath string
|
||||
HTTPHostPort string
|
||||
@@ -80,6 +91,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||
|
||||
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||
|
||||
if err != nil {
|
||||
@@ -115,12 +128,24 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
serverOptions.RuleRepoURL,
|
||||
localDB,
|
||||
reader,
|
||||
serverOptions.DisableRules)
|
||||
serverOptions.DisableRules,
|
||||
lm)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initiate opamp
|
||||
_, err = opAmpModel.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initiate agent config handler
|
||||
if err := agentConf.Initiate(localDB, AppDbEngine); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// start the usagemanager
|
||||
usageManager, err := usage.New("sqlite", localDB, lm.GetRepo(), reader.GetConn())
|
||||
if err != nil {
|
||||
@@ -187,7 +212,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
||||
// ip here for alert manager
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY"},
|
||||
})
|
||||
|
||||
handler := c.Handler(r)
|
||||
@@ -202,13 +227,33 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
|
||||
r := mux.NewRouter()
|
||||
|
||||
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
|
||||
patToken := r.Header.Get("SIGNOZ-API-KEY")
|
||||
if len(patToken) > 0 {
|
||||
zap.S().Debugf("Received a non-zero length PAT token")
|
||||
ctx := context.Background()
|
||||
dao := apiHandler.AppDao()
|
||||
|
||||
user, err := dao.GetUserByPAT(ctx, patToken)
|
||||
if err == nil && user != nil {
|
||||
zap.S().Debugf("Found valid PAT user: %+v", user)
|
||||
return user, nil
|
||||
}
|
||||
if err != nil {
|
||||
zap.S().Debugf("Error while getting user for PAT: %+v", err)
|
||||
}
|
||||
}
|
||||
return baseauth.GetUserFromRequest(r)
|
||||
}
|
||||
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
||||
r.Use(setTimeoutMiddleware)
|
||||
r.Use(s.analyticsMiddleware)
|
||||
r.Use(loggingMiddleware)
|
||||
|
||||
apiHandler.RegisterRoutes(r)
|
||||
apiHandler.RegisterMetricsRoutes(r)
|
||||
apiHandler.RegisterLogsRoutes(r)
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
apiHandler.RegisterMetricsRoutes(r, am)
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
@@ -271,8 +316,9 @@ 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 *basemodel.QueryRangeParamsV2
|
||||
|
||||
if path == pathToExtractBodyFrom && (r.Method == "POST") {
|
||||
if r.Body != nil {
|
||||
@@ -282,7 +328,8 @@ func extractDashboardMetaData(path string, r *http.Request) (map[string]interfac
|
||||
}
|
||||
r.Body.Close() // must close
|
||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
json.Unmarshal(bodyBytes, &requestBody)
|
||||
json.Unmarshal(bodyBytes, &postData)
|
||||
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
@@ -291,31 +338,20 @@ func extractDashboardMetaData(path string, r *http.Request) (map[string]interfac
|
||||
return nil, false
|
||||
}
|
||||
|
||||
compositeMetricQuery, compositeMetricQueryExists := requestBody["compositeMetricQuery"]
|
||||
signozMetricNotFound := false
|
||||
|
||||
signozMetricFound := false
|
||||
if postData != nil {
|
||||
signozMetricNotFound = telemetry.GetInstance().CheckSigNozMetricsV2(postData.CompositeMetricQuery)
|
||||
|
||||
if compositeMetricQueryExists {
|
||||
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
|
||||
|
||||
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.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)
|
||||
}
|
||||
@@ -369,7 +405,7 @@ func setTimeoutMiddleware(next http.Handler) http.Handler {
|
||||
// check if route is not excluded
|
||||
url := r.URL.Path
|
||||
if _, ok := baseconst.TimeoutExcludedRoutes[url]; !ok {
|
||||
ctx, cancel = context.WithTimeout(r.Context(), baseconst.ContextTimeout*time.Second)
|
||||
ctx, cancel = context.WithTimeout(r.Context(), baseconst.ContextTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
@@ -455,7 +491,7 @@ func (s *Server) Start() error {
|
||||
if port, err := utils.GetPort(s.privateConn.Addr()); err == nil {
|
||||
privatePort = port
|
||||
}
|
||||
fmt.Println("starting private http")
|
||||
|
||||
go func() {
|
||||
zap.S().Info("Starting Private HTTP server", zap.Int("port", privatePort), zap.String("addr", s.serverOptions.PrivateHostPort))
|
||||
|
||||
@@ -471,6 +507,37 @@ func (s *Server) Start() error {
|
||||
|
||||
}()
|
||||
|
||||
go func() {
|
||||
zap.S().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
|
||||
err := opamp.InitalizeServer(baseconst.OpAmpWsEndpoint, &opAmpModel.AllAgents)
|
||||
if err != nil {
|
||||
zap.S().Info("opamp ws server failed to start", err)
|
||||
s.unavailableChannel <- healthcheck.Unavailable
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
if s.httpServer != nil {
|
||||
if err := s.httpServer.Shutdown(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.privateHTTP != nil {
|
||||
if err := s.privateHTTP.Shutdown(context.Background()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
opamp.StopServer()
|
||||
|
||||
if s.ruleManager != nil {
|
||||
s.ruleManager.Stop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -480,7 +547,8 @@ func makeRulesManager(
|
||||
ruleRepoURL string,
|
||||
db *sqlx.DB,
|
||||
ch baseint.Reader,
|
||||
disableRules bool) (*rules.Manager, error) {
|
||||
disableRules bool,
|
||||
fm baseInterface.FeatureLookup) (*rules.Manager, error) {
|
||||
|
||||
// create engine
|
||||
pqle, err := pqle.FromConfigPath(promConfigPath)
|
||||
@@ -507,6 +575,7 @@ func makeRulesManager(
|
||||
Context: context.Background(),
|
||||
Logger: nil,
|
||||
DisableRules: disableRules,
|
||||
FeatureFlags: fm,
|
||||
}
|
||||
|
||||
// create Manager
|
||||
|
||||
@@ -3,6 +3,7 @@ package dao
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
@@ -24,7 +25,7 @@ type ModelDao interface {
|
||||
CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError)
|
||||
PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError)
|
||||
GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error)
|
||||
|
||||
|
||||
// org domain (auth domains) CRUD ops
|
||||
ListDomains(ctx context.Context, orgId string) ([]model.OrgDomain, basemodel.BaseApiError)
|
||||
GetDomain(ctx context.Context, id uuid.UUID) (*model.OrgDomain, basemodel.BaseApiError)
|
||||
@@ -32,4 +33,11 @@ type ModelDao interface {
|
||||
UpdateDomain(ctx context.Context, domain *model.OrgDomain) basemodel.BaseApiError
|
||||
DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
|
||||
GetDomainByEmail(ctx context.Context, email string) (*model.OrgDomain, basemodel.BaseApiError)
|
||||
|
||||
CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseApiError
|
||||
GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError)
|
||||
GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError)
|
||||
GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError)
|
||||
ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError)
|
||||
DeletePAT(ctx context.Context, id string) basemodel.BaseApiError
|
||||
}
|
||||
|
||||
@@ -48,7 +48,17 @@ func InitDB(dataSourceName string) (*modelDao, error) {
|
||||
updated_at INTEGER,
|
||||
data TEXT NOT NULL,
|
||||
FOREIGN KEY(org_id) REFERENCES organizations(id)
|
||||
);`
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS personal_access_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
);
|
||||
`
|
||||
|
||||
_, err = m.DB().Exec(table_schema)
|
||||
if err != nil {
|
||||
|
||||
106
ee/query-service/dao/sqlite/pat.go
Normal file
106
ee/query-service/dao/sqlite/pat.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (m *modelDao) CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseApiError {
|
||||
_, err := m.DB().ExecContext(ctx,
|
||||
"INSERT INTO personal_access_tokens (user_id, token, name, created_at, expires_at) VALUES ($1, $2, $3, $4, $5)",
|
||||
p.UserID,
|
||||
p.Token,
|
||||
p.Name,
|
||||
p.CreatedAt,
|
||||
p.ExpiresAt)
|
||||
if err != nil {
|
||||
zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err))
|
||||
return model.InternalError(fmt.Errorf("PAT insertion failed"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *modelDao) ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) {
|
||||
pats := []model.PAT{}
|
||||
|
||||
if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE user_id=?;`, userID); err != nil {
|
||||
zap.S().Errorf("Failed to fetch PATs for user: %s, err: %v", userID, zap.Error(err))
|
||||
return nil, model.InternalError(fmt.Errorf("failed to fetch PATs"))
|
||||
}
|
||||
return pats, nil
|
||||
}
|
||||
|
||||
func (m *modelDao) DeletePAT(ctx context.Context, id string) basemodel.BaseApiError {
|
||||
_, err := m.DB().ExecContext(ctx, `DELETE from personal_access_tokens where id=?;`, id)
|
||||
if err != nil {
|
||||
zap.S().Errorf("Failed to delete PAT, err: %v", zap.Error(err))
|
||||
return model.InternalError(fmt.Errorf("failed to delete PAT"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *modelDao) GetPAT(ctx context.Context, token string) (*model.PAT, basemodel.BaseApiError) {
|
||||
pats := []model.PAT{}
|
||||
|
||||
if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE token=?;`, token); err != nil {
|
||||
return nil, model.InternalError(fmt.Errorf("failed to fetch PAT"))
|
||||
}
|
||||
|
||||
if len(pats) != 1 {
|
||||
return nil, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: fmt.Errorf("found zero or multiple PATs with same token, %s", token),
|
||||
}
|
||||
}
|
||||
|
||||
return &pats[0], nil
|
||||
}
|
||||
|
||||
func (m *modelDao) GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError) {
|
||||
pats := []model.PAT{}
|
||||
|
||||
if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE id=?;`, id); err != nil {
|
||||
return nil, model.InternalError(fmt.Errorf("failed to fetch PAT"))
|
||||
}
|
||||
|
||||
if len(pats) != 1 {
|
||||
return nil, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: fmt.Errorf("found zero or multiple PATs with same token"),
|
||||
}
|
||||
}
|
||||
|
||||
return &pats[0], nil
|
||||
}
|
||||
|
||||
func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError) {
|
||||
users := []basemodel.UserPayload{}
|
||||
|
||||
query := `SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
u.password,
|
||||
u.created_at,
|
||||
u.profile_picture_url,
|
||||
u.org_id,
|
||||
u.group_id
|
||||
FROM users u, personal_access_tokens p
|
||||
WHERE u.id = p.user_id and p.token=?;`
|
||||
|
||||
if err := m.DB().Select(&users, query, token); err != nil {
|
||||
return nil, model.InternalError(fmt.Errorf("failed to fetch user from PAT, err: %v", err))
|
||||
}
|
||||
|
||||
if len(users) != 1 {
|
||||
return nil, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: fmt.Errorf("found zero or multiple users with same PAT token"),
|
||||
}
|
||||
}
|
||||
return &users[0], nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package license
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/license/sqlite"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -125,3 +127,79 @@ func (r *Repo) UpdatePlanDetails(ctx context.Context,
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) CreateFeature(req *basemodel.Feature) *basemodel.ApiError {
|
||||
|
||||
_, err := r.db.Exec(
|
||||
`INSERT INTO feature_status (name, active, usage, usage_limit, route)
|
||||
VALUES (?, ?, ?, ?, ?);`,
|
||||
req.Name, req.Active, req.Usage, req.UsageLimit, req.Route)
|
||||
if err != nil {
|
||||
return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) GetFeature(featureName string) (basemodel.Feature, error) {
|
||||
|
||||
var feature basemodel.Feature
|
||||
|
||||
err := r.db.Get(&feature,
|
||||
`SELECT * FROM feature_status WHERE name = ?;`, featureName)
|
||||
if err != nil {
|
||||
return feature, err
|
||||
}
|
||||
if feature.Name == "" {
|
||||
return feature, basemodel.ErrFeatureUnavailable{Key: featureName}
|
||||
}
|
||||
return feature, nil
|
||||
}
|
||||
|
||||
func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) {
|
||||
|
||||
var feature []basemodel.Feature
|
||||
|
||||
err := r.db.Select(&feature,
|
||||
`SELECT * FROM feature_status;`)
|
||||
if err != nil {
|
||||
return feature, err
|
||||
}
|
||||
|
||||
return feature, nil
|
||||
}
|
||||
|
||||
func (r *Repo) UpdateFeature(req basemodel.Feature) error {
|
||||
|
||||
_, err := r.db.Exec(
|
||||
`UPDATE feature_status SET active = ?, usage = ?, usage_limit = ?, route = ? WHERE name = ?;`,
|
||||
req.Active, req.Usage, req.UsageLimit, req.Route, req.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
|
||||
// get a feature by name, if it doesn't exist, create it. If it does exist, update it.
|
||||
for _, feature := range req {
|
||||
currentFeature, err := r.GetFeature(feature.Name)
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
err := r.CreateFeature(&feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
feature.Usage = currentFeature.Usage
|
||||
if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 {
|
||||
feature.Active = false
|
||||
}
|
||||
err = r.UpdateFeature(feature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -96,6 +96,11 @@ func (lm *Manager) SetActive(l *model.License) {
|
||||
lm.activeFeatures = l.FeatureSet
|
||||
// set default features
|
||||
setDefaultFeatures(lm)
|
||||
|
||||
err := lm.InitFeatures(lm.activeFeatures)
|
||||
if err != nil {
|
||||
zap.S().Panicf("Couldn't activate features: %v", err)
|
||||
}
|
||||
if !lm.validatorRunning {
|
||||
// we want to make sure only one validator runs,
|
||||
// we already have lock() so good to go
|
||||
@@ -106,9 +111,7 @@ func (lm *Manager) SetActive(l *model.License) {
|
||||
}
|
||||
|
||||
func setDefaultFeatures(lm *Manager) {
|
||||
for k, v := range baseconstants.DEFAULT_FEATURE_SET {
|
||||
lm.activeFeatures[k] = v
|
||||
}
|
||||
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
|
||||
}
|
||||
|
||||
// LoadActiveLicense loads the most recent active license
|
||||
@@ -123,8 +126,13 @@ func (lm *Manager) LoadActiveLicense() error {
|
||||
} else {
|
||||
zap.S().Info("No active license found, defaulting to basic plan")
|
||||
// if no active license is found, we default to basic(free) plan with all default features
|
||||
lm.activeFeatures = basemodel.BasicPlan
|
||||
lm.activeFeatures = model.BasicPlan
|
||||
setDefaultFeatures(lm)
|
||||
err := lm.InitFeatures(lm.activeFeatures)
|
||||
if err != nil {
|
||||
zap.S().Error("Couldn't initialize features: ", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -291,18 +299,31 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
|
||||
// CheckFeature will be internally used by backend routines
|
||||
// for feature gating
|
||||
func (lm *Manager) CheckFeature(featureKey string) error {
|
||||
if value, ok := lm.activeFeatures[featureKey]; ok {
|
||||
if value {
|
||||
return nil
|
||||
}
|
||||
return basemodel.ErrFeatureUnavailable{Key: featureKey}
|
||||
feature, err := lm.repo.GetFeature(featureKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if feature.Active {
|
||||
return nil
|
||||
}
|
||||
return basemodel.ErrFeatureUnavailable{Key: featureKey}
|
||||
}
|
||||
|
||||
// GetFeatureFlags returns current active features
|
||||
func (lm *Manager) GetFeatureFlags() basemodel.FeatureSet {
|
||||
return lm.activeFeatures
|
||||
func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
|
||||
return lm.repo.GetAllFeatures()
|
||||
}
|
||||
|
||||
func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
|
||||
return lm.repo.InitFeatures(features)
|
||||
}
|
||||
|
||||
func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
|
||||
return lm.repo.UpdateFeature(feature)
|
||||
}
|
||||
|
||||
func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
|
||||
return lm.repo.GetFeature(key)
|
||||
}
|
||||
|
||||
// GetRepo return the license repo
|
||||
|
||||
@@ -2,6 +2,7 @@ package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
@@ -33,5 +34,19 @@ func InitDB(db *sqlx.DB) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error in creating licenses table: %s", err.Error())
|
||||
}
|
||||
|
||||
table_schema = `CREATE TABLE IF NOT EXISTS feature_status (
|
||||
name TEXT PRIMARY KEY,
|
||||
active bool,
|
||||
usage INTEGER DEFAULT 0,
|
||||
usage_limit INTEGER DEFAULT 0,
|
||||
route TEXT
|
||||
);`
|
||||
|
||||
_, err = db.Exec(table_schema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error in creating feature_status table: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
10
ee/query-service/model/pat.go
Normal file
10
ee/query-service/model/pat.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
type PAT struct {
|
||||
Id string `json:"id" db:"id"`
|
||||
UserID string `json:"userId" db:"user_id"`
|
||||
Token string `json:"token" db:"token"`
|
||||
Name string `json:"name" db:"name"`
|
||||
CreatedAt int64 `json:"createdAt" db:"created_at"`
|
||||
ExpiresAt int64 `json:"expiresAt" db:"expires_at"` // unused as of now
|
||||
}
|
||||
@@ -11,21 +11,143 @@ const Enterprise = "ENTERPRISE_PLAN"
|
||||
const DisableUpsell = "DISABLE_UPSELL"
|
||||
|
||||
var BasicPlan = basemodel.FeatureSet{
|
||||
Basic: true,
|
||||
SSO: false,
|
||||
DisableUpsell: false,
|
||||
basemodel.Feature{
|
||||
Name: SSO,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.OSS,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: DisableUpsell,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.SmartTraceDetail,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.CustomMetricsFunction,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderPanels,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: 5,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderAlerts,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: 5,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ProPlan = basemodel.FeatureSet{
|
||||
Pro: true,
|
||||
SSO: true,
|
||||
basemodel.SmartTraceDetail: true,
|
||||
basemodel.CustomMetricsFunction: true,
|
||||
basemodel.Feature{
|
||||
Name: SSO,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.OSS,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.SmartTraceDetail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.CustomMetricsFunction,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderPanels,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderAlerts,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var EnterprisePlan = basemodel.FeatureSet{
|
||||
Enterprise: true,
|
||||
SSO: true,
|
||||
basemodel.SmartTraceDetail: true,
|
||||
basemodel.CustomMetricsFunction: true,
|
||||
basemodel.Feature{
|
||||
Name: SSO,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.OSS,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.SmartTraceDetail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.CustomMetricsFunction,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderPanels,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderAlerts,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react",
|
||||
["@babel/preset-react", { "runtime": "automatic" }],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
'plugin:sonarjs/recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings',
|
||||
'plugin:react/jsx-runtime',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
@@ -58,7 +59,7 @@ module.exports = {
|
||||
'react/no-array-index-key': 'error',
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
process.platform === 'win32' ? 'windows' : 'unix',
|
||||
process.env.platform === 'win32' ? 'windows' : 'unix',
|
||||
],
|
||||
'@typescript-eslint/default-param-last': 'off',
|
||||
|
||||
@@ -102,9 +103,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',
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && npm run commitlint
|
||||
cd frontend && yarn run commitlint --edit $1
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
network-timeout 600000
|
||||
save-prefix ""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Builder stage
|
||||
FROM node:16.15.0-slim as builder
|
||||
FROM node:16.15.0 as builder
|
||||
|
||||
# Add Maintainer Info
|
||||
LABEL maintainer="signoz"
|
||||
@@ -11,6 +11,8 @@ WORKDIR /frontend
|
||||
|
||||
# Copy the package.json and .yarnrc files prior to install dependencies
|
||||
COPY package.json ./
|
||||
# Copy lock file
|
||||
COPY yarn.lock ./
|
||||
COPY .yarnrc ./
|
||||
|
||||
# Install the dependencies and make the folder
|
||||
|
||||
@@ -15,7 +15,7 @@ const config: Config.InitialOptions = {
|
||||
useESM: true,
|
||||
},
|
||||
},
|
||||
testMatch: ['<rootDir>/src/**/?(*.)(test).(ts|js)?(x)'],
|
||||
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
||||
preset: 'ts-jest/presets/js-with-ts-esm',
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
||||
@@ -25,6 +25,7 @@ const config: Config.InitialOptions = {
|
||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
testEnvironmentOptions: {
|
||||
'jest-playwright': {
|
||||
browsers: ['chromium', 'firefox', 'webkit'],
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable object-shorthand */
|
||||
/* eslint-disable func-names */
|
||||
|
||||
/**
|
||||
* Adds custom matchers from the react testing library to all tests
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-styled-components';
|
||||
|
||||
// Mock window.matchMedia
|
||||
window.matchMedia =
|
||||
window.matchMedia ||
|
||||
function (): any {
|
||||
return {
|
||||
matches: false,
|
||||
addListener: function () {},
|
||||
removeListener: function () {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,12 +31,10 @@
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@grafana/data": "^8.4.3",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@welldone-software/why-did-you-render": "^6.2.1",
|
||||
"@xstate/react": "^3.0.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"antd": "5.0.5",
|
||||
"antd-table-saveas-excel": "2.2.1",
|
||||
"axios": "^0.21.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.0",
|
||||
@@ -44,21 +42,19 @@
|
||||
"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",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "4.3.0",
|
||||
"css-minimizer-webpack-plugin": "^3.2.0",
|
||||
"d3": "^6.2.0",
|
||||
"d3-flame-graph": "^3.1.1",
|
||||
"d3-tip": "^0.9.1",
|
||||
"dayjs": "^1.10.7",
|
||||
"dompurify": "3.0.0",
|
||||
"dotenv": "8.2.0",
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"file-loader": "6.1.1",
|
||||
"flat": "^5.0.2",
|
||||
"fontfaceobserver": "2.3.0",
|
||||
"history": "4.10.1",
|
||||
"html-webpack-plugin": "5.1.0",
|
||||
"i18next": "^21.6.12",
|
||||
@@ -70,17 +66,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",
|
||||
"papaparse": "5.4.1",
|
||||
"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-vis": "^1.11.7",
|
||||
"react-virtuoso": "4.0.3",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"stream": "^0.0.2",
|
||||
@@ -120,31 +117,33 @@
|
||||
"@commitlint/config-conventional": "^16.2.4",
|
||||
"@jest/globals": "^27.5.1",
|
||||
"@playwright/test": "^1.22.0",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "13.4.0",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/compression-webpack-plugin": "^9.0.0",
|
||||
"@types/copy-webpack-plugin": "^8.0.1",
|
||||
"@types/d3": "^6.2.0",
|
||||
"@types/d3-tip": "^3.5.5",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
"@types/flat": "^5.0.2",
|
||||
"@types/fontfaceobserver": "2.1.0",
|
||||
"@types/jest": "^27.5.1",
|
||||
"@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/papaparse": "5.3.7",
|
||||
"@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-resizable": "3.0.3",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/redux": "^3.6.0",
|
||||
"@types/styled-components": "^5.1.4",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@types/vis": "^4.21.21",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/webpack-dev-server": "^4.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"@welldone-software/why-did-you-render": "6.2.1",
|
||||
"autoprefixer": "^9.0.0",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
@@ -169,11 +168,12 @@
|
||||
"is-ci": "^3.0.1",
|
||||
"jest-playwright-preset": "^1.7.0",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"less-plugin-npm-import": "^2.1.0",
|
||||
"lint-staged": "^12.3.7",
|
||||
"portfinder-sync": "^0.0.2",
|
||||
"prettier": "2.2.1",
|
||||
"react-hooks-testing-library": "0.6.0",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-resizable": "3.0.4",
|
||||
"ts-jest": "^27.1.4",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
@@ -186,7 +186,7 @@
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0"
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { notification } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import loginApi from 'api/user/login';
|
||||
import { Logout } from 'api/utils';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { ReactChild, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { matchPath, Redirect, useLocation } from 'react-router-dom';
|
||||
@@ -47,6 +47,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const currentRoute = mapRoutes.get('current');
|
||||
|
||||
const navigateToLoginIfNotLoggedIn = (isLoggedIn = isLoggedInState): void => {
|
||||
@@ -106,7 +108,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
} else {
|
||||
Logout();
|
||||
|
||||
notification.error({
|
||||
notifications.error({
|
||||
message: response.error || t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
@@ -159,7 +161,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface PrivateRouteProps {
|
||||
children: React.ReactChild;
|
||||
children: ReactChild;
|
||||
}
|
||||
|
||||
export default PrivateRoute;
|
||||
|
||||
@@ -3,8 +3,11 @@ import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import AppLayout from 'container/AppLayout';
|
||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||
import { NotificationProvider } from 'hooks/useNotifications';
|
||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||
import history from 'lib/history';
|
||||
import React, { Suspense } from 'react';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { Suspense } from 'react';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
|
||||
import PrivateRoute from './Private';
|
||||
@@ -16,24 +19,30 @@ function App(): JSX.Element {
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
<NotificationProvider>
|
||||
<PrivateRoute>
|
||||
<ResourceProvider>
|
||||
<QueryBuilderProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</PrivateRoute>
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</QueryBuilderProvider>
|
||||
</ResourceProvider>
|
||||
</PrivateRoute>
|
||||
</NotificationProvider>
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const apiV1 = '/api/v1/';
|
||||
|
||||
export const apiV2 = '/api/v2/';
|
||||
export const apiV3 = '/api/v3/';
|
||||
export const apiAlertManager = '/api/alertmanager';
|
||||
|
||||
export default apiV1;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getAll';
|
||||
|
||||
@@ -9,11 +8,17 @@ const getAll = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/listErrors?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
const response = await axios.post(`/listErrors`, {
|
||||
start: `${props.start}`,
|
||||
end: `${props.end}`,
|
||||
order: props.order,
|
||||
orderParam: props.orderParam,
|
||||
limit: props.limit,
|
||||
offset: props.offset,
|
||||
exceptionType: props.exceptionType,
|
||||
serviceName: props.serviceName,
|
||||
tags: props.tags,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/errors/getErrorCounts';
|
||||
|
||||
@@ -9,11 +8,13 @@ const getErrorCounts = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/countErrors?${createQueryParams({
|
||||
...props,
|
||||
})}`,
|
||||
);
|
||||
const response = await axios.post(`/countErrors`, {
|
||||
start: `${props.start}`,
|
||||
end: `${props.end}`,
|
||||
exceptionType: props.exceptionType,
|
||||
serviceName: props.serviceName,
|
||||
tags: props.tags,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/features/getFeatures';
|
||||
|
||||
const getFeaturesFlags = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/featureFlags`);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getFeaturesFlags;
|
||||
@@ -9,7 +9,7 @@ import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import store from 'store';
|
||||
|
||||
import apiV1, { apiAlertManager, apiV2 } from './apiV1';
|
||||
import apiV1, { apiAlertManager, apiV2, apiV3 } from './apiV1';
|
||||
import { Logout } from './utils';
|
||||
|
||||
const interceptorsResponse = (
|
||||
@@ -109,6 +109,17 @@ ApiV2Instance.interceptors.response.use(
|
||||
);
|
||||
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
|
||||
// axios V3
|
||||
export const ApiV3Instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV3}`,
|
||||
});
|
||||
ApiV3Instance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
AxiosAlertManagerInstance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
MetricRangePayloadProps,
|
||||
MetricRangePayloadV3,
|
||||
MetricsRangeProps,
|
||||
} from 'types/api/metrics/getQueryRange';
|
||||
|
||||
export const getMetricsQueryRange = async (
|
||||
props: MetricsRangeProps,
|
||||
): Promise<SuccessResponse<MetricRangePayloadProps> | ErrorResponse> => {
|
||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`/metrics/query_range`, props);
|
||||
const response = await axios.post('/query_range', props);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
49
frontend/src/api/queryBuilder/getAggregateAttribute.ts
Normal file
49
frontend/src/api/queryBuilder/getAggregateAttribute.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ApiV3Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
// ** Helpers
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
// ** Types
|
||||
import { IGetAggregateAttributePayload } from 'types/api/queryBuilder/getAggregatorAttribute';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
IQueryAutocompleteResponse,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const getAggregateAttribute = async ({
|
||||
aggregateOperator,
|
||||
searchText,
|
||||
dataSource,
|
||||
}: IGetAggregateAttributePayload): Promise<
|
||||
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response: AxiosResponse<{
|
||||
data: IQueryAutocompleteResponse;
|
||||
}> = await ApiV3Instance.get(
|
||||
`autocomplete/aggregate_attributes?${createQueryParams({
|
||||
aggregateOperator,
|
||||
searchText,
|
||||
dataSource,
|
||||
})}`,
|
||||
);
|
||||
|
||||
const payload: BaseAutocompleteData[] =
|
||||
response.data.data.attributeKeys?.map(({ id: _, ...item }) => ({
|
||||
...item,
|
||||
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.statusText,
|
||||
payload: { attributeKeys: payload },
|
||||
};
|
||||
} catch (e) {
|
||||
return ErrorResponseHandler(e as AxiosError);
|
||||
}
|
||||
};
|
||||
51
frontend/src/api/queryBuilder/getAttributeKeys.ts
Normal file
51
frontend/src/api/queryBuilder/getAttributeKeys.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ApiV3Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
// ** Types
|
||||
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
IQueryAutocompleteResponse,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const getAggregateKeys = async ({
|
||||
aggregateOperator,
|
||||
searchText,
|
||||
dataSource,
|
||||
aggregateAttribute,
|
||||
tagType,
|
||||
}: IGetAttributeKeysPayload): Promise<
|
||||
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response: AxiosResponse<{
|
||||
data: IQueryAutocompleteResponse;
|
||||
}> = await ApiV3Instance.get(
|
||||
`autocomplete/attribute_keys?${createQueryParams({
|
||||
aggregateOperator,
|
||||
searchText,
|
||||
dataSource,
|
||||
aggregateAttribute,
|
||||
})}&tagType=${tagType}`,
|
||||
);
|
||||
|
||||
const payload: BaseAutocompleteData[] =
|
||||
response.data.data.attributeKeys?.map(({ id: _, ...item }) => ({
|
||||
...item,
|
||||
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.statusText,
|
||||
payload: { attributeKeys: payload },
|
||||
};
|
||||
} catch (e) {
|
||||
return ErrorResponseHandler(e as AxiosError);
|
||||
}
|
||||
};
|
||||
42
frontend/src/api/queryBuilder/getAttributesValues.ts
Normal file
42
frontend/src/api/queryBuilder/getAttributesValues.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ApiV3Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IAttributeValuesResponse,
|
||||
IGetAttributeValuesPayload,
|
||||
} from 'types/api/queryBuilder/getAttributesValues';
|
||||
|
||||
export const getAttributesValues = async ({
|
||||
aggregateOperator,
|
||||
dataSource,
|
||||
aggregateAttribute,
|
||||
attributeKey,
|
||||
filterAttributeKeyDataType,
|
||||
tagType,
|
||||
searchText,
|
||||
}: IGetAttributeValuesPayload): Promise<
|
||||
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await ApiV3Instance.get(
|
||||
`/autocomplete/attribute_values?${createQueryParams({
|
||||
aggregateOperator,
|
||||
dataSource,
|
||||
aggregateAttribute,
|
||||
attributeKey,
|
||||
searchText,
|
||||
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -32,6 +32,7 @@ const getFilters = async (
|
||||
maxDuration: String((duration.duration || [])[0] || ''),
|
||||
minDuration: String((duration.duration || [])[1] || ''),
|
||||
exclude,
|
||||
spanKind: props.spanKind,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,9 +10,11 @@ const getSpans = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||
Key: e.Key[0],
|
||||
Key: `${e.Key}.(string)`,
|
||||
Operator: e.Operator,
|
||||
Values: e.Values,
|
||||
StringValues: e.StringValues,
|
||||
NumberValues: e.NumberValues,
|
||||
BoolValues: e.BoolValues,
|
||||
}));
|
||||
|
||||
const exclude: string[] = [];
|
||||
@@ -35,13 +37,14 @@ const getSpans = async (
|
||||
start: String(props.start),
|
||||
end: String(props.end),
|
||||
function: props.function,
|
||||
groupBy: props.groupBy,
|
||||
groupBy: props.groupBy === 'none' ? '' : props.groupBy,
|
||||
step: props.step,
|
||||
tags: updatedSelectedTags,
|
||||
...nonDuration,
|
||||
maxDuration: String((duration.duration || [])[0] || ''),
|
||||
minDuration: String((duration.duration || [])[1] || ''),
|
||||
exclude,
|
||||
spanKind: props.spanKind,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -28,9 +28,11 @@ const getSpanAggregate = async (
|
||||
});
|
||||
|
||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||
Key: e.Key[0],
|
||||
Key: `${e.Key}.(string)`,
|
||||
Operator: e.Operator,
|
||||
Values: e.Values,
|
||||
StringValues: e.StringValues,
|
||||
NumberValues: e.NumberValues,
|
||||
BoolValues: e.BoolValues,
|
||||
}));
|
||||
|
||||
const other = Object.fromEntries(props.selectedFilter);
|
||||
@@ -46,6 +48,7 @@ const getSpanAggregate = async (
|
||||
maxDuration: String((duration.duration || [])[0] || ''),
|
||||
minDuration: String((duration.duration || [])[1] || ''),
|
||||
exclude,
|
||||
spanKind: props.spanKind,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -32,6 +32,7 @@ const getTagFilters = async (
|
||||
maxDuration: String((duration.duration || [])[0] || ''),
|
||||
minDuration: String((duration.duration || [])[1] || ''),
|
||||
exclude,
|
||||
spanKind: props.spanKind,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,9 +11,12 @@ 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,
|
||||
},
|
||||
spanKind: props.spanKind,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
|
||||
@@ -14,7 +14,6 @@ const signup = async (
|
||||
const response = await axios.post(`/register`, {
|
||||
...props,
|
||||
});
|
||||
console.log(' response.data.data', response.data.data);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
function TimeSeries(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
function Value(props: ValueProps): JSX.Element {
|
||||
const { fillColor } = props;
|
||||
@@ -20,7 +20,7 @@ function Value(props: ValueProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface ValueProps {
|
||||
fillColor: React.CSSProperties['color'];
|
||||
fillColor: CSSProperties['color'];
|
||||
}
|
||||
|
||||
export default Value;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
function NotFound(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
function SomethingWentWrong(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
function UnAuthorized(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
|
||||
59
frontend/src/components/Editor/Editor.test.tsx
Normal file
59
frontend/src/components/Editor/Editor.test.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import Editor from './index';
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Editor', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
const { container } = render(<Editor value="" />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly with custom props', () => {
|
||||
const customProps = {
|
||||
value: 'test',
|
||||
language: 'javascript',
|
||||
readOnly: true,
|
||||
height: '50vh',
|
||||
options: { minimap: { enabled: false } },
|
||||
};
|
||||
const { container } = render(
|
||||
<Editor
|
||||
value={customProps.value}
|
||||
height={customProps.height}
|
||||
language={customProps.language}
|
||||
options={customProps.options}
|
||||
readOnly={customProps.readOnly}
|
||||
/>,
|
||||
);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders with dark mode theme', () => {
|
||||
(useIsDarkMode as jest.Mock).mockImplementation(() => true);
|
||||
|
||||
const { container } = render(<Editor value="dark mode text" />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders with light mode theme', () => {
|
||||
(useIsDarkMode as jest.Mock).mockImplementation(() => false);
|
||||
|
||||
const { container } = render(<Editor value="light mode text" />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays "Loading..." message initially', () => {
|
||||
const { rerender } = render(<Editor value="initial text" />);
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
|
||||
rerender(<Editor value="initial text" />);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Editor renders correctly with custom props 1`] = `
|
||||
<div>
|
||||
<section
|
||||
style="display: flex; position: relative; text-align: initial; width: 100%; height: 50vh;"
|
||||
>
|
||||
<div
|
||||
style="display: flex; height: 100%; width: 100%; justify-content: center; align-items: center;"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
<div
|
||||
style="width: 100%; display: none;"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Editor renders correctly with default props 1`] = `
|
||||
<div>
|
||||
<section
|
||||
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"
|
||||
>
|
||||
<div
|
||||
style="display: flex; height: 100%; width: 100%; justify-content: center; align-items: center;"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
<div
|
||||
style="width: 100%; display: none;"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Editor renders with dark mode theme 1`] = `
|
||||
<div>
|
||||
<section
|
||||
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"
|
||||
>
|
||||
<div
|
||||
style="display: flex; height: 100%; width: 100%; justify-content: center; align-items: center;"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
<div
|
||||
style="width: 100%; display: none;"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Editor renders with light mode theme 1`] = `
|
||||
<div>
|
||||
<section
|
||||
style="display: flex; position: relative; text-align: initial; width: 100%; height: 40vh;"
|
||||
>
|
||||
<div
|
||||
style="display: flex; height: 100%; width: 100%; justify-content: center; align-items: center;"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
<div
|
||||
style="width: 100%; display: none;"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,6 +1,6 @@
|
||||
import MEditor, { EditorProps } from '@monaco-editor/react';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
function Editor({
|
||||
value,
|
||||
@@ -11,16 +11,27 @@ function Editor({
|
||||
options,
|
||||
}: MEditorProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onChangeHandler = (newValue?: string): void => {
|
||||
if (readOnly) return;
|
||||
|
||||
if (typeof newValue === 'string' && onChange) onChange(newValue);
|
||||
};
|
||||
|
||||
const editorOptions = useMemo(
|
||||
() => ({ fontSize: 16, automaticLayout: true, readOnly, ...options }),
|
||||
[options, readOnly],
|
||||
);
|
||||
|
||||
return (
|
||||
<MEditor
|
||||
theme={isDarkMode ? 'vs-dark' : 'vs-light'}
|
||||
language={language}
|
||||
value={value}
|
||||
options={{ fontSize: 16, automaticLayout: true, readOnly, ...options }}
|
||||
options={editorOptions}
|
||||
height={height}
|
||||
onChange={(newValue): void => {
|
||||
if (typeof newValue === 'string') onChange(newValue);
|
||||
}}
|
||||
onChange={onChangeHandler}
|
||||
data-testid="monaco-editor"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +39,7 @@ function Editor({
|
||||
interface MEditorProps {
|
||||
value: string;
|
||||
language?: string;
|
||||
onChange: (value: string) => void;
|
||||
onChange?: (value: string) => void;
|
||||
readOnly?: boolean;
|
||||
height?: string;
|
||||
options?: EditorProps['options'];
|
||||
@@ -39,6 +50,7 @@ Editor.defaultProps = {
|
||||
readOnly: false,
|
||||
height: '40vh',
|
||||
options: {},
|
||||
onChange: (): void => {},
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
||||
321
frontend/src/components/Graph/Plugin/DragSelect.ts
Normal file
321
frontend/src/components/Graph/Plugin/DragSelect.ts
Normal 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;
|
||||
};
|
||||
164
frontend/src/components/Graph/Plugin/IntersectionCursor.ts
Normal file
164
frontend/src/components/Graph/Plugin/IntersectionCursor.ts
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
46
frontend/src/components/Graph/Plugin/Tooltip.ts
Normal file
46
frontend/src/components/Graph/Plugin/Tooltip.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
ActiveElement,
|
||||
ChartTypeRegistry,
|
||||
Point,
|
||||
TooltipModel,
|
||||
TooltipXAlignment,
|
||||
TooltipYAlignment,
|
||||
} from 'chart.js';
|
||||
|
||||
export function TooltipPosition(
|
||||
this: TooltipModel<keyof ChartTypeRegistry>,
|
||||
_: readonly ActiveElement[],
|
||||
eventPosition: Point,
|
||||
): ITooltipPosition {
|
||||
const {
|
||||
chartArea: { width },
|
||||
scales: { x, y },
|
||||
} = this.chart;
|
||||
|
||||
const valueForPixelOnX = Number(x.getValueForPixel(eventPosition.x));
|
||||
const valueForPixelonY = Number(y.getValueForPixel(eventPosition.y));
|
||||
|
||||
const rightmostWidth = this.width + x.getPixelForValue(valueForPixelOnX) + 20;
|
||||
|
||||
if (rightmostWidth > width) {
|
||||
return {
|
||||
x: x.getPixelForValue(valueForPixelOnX) - 20,
|
||||
y: y.getPixelForValue(valueForPixelonY) + 10,
|
||||
xAlign: 'right',
|
||||
yAlign: 'top',
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: x.getPixelForValue(valueForPixelOnX) + 20,
|
||||
y: y.getPixelForValue(valueForPixelonY) + 10,
|
||||
xAlign: 'left',
|
||||
yAlign: 'top',
|
||||
};
|
||||
}
|
||||
|
||||
interface ITooltipPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
xAlign: TooltipXAlignment;
|
||||
yAlign: TooltipYAlignment;
|
||||
}
|
||||
20
frontend/src/components/Graph/Plugin/utils.ts
Normal file
20
frontend/src/components/Graph/Plugin/utils.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
8
frontend/src/components/Graph/helpers.ts
Normal file
8
frontend/src/components/Graph/helpers.ts
Normal 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;
|
||||
};
|
||||
@@ -23,12 +23,28 @@ import {
|
||||
} from 'chart.js';
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
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 { TooltipPosition as TooltipPositionHandler } from './Plugin/Tooltip';
|
||||
import { LegendsContainer } from './styles';
|
||||
import { useXAxisTimeUnit } from './xAxisConfig';
|
||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||
@@ -52,6 +68,8 @@ Chart.register(
|
||||
annotationPlugin,
|
||||
);
|
||||
|
||||
Tooltip.positioners.custom = TooltipPositionHandler;
|
||||
|
||||
function Graph({
|
||||
animate = true,
|
||||
data,
|
||||
@@ -64,7 +82,10 @@ function Graph({
|
||||
forceReRender,
|
||||
staticLine,
|
||||
containerHeight,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
}: GraphProps): JSX.Element {
|
||||
const nearestDatasetIndex = useRef<null | number>(null);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -91,7 +112,7 @@ function Graph({
|
||||
}
|
||||
|
||||
if (chartRef.current !== null) {
|
||||
const options: ChartOptions = {
|
||||
const options: CustomChartOptions = {
|
||||
animation: {
|
||||
duration: animate ? 200 : 0,
|
||||
},
|
||||
@@ -135,6 +156,10 @@ function Graph({
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title(context) {
|
||||
const date = dayjs(context[0].parsed.x);
|
||||
return date.format('MMM DD, YYYY, HH:mm:ss');
|
||||
},
|
||||
label(context) {
|
||||
let label = context.dataset.label || '';
|
||||
|
||||
@@ -144,10 +169,28 @@ function Graph({
|
||||
if (context.parsed.y !== null) {
|
||||
label += getToolTipValue(context.parsed.y.toString(), yAxisUnit);
|
||||
}
|
||||
|
||||
return label;
|
||||
},
|
||||
labelTextColor(labelData) {
|
||||
if (labelData.datasetIndex === nearestDatasetIndex.current) {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
|
||||
return 'rgba(255, 255, 255, 0.75)';
|
||||
},
|
||||
},
|
||||
position: 'custom',
|
||||
},
|
||||
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||
!!onDragSelect,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
),
|
||||
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
|
||||
!!onDragSelect,
|
||||
currentTheme === 'dark' ? 'white' : 'black',
|
||||
),
|
||||
},
|
||||
layout: {
|
||||
padding: 0,
|
||||
@@ -177,6 +220,7 @@ function Graph({
|
||||
},
|
||||
},
|
||||
type: 'time',
|
||||
ticks: { color: getAxisLabelColor(currentTheme) },
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
@@ -185,6 +229,7 @@ function Graph({
|
||||
color: getGridColor(),
|
||||
},
|
||||
ticks: {
|
||||
color: getAxisLabelColor(currentTheme),
|
||||
// Include a dollar sign in the ticks
|
||||
callback(value) {
|
||||
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
||||
@@ -200,18 +245,50 @@ function Graph({
|
||||
tension: 0,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
},
|
||||
point: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
hoverBackgroundColor: (ctx: any) => {
|
||||
if (ctx?.element?.options?.borderColor) {
|
||||
return ctx.element.options.borderColor;
|
||||
}
|
||||
return 'rgba(0,0,0,0.1)';
|
||||
},
|
||||
hoverRadius: 5,
|
||||
},
|
||||
},
|
||||
onClick: (event, element, chart) => {
|
||||
if (onClickHandler) {
|
||||
onClickHandler(event, element, chart, data);
|
||||
}
|
||||
},
|
||||
onHover: (event, _, chart) => {
|
||||
if (event.native) {
|
||||
const interactions = chart.getElementsAtEventForMode(
|
||||
event.native,
|
||||
'nearest',
|
||||
{
|
||||
intersect: false,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
if (interactions[0]) {
|
||||
nearestDatasetIndex.current = interactions[0].datasetIndex;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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, {
|
||||
@@ -234,6 +311,9 @@ function Graph({
|
||||
yAxisUnit,
|
||||
onClickHandler,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
currentTheme,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -248,6 +328,19 @@ function Graph({
|
||||
);
|
||||
}
|
||||
|
||||
declare module 'chart.js' {
|
||||
interface TooltipPositionerMap {
|
||||
custom: TooltipPositionerFunction<ChartType>;
|
||||
}
|
||||
}
|
||||
|
||||
type CustomChartOptions = ChartOptions & {
|
||||
plugins: {
|
||||
[dragSelectPluginId]: DragSelectPluginOptions | false;
|
||||
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
|
||||
};
|
||||
};
|
||||
|
||||
interface GraphProps {
|
||||
animate?: boolean;
|
||||
type: ChartType;
|
||||
@@ -260,6 +353,8 @@ interface GraphProps {
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
containerHeight?: string | number;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
dragSelectColor?: string;
|
||||
}
|
||||
|
||||
export interface StaticLineProps {
|
||||
@@ -286,6 +381,11 @@ Graph.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
forceReRender: undefined,
|
||||
staticLine: undefined,
|
||||
containerHeight: '85%',
|
||||
containerHeight: '90%',
|
||||
onDragSelect: undefined,
|
||||
dragSelectColor: undefined,
|
||||
};
|
||||
export default Graph;
|
||||
|
||||
export default memo(Graph, (prevProps, nextProps) =>
|
||||
isEqual(prevProps.data, nextProps.data),
|
||||
);
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
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.3rem;
|
||||
}
|
||||
::-webkit-scrollbar:horizontal {
|
||||
height: 0.3rem;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: ${themeColors.royalGrey};
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: ${themeColors.matterhornGrey};
|
||||
}
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
-ms-overflow-style: none !important; /* IE and Edge */
|
||||
scrollbar-width: none !important; /* Firefox */
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { Form, Input, InputProps, InputRef } from 'antd';
|
||||
import React from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
FocusEventHandler,
|
||||
KeyboardEventHandler,
|
||||
LegacyRef,
|
||||
ReactNode,
|
||||
Ref,
|
||||
} from 'react';
|
||||
|
||||
function InputComponent({
|
||||
value,
|
||||
@@ -22,7 +29,7 @@ function InputComponent({
|
||||
type={type}
|
||||
onChange={onChangeHandler}
|
||||
value={value}
|
||||
ref={ref as React.Ref<InputRef>}
|
||||
ref={ref as Ref<InputRef>}
|
||||
size={size}
|
||||
addonBefore={addonBefore}
|
||||
onBlur={onBlurHandler}
|
||||
@@ -37,15 +44,15 @@ function InputComponent({
|
||||
interface InputComponentProps extends InputProps {
|
||||
value: InputProps['value'];
|
||||
type?: InputProps['type'];
|
||||
onChangeHandler?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
onChangeHandler?: ChangeEventHandler<HTMLInputElement>;
|
||||
placeholder?: InputProps['placeholder'];
|
||||
ref?: React.LegacyRef<InputRef>;
|
||||
ref?: LegacyRef<InputRef>;
|
||||
size?: InputProps['size'];
|
||||
onBlurHandler?: React.FocusEventHandler<HTMLInputElement>;
|
||||
onPressEnterHandler?: React.KeyboardEventHandler<HTMLInputElement>;
|
||||
onBlurHandler?: FocusEventHandler<HTMLInputElement>;
|
||||
onPressEnterHandler?: KeyboardEventHandler<HTMLInputElement>;
|
||||
label?: string;
|
||||
labelOnTop?: boolean;
|
||||
addonBefore?: React.ReactNode;
|
||||
addonBefore?: ReactNode;
|
||||
}
|
||||
|
||||
InputComponent.defaultProps = {
|
||||
|
||||
49
frontend/src/components/Loadable/Loadable.test.tsx
Normal file
49
frontend/src/components/Loadable/Loadable.test.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/react';
|
||||
import React, { ComponentType, Suspense } from 'react';
|
||||
|
||||
import Loadable from './index';
|
||||
|
||||
// Sample component to be loaded lazily
|
||||
function SampleComponent(): JSX.Element {
|
||||
return <div>Sample Component</div>;
|
||||
}
|
||||
|
||||
const loadSampleComponent = (): Promise<{
|
||||
default: ComponentType;
|
||||
}> =>
|
||||
new Promise<{ default: ComponentType }>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ default: SampleComponent });
|
||||
}, 500);
|
||||
});
|
||||
|
||||
describe('Loadable', () => {
|
||||
it('should render the lazily loaded component', async () => {
|
||||
const LoadableSampleComponent = Loadable(loadSampleComponent);
|
||||
|
||||
const { container } = render(
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<LoadableSampleComponent />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
await waitForElementToBeRemoved(() => screen.queryByText('Loading...'));
|
||||
|
||||
expect(container.querySelector('div')).toHaveTextContent('Sample Component');
|
||||
});
|
||||
|
||||
it('should call lazy with the provided import path', () => {
|
||||
const reactLazySpy = jest.spyOn(React, 'lazy');
|
||||
Loadable(loadSampleComponent);
|
||||
|
||||
expect(reactLazySpy).toHaveBeenCalledTimes(1);
|
||||
expect(reactLazySpy).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
reactLazySpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ComponentType, lazy } from 'react';
|
||||
import { ComponentType, lazy, LazyExoticComponent } from 'react';
|
||||
|
||||
function Loadable(importPath: {
|
||||
(): LoadableProps;
|
||||
}): React.LazyExoticComponent<LazyComponent> {
|
||||
}): LazyExoticComponent<LazyComponent> {
|
||||
return lazy(() => importPath());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Button, Popover } from 'antd';
|
||||
import { Popover } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { memo, ReactNode, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
function AddToQueryHOC({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
@@ -14,7 +17,6 @@ function AddToQueryHOC({
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const generatedQuery = useMemo(
|
||||
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
|
||||
@@ -29,29 +31,27 @@ function AddToQueryHOC({
|
||||
} else {
|
||||
updatedQueryString += ` AND ${generatedQuery}`;
|
||||
}
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: updatedQueryString,
|
||||
});
|
||||
}, [dispatch, generatedQuery, queryString]);
|
||||
|
||||
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
|
||||
}, [generatedQuery, queryString]);
|
||||
|
||||
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
||||
fieldKey,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Button size="small" type="text" onClick={handleQueryAdd}>
|
||||
<ButtonContainer size="small" type="text" onClick={handleQueryAdd}>
|
||||
<Popover placement="top" content={popOverContent}>
|
||||
{children}
|
||||
</Popover>
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface AddToQueryHOCProps {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default memo(AddToQueryHOC);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { CategoryHeadingText } from './styles';
|
||||
|
||||
interface ICategoryHeadingProps {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element {
|
||||
return <CategoryHeadingText type="secondary">{children}</CategoryHeadingText>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { notification, Popover } from 'antd';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
function CopyClipboardHOC({
|
||||
@@ -7,21 +8,21 @@ function CopyClipboardHOC({
|
||||
children,
|
||||
}: CopyClipboardHOCProps): JSX.Element {
|
||||
const [value, setCopy] = useCopyToClipboard();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
useEffect(() => {
|
||||
if (value.value) {
|
||||
notification.success({
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
}
|
||||
}, [value]);
|
||||
}, [value, notifications]);
|
||||
|
||||
const onClick = useCallback((): void => {
|
||||
setCopy(textToCopy);
|
||||
}, [setCopy, textToCopy]);
|
||||
|
||||
return (
|
||||
<span onClick={onClick} onKeyDown={onClick} role="button" tabIndex={0}>
|
||||
<span onClick={onClick} role="presentation" tabIndex={-1}>
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
|
||||
@@ -34,7 +35,7 @@ function CopyClipboardHOC({
|
||||
|
||||
interface CopyClipboardHOCProps {
|
||||
textToCopy: string;
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default CopyClipboardHOC;
|
||||
|
||||
@@ -1,84 +1,95 @@
|
||||
import { blue, grey, orange } from '@ant-design/colors';
|
||||
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, Divider, Row, Typography } from 'antd';
|
||||
import { map } from 'd3';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
// utils
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
// interfaces
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
// components
|
||||
import AddToQueryHOC from '../AddToQueryHOC';
|
||||
import CopyClipboardHOC from '../CopyClipboardHOC';
|
||||
import { Container, Text, TextContainer } from './styles';
|
||||
// styles
|
||||
import {
|
||||
Container,
|
||||
LogContainer,
|
||||
LogText,
|
||||
SelectedLog,
|
||||
Text,
|
||||
TextContainer,
|
||||
} from './styles';
|
||||
import { isValidLogField } from './util';
|
||||
|
||||
const convert = new Convert();
|
||||
|
||||
interface LogFieldProps {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
}
|
||||
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
||||
const html = useMemo(
|
||||
() => ({
|
||||
__html: convert.toHtml(dompurify.sanitize(fieldValue)),
|
||||
}),
|
||||
[fieldValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<TextContainer>
|
||||
<Text ellipsis type="secondary">
|
||||
{fieldKey}
|
||||
{`${fieldKey}: `}
|
||||
</Text>
|
||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||
<Typography.Text ellipsis>
|
||||
{': '}
|
||||
{fieldValue}
|
||||
</Typography.Text>
|
||||
<LogText dangerouslySetInnerHTML={html} />
|
||||
</CopyClipboardHOC>
|
||||
</TextContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function LogSelectedField({
|
||||
fieldKey = '',
|
||||
fieldValue = '',
|
||||
}: LogFieldProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<SelectedLog>
|
||||
<AddToQueryHOC fieldKey={fieldKey} fieldValue={fieldValue}>
|
||||
<Typography.Text>
|
||||
{`"`}
|
||||
<span style={{ color: blue[4] }}>{fieldKey}</span>
|
||||
{`"`}
|
||||
</Typography.Text>
|
||||
</AddToQueryHOC>
|
||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||
<Typography.Text ellipsis>
|
||||
<span>
|
||||
{': '}
|
||||
{typeof fieldValue === 'string' && `"`}
|
||||
</span>
|
||||
<span style={{ color: orange[6] }}>{fieldValue}</span>
|
||||
{typeof fieldValue === 'string' && `"`}
|
||||
<span>{': '}</span>
|
||||
<span style={{ color: orange[6] }}>{fieldValue || "''"}</span>
|
||||
</Typography.Text>
|
||||
</CopyClipboardHOC>
|
||||
</div>
|
||||
</SelectedLog>
|
||||
);
|
||||
}
|
||||
|
||||
interface LogItemProps {
|
||||
interface ListLogViewProps {
|
||||
logData: ILog;
|
||||
}
|
||||
function LogItem({ logData }: LogItemProps): JSX.Element {
|
||||
function ListLogView({ logData }: ListLogViewProps): JSX.Element {
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const handleDetailedView = useCallback(() => {
|
||||
dispatch({
|
||||
@@ -89,40 +100,41 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
|
||||
|
||||
const handleCopyJSON = (): void => {
|
||||
setCopy(JSON.stringify(logData, null, 2));
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
const updatedSelecedFields = useMemo(
|
||||
() => selected.filter((e) => e.name !== 'id'),
|
||||
[selected],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div style={{ maxWidth: '100%' }}>
|
||||
<div>
|
||||
{'{'}
|
||||
<div style={{ marginLeft: '0.5rem' }}>
|
||||
<LogGeneralField
|
||||
fieldKey="log"
|
||||
fieldValue={flattenLogData.body as never}
|
||||
/>
|
||||
<div>
|
||||
<LogContainer>
|
||||
<>
|
||||
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField
|
||||
fieldKey="stream"
|
||||
fieldValue={flattenLogData.stream as never}
|
||||
/>
|
||||
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
|
||||
)}
|
||||
<LogGeneralField
|
||||
fieldKey="timestamp"
|
||||
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
||||
/>
|
||||
</div>
|
||||
{'}'}
|
||||
</div>
|
||||
</>
|
||||
</LogContainer>
|
||||
<div>
|
||||
{map(selected, (field) => {
|
||||
return isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
{updatedSelecedFields.map((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 }} />
|
||||
@@ -131,23 +143,23 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={handleDetailedView}
|
||||
style={{ color: blue[5], padding: 0, margin: 0 }}
|
||||
style={{ color: blue[5] }}
|
||||
icon={<ExpandAltOutlined />}
|
||||
>
|
||||
{' '}
|
||||
<ExpandAltOutlined /> View Details
|
||||
View Details
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={handleCopyJSON}
|
||||
style={{ padding: 0, margin: 0, color: grey[1] }}
|
||||
style={{ color: grey[1] }}
|
||||
icon={<CopyFilled />}
|
||||
>
|
||||
{' '}
|
||||
<CopyFilled /> Copy JSON
|
||||
Copy JSON
|
||||
</Button>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogItem;
|
||||
export default ListLogView;
|
||||
@@ -29,3 +29,21 @@ export const TextContainer = styled.div`
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LogContainer = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
`;
|
||||
|
||||
export const LogText = styled.div`
|
||||
display: inline-block;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const SelectedLog = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
3
frontend/src/components/Logs/RawLogView/config.ts
Normal file
3
frontend/src/components/Logs/RawLogView/config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const rawLineStyle: CSSProperties = {};
|
||||
63
frontend/src/components/Logs/RawLogView/index.tsx
Normal file
63
frontend/src/components/Logs/RawLogView/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ExpandAltOutlined } from '@ant-design/icons';
|
||||
// const Convert = require('ansi-to-html');
|
||||
import Convert from 'ansi-to-html';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
// styles
|
||||
import {
|
||||
ExpandIconWrapper,
|
||||
RawLogContent,
|
||||
RawLogViewContainer,
|
||||
} from './styles';
|
||||
|
||||
const convert = new Convert();
|
||||
|
||||
interface RawLogViewProps {
|
||||
data: ILog;
|
||||
linesPerRow: number;
|
||||
onClickExpand: (log: ILog) => void;
|
||||
}
|
||||
|
||||
function RawLogView(props: RawLogViewProps): JSX.Element {
|
||||
const { data, linesPerRow, onClickExpand } = props;
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const text = useMemo(
|
||||
() => `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`,
|
||||
[data.timestamp, data.body],
|
||||
);
|
||||
|
||||
const handleClickExpand = useCallback(() => {
|
||||
onClickExpand(data);
|
||||
}, [onClickExpand, data]);
|
||||
|
||||
const html = useMemo(
|
||||
() => ({
|
||||
__html: convert.toHtml(dompurify.sanitize(text)),
|
||||
}),
|
||||
[text],
|
||||
);
|
||||
|
||||
return (
|
||||
<RawLogViewContainer
|
||||
onClick={handleClickExpand}
|
||||
wrap={false}
|
||||
align="middle"
|
||||
$isDarkMode={isDarkMode}
|
||||
>
|
||||
<ExpandIconWrapper flex="30px">
|
||||
<ExpandAltOutlined />
|
||||
</ExpandIconWrapper>
|
||||
<RawLogContent linesPerRow={linesPerRow} dangerouslySetInnerHTML={html} />
|
||||
</RawLogViewContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default RawLogView;
|
||||
46
frontend/src/components/Logs/RawLogView/styles.ts
Normal file
46
frontend/src/components/Logs/RawLogView/styles.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { Col, Row } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const RawLogViewContainer = styled(Row)<{ $isDarkMode: boolean }>`
|
||||
width: 100%;
|
||||
font-weight: 700;
|
||||
font-size: 0.625rem;
|
||||
line-height: 1.25rem;
|
||||
|
||||
transition: background-color 0.2s ease-in;
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ $isDarkMode }): string =>
|
||||
$isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0, 0, 0, 0.1)'};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ExpandIconWrapper = styled(Col)`
|
||||
color: ${blue[6]};
|
||||
padding: 0.25rem 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
interface RawLogContentProps {
|
||||
linesPerRow: number;
|
||||
}
|
||||
|
||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
margin-bottom: 0;
|
||||
font-family: Fira Code, monospace;
|
||||
font-weight: 300;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||
line-clamp: ${(props): number => props.linesPerRow};
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
|
||||
cursor: pointer;
|
||||
`;
|
||||
18
frontend/src/components/Logs/TableView/config.ts
Normal file
18
frontend/src/components/Logs/TableView/config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { TableProps } from 'antd';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const defaultCellStyle: CSSProperties = {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 6,
|
||||
paddingRight: 8,
|
||||
paddingLeft: 8,
|
||||
};
|
||||
|
||||
export const defaultTableStyle: CSSProperties = {
|
||||
minWidth: '40rem',
|
||||
maxWidth: '40rem',
|
||||
};
|
||||
|
||||
export const tableScroll: TableProps<Record<string, unknown>>['scroll'] = {
|
||||
x: true,
|
||||
};
|
||||
127
frontend/src/components/Logs/TableView/index.tsx
Normal file
127
frontend/src/components/Logs/TableView/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { ExpandAltOutlined } from '@ant-design/icons';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Table, Typography } from 'antd';
|
||||
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
// utils
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useMemo } from 'react';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
// styles
|
||||
import { ExpandIconWrapper } from '../RawLogView/styles';
|
||||
// config
|
||||
import { defaultCellStyle, defaultTableStyle, tableScroll } from './config';
|
||||
import { TableBodyContent } from './styles';
|
||||
|
||||
type ColumnTypeRender<T = unknown> = ReturnType<
|
||||
NonNullable<ColumnType<T>['render']>
|
||||
>;
|
||||
|
||||
type LogsTableViewProps = {
|
||||
logs: ILog[];
|
||||
fields: IField[];
|
||||
linesPerRow: number;
|
||||
onClickExpand: (log: ILog) => void;
|
||||
};
|
||||
|
||||
const convert = new Convert();
|
||||
|
||||
function LogsTableView(props: LogsTableViewProps): JSX.Element {
|
||||
const { logs, fields, linesPerRow, onClickExpand } = props;
|
||||
|
||||
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
||||
logs,
|
||||
]);
|
||||
|
||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||
.filter((e) => e.name !== 'id')
|
||||
.map(({ name }) => ({
|
||||
title: name,
|
||||
dataIndex: name,
|
||||
key: name,
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: defaultCellStyle,
|
||||
},
|
||||
children: (
|
||||
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
||||
{field}
|
||||
</Typography.Paragraph>
|
||||
),
|
||||
}),
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'id',
|
||||
key: 'expand',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: defaultCellStyle,
|
||||
},
|
||||
children: (
|
||||
<ExpandIconWrapper
|
||||
onClick={(): void => {
|
||||
onClickExpand((item as unknown) as ILog);
|
||||
}}
|
||||
>
|
||||
<ExpandAltOutlined />
|
||||
</ExpandIconWrapper>
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'timestamp',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const date = dayjs(field / 1e6).format();
|
||||
return {
|
||||
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
|
||||
};
|
||||
},
|
||||
},
|
||||
...fieldColumns,
|
||||
{
|
||||
title: 'body',
|
||||
dataIndex: 'body',
|
||||
key: 'body',
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: defaultTableStyle,
|
||||
},
|
||||
children: (
|
||||
<TableBodyContent
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: convert.toHtml(dompurify.sanitize(field)),
|
||||
}}
|
||||
linesPerRow={linesPerRow}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
},
|
||||
];
|
||||
}, [fields, linesPerRow, onClickExpand]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
size="small"
|
||||
columns={columns}
|
||||
dataSource={flattenLogData}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
bordered
|
||||
scroll={tableScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogsTableView;
|
||||
19
frontend/src/components/Logs/TableView/styles.ts
Normal file
19
frontend/src/components/Logs/TableView/styles.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface TableBodyContentProps {
|
||||
linesPerRow: number;
|
||||
}
|
||||
|
||||
export const TableBodyContent = styled.div<TableBodyContentProps>`
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||
line-clamp: ${(props): number => props.linesPerRow};
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
font-size: 0.875rem;
|
||||
|
||||
line-height: 2rem;
|
||||
`;
|
||||
8
frontend/src/components/Logs/styles.ts
Normal file
8
frontend/src/components/Logs/styles.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ButtonContainer = styled(Button)`
|
||||
&&& {
|
||||
padding-left: 0;
|
||||
}
|
||||
`;
|
||||
47
frontend/src/components/MessageTip/MessageTip.test.tsx
Normal file
47
frontend/src/components/MessageTip/MessageTip.test.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import MessageTip from './index';
|
||||
|
||||
describe('MessageTip', () => {
|
||||
it('should not render when show prop is false', () => {
|
||||
render(
|
||||
<MessageTip
|
||||
show={false}
|
||||
message="Test Message"
|
||||
action={<button type="button">Close</button>}
|
||||
/>,
|
||||
);
|
||||
|
||||
const messageTip = screen.queryByRole('alert');
|
||||
|
||||
expect(messageTip).toBeNull();
|
||||
});
|
||||
|
||||
it('should render with the provided message and action', () => {
|
||||
const message = 'Test Message';
|
||||
const action = <button type="button">Close</button>;
|
||||
|
||||
render(<MessageTip show message={message} action={action} />);
|
||||
|
||||
const messageTip = screen.getByRole('alert');
|
||||
const messageText = screen.getByText(message);
|
||||
const actionButton = screen.getByRole('button', { name: 'Close' });
|
||||
|
||||
expect(messageTip).toBeInTheDocument();
|
||||
expect(messageText).toBeInTheDocument();
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// taken from antd docs
|
||||
// https://github.com/ant-design/ant-design/blob/master/components/alert/__tests__/index.test.tsx
|
||||
it('custom action', () => {
|
||||
const { container } = render(
|
||||
<MessageTip
|
||||
show
|
||||
message="Success Tips"
|
||||
action={<button type="button">Close</button>}
|
||||
/>,
|
||||
);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MessageTip custom action 1`] = `
|
||||
.c0 {
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
<div
|
||||
class="ant-alert ant-alert-info ant-alert-with-description c0 css-dev-only-do-not-override-1i536d8"
|
||||
data-show="true"
|
||||
role="alert"
|
||||
>
|
||||
<span
|
||||
aria-label="info-circle"
|
||||
class="anticon anticon-info-circle ant-alert-icon"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="info-circle"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="ant-alert-content"
|
||||
>
|
||||
<div
|
||||
class="ant-alert-description"
|
||||
>
|
||||
Success Tips
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-alert-action"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user