Compare commits
154 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09d579311e | ||
|
|
8072fede85 | ||
|
|
112783d618 | ||
|
|
4644b1c200 | ||
|
|
bb09c84679 | ||
|
|
fc5f0fbf9e | ||
|
|
d6f0559adc | ||
|
|
0d7f7df76c | ||
|
|
7104d8e0f5 | ||
|
|
a20693fa9f | ||
|
|
0b991331d7 | ||
|
|
aad44a1037 | ||
|
|
3e29161fea | ||
|
|
b616dca52d | ||
|
|
be519666a3 | ||
|
|
a48edac13b | ||
|
|
0a77c7ab85 | ||
|
|
9fb32acf6d | ||
|
|
b2d6d75eef | ||
|
|
07d126c669 | ||
|
|
50d584cc89 | ||
|
|
1b6b3c2fdf | ||
|
|
1f0fdfd403 | ||
|
|
ae3b604cdc | ||
|
|
381f497b95 | ||
|
|
8045c4e5ae | ||
|
|
7451e885c3 | ||
|
|
01df53074c | ||
|
|
b6a79ab22c | ||
|
|
dae817640b | ||
|
|
16839eb7d3 | ||
|
|
780a863943 | ||
|
|
5e0b6366cc | ||
|
|
8eb2b9e3d0 | ||
|
|
97ed163002 | ||
|
|
e18bb7d5bc | ||
|
|
1e4cf2513c | ||
|
|
988ede7776 | ||
|
|
d1acad8ee4 | ||
|
|
f5b1d4146f | ||
|
|
feaac39e2a | ||
|
|
fc4cdea539 | ||
|
|
399d49b3c0 | ||
|
|
ec8a74d385 | ||
|
|
7c87310fa6 | ||
|
|
349c4020f5 | ||
|
|
92e2f1c467 | ||
|
|
e3a89be86b | ||
|
|
40090aaf12 | ||
|
|
4009ac83fe | ||
|
|
e7f9c3981b | ||
|
|
fe75f6347b | ||
|
|
bc72b5fcea | ||
|
|
a54cf38e21 | ||
|
|
94d99ee0a4 | ||
|
|
c109636889 | ||
|
|
d9950d9223 | ||
|
|
a578f9509a | ||
|
|
b1e4ee1d26 | ||
|
|
31b07cc02c | ||
|
|
d42bf50ddb | ||
|
|
93a11b2031 | ||
|
|
af71474bec | ||
|
|
bc942d218b | ||
|
|
f2e7f09a32 | ||
|
|
7e87df2d69 | ||
|
|
c0226ab584 | ||
|
|
84f2885533 | ||
|
|
e58ecff19b | ||
|
|
f4ecfb510a | ||
|
|
c4536f9069 | ||
|
|
2a55f3d680 | ||
|
|
5d6eea3045 | ||
|
|
12029a6d90 | ||
|
|
4083970289 | ||
|
|
b3c0681a85 | ||
|
|
36aced6d1a | ||
|
|
bad69abcc2 | ||
|
|
d091d90d66 | ||
|
|
29bfdb8909 | ||
|
|
31b5635339 | ||
|
|
73fc262f04 | ||
|
|
dc23368f6e | ||
|
|
75526c6de5 | ||
|
|
5b419cb668 | ||
|
|
d8a8430a5b | ||
|
|
dc7a55e871 | ||
|
|
9333fdcd0b | ||
|
|
58ccbdbec4 | ||
|
|
12819113c1 | ||
|
|
37f61ebe60 | ||
|
|
f2f89eb38b | ||
|
|
a99d7f09a1 | ||
|
|
2ae75e6196 | ||
|
|
f86fc03fd6 | ||
|
|
5a9f626da5 | ||
|
|
758013d7cd | ||
|
|
ddc3cc4911 | ||
|
|
6b2f857a12 | ||
|
|
30b0d42604 | ||
|
|
88aabb2060 | ||
|
|
f939d41acd | ||
|
|
d165f727ac | ||
|
|
e4ef137c72 | ||
|
|
dda01678e8 | ||
|
|
3e65543b5f | ||
|
|
050b866173 | ||
|
|
0906886e9a | ||
|
|
8371670512 | ||
|
|
123f2e7d52 | ||
|
|
0ab09c1c67 | ||
|
|
9f5039dbf3 | ||
|
|
5e349d8294 | ||
|
|
b5654c8bfa | ||
|
|
71e487dc0c | ||
|
|
2d60805b28 | ||
|
|
7603e0ebe0 | ||
|
|
1e8a8d19ea | ||
|
|
092d164d55 | ||
|
|
0400d5378b | ||
|
|
626da7533e | ||
|
|
bff7142a61 | ||
|
|
ed3017d247 | ||
|
|
ec3eba612c | ||
|
|
b958a06ba0 | ||
|
|
64f0ff05f9 | ||
|
|
f94a5f4481 | ||
|
|
27869f03bd | ||
|
|
9c21449239 | ||
|
|
991e39aad3 | ||
|
|
eddb607c9c | ||
|
|
3341cb7396 | ||
|
|
4ca1e34378 | ||
|
|
658a9cc11b | ||
|
|
4ef973ceb6 | ||
|
|
bbfaad15c2 | ||
|
|
45ead71359 | ||
|
|
79aef73767 | ||
|
|
fc49833c9f | ||
|
|
b34eafcab1 | ||
|
|
ed4ba1aa24 | ||
|
|
f427bac993 | ||
|
|
7de3cec477 | ||
|
|
856c04220f | ||
|
|
6a8096b8d7 | ||
|
|
9bad663c4f | ||
|
|
720a735338 | ||
|
|
1ad7ba0afd | ||
|
|
176d01544e | ||
|
|
c55be0e392 | ||
|
|
2c2775c766 | ||
|
|
f90ae99018 | ||
|
|
e12cf3e494 | ||
|
|
f12abfbe01 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -4,6 +4,8 @@
|
||||
* @ankitnayan
|
||||
|
||||
/frontend/ @palashgdev @YounixM
|
||||
/frontend/src/container/MetricsApplication @srikanthccv
|
||||
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
|
||||
/deploy/ @prashant-shahi
|
||||
/sample-apps/ @prashant-shahi
|
||||
**/query-service/ @srikanthccv
|
||||
|
||||
17
.github/pull_request_template.md
vendored
Normal file
17
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
### Summary
|
||||
|
||||
<!-- ✍️ A clear and concise description...-->
|
||||
|
||||
#### Related Issues / PR's
|
||||
|
||||
<!-- ✍️ Add the issues being resolved here and related PR's where applicable -->
|
||||
|
||||
#### Screenshots
|
||||
|
||||
NA
|
||||
|
||||
<!-- ✍️ Add screenshots of before and after changes where applicable-->
|
||||
|
||||
#### Affected Areas and Manually Tested Areas
|
||||
|
||||
<!-- ✍️ Add details of blast radius and dev testing areas where applicable-->
|
||||
8
.github/workflows/push.yaml
vendored
8
.github/workflows/push.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v5.1
|
||||
uses: tj-actions/branch-names@v7.0.7
|
||||
- name: Set docker tag environment
|
||||
run: |
|
||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v5.1
|
||||
uses: tj-actions/branch-names@v7.0.7
|
||||
- name: Set docker tag environment
|
||||
run: |
|
||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v5.1
|
||||
uses: tj-actions/branch-names@v7.0.7
|
||||
- name: Set docker tag environment
|
||||
run: |
|
||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v5.1
|
||||
uses: tj-actions/branch-names@v7.0.7
|
||||
- name: Set docker tag environment
|
||||
run: |
|
||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||
|
||||
1
.github/workflows/staging-deployment.yaml
vendored
1
.github/workflows/staging-deployment.yaml
vendored
@@ -29,6 +29,7 @@ jobs:
|
||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||
docker system prune --force
|
||||
docker pull signoz/signoz-otel-collector:main
|
||||
docker pull signoz/signoz-schema-migrator:main
|
||||
cd ~/signoz
|
||||
git status
|
||||
git add .
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -37,7 +37,7 @@ frontend/src/constants/env.ts
|
||||
**/locust-scripts/__pycache__/
|
||||
**/__debug_bin
|
||||
|
||||
frontend/.env
|
||||
.env
|
||||
pkg/query-service/signoz.db
|
||||
|
||||
pkg/query-service/tests/test-deploy/data/
|
||||
@@ -53,3 +53,12 @@ ee/query-service/tests/test-deploy/data/
|
||||
bin/
|
||||
|
||||
*/query-service/queries.active
|
||||
|
||||
# e2e
|
||||
|
||||
e2e/node_modules/
|
||||
e2e/test-results/
|
||||
e2e/playwright-report/
|
||||
e2e/blob-report/
|
||||
e2e/playwright/.cache/
|
||||
e2e/.auth
|
||||
@@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.32.0
|
||||
image: signoz/query-service:0.35.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.32.0
|
||||
image: signoz/frontend:0.35.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -199,7 +199,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.79.11
|
||||
image: signoz/signoz-otel-collector:0.88.1
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.79.11
|
||||
image: signoz/signoz-schema-migrator:0.88.1
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -250,7 +250,7 @@ services:
|
||||
# - clickhouse-3
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.79.11
|
||||
image: signoz/signoz-otel-collector:0.88.1
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
|
||||
@@ -61,40 +61,6 @@ receivers:
|
||||
job_name: otel-collector
|
||||
|
||||
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
|
||||
@@ -193,5 +159,5 @@ service:
|
||||
exporters: [prometheus]
|
||||
logs:
|
||||
receivers: [otlp, tcplog/docker]
|
||||
processors: [logstransform/internal, batch]
|
||||
processors: [batch]
|
||||
exporters: [clickhouselogsexporter]
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.79.11}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.1}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.79.11
|
||||
image: signoz/signoz-otel-collector:0.88.1
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -118,7 +118,7 @@ services:
|
||||
|
||||
otel-collector-metrics:
|
||||
container_name: signoz-otel-collector-metrics
|
||||
image: signoz/signoz-otel-collector:0.79.11
|
||||
image: signoz/signoz-otel-collector:0.88.1
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
|
||||
@@ -164,7 +164,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.32.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.35.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.32.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.35.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.79.11}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.1}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.11}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.1}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
@@ -269,7 +269,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.11}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.1}
|
||||
container_name: signoz-otel-collector-metrics
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -62,40 +62,6 @@ 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
|
||||
@@ -198,5 +164,5 @@ service:
|
||||
exporters: [prometheus]
|
||||
logs:
|
||||
receivers: [otlp, tcplog/docker]
|
||||
processors: [logstransform/internal, batch]
|
||||
processors: [batch]
|
||||
exporters: [clickhouselogsexporter]
|
||||
14
e2e/package.json
Normal file
14
e2e/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "e2e",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.22.0",
|
||||
"@types/node": "^20.9.2"
|
||||
},
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"dotenv": "8.2.0"
|
||||
}
|
||||
}
|
||||
46
e2e/playwright.config.ts
Normal file
46
e2e/playwright.config.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
|
||||
fullyParallel: true,
|
||||
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
name: "Signoz E2E",
|
||||
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
reporter: process.env.CI ? "github" : "list",
|
||||
|
||||
preserveOutput: "always",
|
||||
|
||||
updateSnapshots: "all",
|
||||
|
||||
quiet: false,
|
||||
|
||||
testMatch: ["**/*.spec.ts"],
|
||||
|
||||
use: {
|
||||
trace: "on-first-retry",
|
||||
|
||||
baseURL:
|
||||
process.env.PLAYWRIGHT_TEST_BASE_URL || "https://stagingapp.signoz.io/",
|
||||
},
|
||||
|
||||
projects: [
|
||||
{ name: "setup", testMatch: /.*\.setup\.ts/ },
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
// Use prepared auth state.
|
||||
storageState: ".auth/user.json",
|
||||
},
|
||||
dependencies: ["setup"],
|
||||
},
|
||||
],
|
||||
});
|
||||
37
e2e/tests/auth.setup.ts
Normal file
37
e2e/tests/auth.setup.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import ROUTES from "../../frontend/src/constants/routes";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const authFile = ".auth/user.json";
|
||||
|
||||
test("E2E Login Test", async ({ page }) => {
|
||||
await Promise.all([page.goto("/"), page.waitForRequest("**/version")]);
|
||||
|
||||
const signup = "Monitor your applications. Find what is causing issues.";
|
||||
|
||||
const el = await page.locator(`text=${signup}`);
|
||||
|
||||
expect(el).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator("id=loginEmail")
|
||||
.type(
|
||||
process.env.PLAYWRIGHT_USERNAME ? process.env.PLAYWRIGHT_USERNAME : ""
|
||||
);
|
||||
|
||||
await page.getByText("Next").click();
|
||||
|
||||
await page
|
||||
.locator('input[id="currentPassword"]')
|
||||
.fill(
|
||||
process.env.PLAYWRIGHT_PASSWORD ? process.env.PLAYWRIGHT_PASSWORD : ""
|
||||
);
|
||||
|
||||
await page.locator('button[data-attr="signup"]').click();
|
||||
|
||||
await expect(page).toHaveURL(ROUTES.APPLICATION);
|
||||
|
||||
await page.context().storageState({ path: authFile });
|
||||
});
|
||||
10
e2e/tests/contants.ts
Normal file
10
e2e/tests/contants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const SERVICE_TABLE_HEADERS = {
|
||||
APPLICATION: "Applicaton",
|
||||
P99LATENCY: "P99 latency (in ms)",
|
||||
ERROR_RATE: "Error Rate (% of total)",
|
||||
OPS_PER_SECOND: "Operations Per Second",
|
||||
};
|
||||
|
||||
export const DATA_TEST_IDS = {
|
||||
NEW_DASHBOARD_BTN: "create-new-dashboard",
|
||||
};
|
||||
40
e2e/tests/navigation.spec.ts
Normal file
40
e2e/tests/navigation.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import ROUTES from "../../frontend/src/constants/routes";
|
||||
import { DATA_TEST_IDS, SERVICE_TABLE_HEADERS } from "./contants";
|
||||
|
||||
test("Basic Navigation Check across different resources", async ({ page }) => {
|
||||
// route to services page and check if the page renders fine with BE contract
|
||||
await Promise.all([
|
||||
page.goto(ROUTES.APPLICATION),
|
||||
page.waitForRequest("**/v1/services"),
|
||||
]);
|
||||
|
||||
const p99Latency = page.locator(
|
||||
`th:has-text("${SERVICE_TABLE_HEADERS.P99LATENCY}")`
|
||||
);
|
||||
|
||||
await expect(p99Latency).toBeVisible();
|
||||
|
||||
// route to the new trace explorer page and check if the page renders fine
|
||||
await page.goto(ROUTES.TRACES_EXPLORER);
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const listViewTable = await page
|
||||
.locator('div[role="presentation"]')
|
||||
.isVisible();
|
||||
|
||||
expect(listViewTable).toBeTruthy();
|
||||
|
||||
// route to the dashboards page and check if the page renders fine
|
||||
await Promise.all([
|
||||
page.goto(ROUTES.ALL_DASHBOARD),
|
||||
page.waitForRequest("**/v1/dashboards"),
|
||||
]);
|
||||
|
||||
const newDashboardBtn = await page
|
||||
.locator(`data-testid=${DATA_TEST_IDS.NEW_DASHBOARD_BTN}`)
|
||||
.isVisible();
|
||||
|
||||
expect(newDashboardBtn).toBeTruthy();
|
||||
});
|
||||
46
e2e/yarn.lock
Normal file
46
e2e/yarn.lock
Normal file
@@ -0,0 +1,46 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@playwright/test@^1.22.0":
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.40.0.tgz#d06c506977dd7863aa16e07f2136351ecc1be6ed"
|
||||
integrity sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==
|
||||
dependencies:
|
||||
playwright "1.40.0"
|
||||
|
||||
"@types/node@^20.9.2":
|
||||
version "20.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.2.tgz#002815c8e87fe0c9369121c78b52e800fadc0ac6"
|
||||
integrity sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
dotenv@8.2.0:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||
|
||||
fsevents@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
playwright-core@1.40.0:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.0.tgz#82f61e5504cb3097803b6f8bbd98190dd34bdf14"
|
||||
integrity sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==
|
||||
|
||||
playwright@1.40.0:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.0.tgz#2a1824b9fe5c4fe52ed53db9ea68003543a99df0"
|
||||
integrity sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==
|
||||
dependencies:
|
||||
playwright-core "1.40.0"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
@@ -1,5 +1,5 @@
|
||||
# use a minimal alpine image
|
||||
FROM alpine:3.17
|
||||
FROM alpine:3.18.5
|
||||
|
||||
# Add Maintainer Info
|
||||
LABEL maintainer="signoz"
|
||||
|
||||
@@ -160,6 +160,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
|
||||
|
||||
router.HandleFunc("/api/v2/licenses",
|
||||
am.ViewAccess(ah.listLicensesV2)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
51
ee/query-service/app/api/dashboard.go
Normal file
51
ee/query-service/app/api/dashboard.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ah.lockUnlockDashboard(w, r, true)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ah.lockUnlockDashboard(w, r, false)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) {
|
||||
// Locking can only be done by the owner of the dashboard
|
||||
// or an admin
|
||||
|
||||
// - Fetch the dashboard
|
||||
// - Check if the user is the owner or an admin
|
||||
// - If yes, lock/unlock the dashboard
|
||||
// - If no, return 403
|
||||
|
||||
// Get the dashboard UUID from the request
|
||||
uuid := mux.Vars(r)["uuid"]
|
||||
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user := common.GetUserFromContext(r.Context())
|
||||
if !auth.IsAdmin(user) && (dashboard.CreateBy != nil && *dashboard.CreateBy != user.Email) {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: err}, "You are not authorized to lock/unlock this dashboard")
|
||||
return
|
||||
}
|
||||
|
||||
// Lock/Unlock the dashboard
|
||||
err = dashboards.LockUnlockDashboard(r.Context(), uuid, lock)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, "Dashboard updated successfully")
|
||||
}
|
||||
@@ -52,7 +52,6 @@ func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
var l model.License
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
|
||||
@@ -64,8 +63,7 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
||||
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
||||
return
|
||||
}
|
||||
|
||||
license, apiError := ah.LM().Activate(ctx, l.Key)
|
||||
license, apiError := ah.LM().Activate(r.Context(), l.Key)
|
||||
if apiError != nil {
|
||||
RespondError(w, apiError, nil)
|
||||
return
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"go.signoz.io/signoz/ee/query-service/constants"
|
||||
"go.signoz.io/signoz/ee/query-service/dao"
|
||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
baseInterface "go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
|
||||
@@ -437,7 +438,10 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
|
||||
telemetry.GetInstance().AddActiveLogsUser()
|
||||
}
|
||||
data["dataSources"] = dataSources
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, true)
|
||||
userEmail, err := auth.GetEmailFromJwt(r.Context())
|
||||
if err == nil {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_V3, data, userEmail, true)
|
||||
}
|
||||
}
|
||||
return data, true
|
||||
}
|
||||
@@ -458,6 +462,8 @@ func getActiveLogs(path string, r *http.Request) {
|
||||
|
||||
func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := auth.AttachJwtToContext(r.Context(), r)
|
||||
r = r.WithContext(ctx)
|
||||
route := mux.CurrentRoute(r)
|
||||
path, _ := route.GetPathTemplate()
|
||||
|
||||
@@ -475,7 +481,10 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
if _, ok := telemetry.IgnoredPaths()[path]; !ok {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data)
|
||||
userEmail, err := auth.GetEmailFromJwt(r.Context())
|
||||
if err == nil {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"sync"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
||||
|
||||
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
|
||||
@@ -203,7 +204,7 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
|
||||
zap.S().Errorf("License validation completed with error", reterr)
|
||||
atomic.AddUint64(&lm.failedAttempts, 1)
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
||||
map[string]interface{}{"err": reterr.Error()})
|
||||
map[string]interface{}{"err": reterr.Error()}, "")
|
||||
} else {
|
||||
zap.S().Info("License validation completed with no errors")
|
||||
}
|
||||
@@ -259,8 +260,11 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
|
||||
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
|
||||
defer func() {
|
||||
if errResponse != nil {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
||||
map[string]interface{}{"err": errResponse.Err.Error()})
|
||||
userEmail, err := auth.GetEmailFromJwt(ctx)
|
||||
if err == nil {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
||||
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@uiw/react-md-editor": "3.23.5",
|
||||
"@xstate/react": "^3.0.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"antd": "5.0.5",
|
||||
"antd": "5.11.0",
|
||||
"antd-table-saveas-excel": "2.2.1",
|
||||
"axios": "^0.21.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
@@ -80,11 +80,12 @@
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-drag-listview": "2.0.0",
|
||||
"react-error-boundary": "4.0.11",
|
||||
"react-force-graph": "^1.43.0",
|
||||
"react-full-screen": "1.1.1",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-helmet-async": "1.3.0",
|
||||
"react-i18next": "^11.16.1",
|
||||
"react-intersection-observer": "9.4.1",
|
||||
"react-markdown": "8.0.7",
|
||||
"react-query": "^3.34.19",
|
||||
"react-redux": "^7.2.2",
|
||||
@@ -102,6 +103,7 @@
|
||||
"ts-node": "^10.2.1",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "^4.0.5",
|
||||
"uplot": "1.6.26",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "5.88.2",
|
||||
@@ -154,6 +156,7 @@
|
||||
"@types/react-resizable": "3.0.3",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"@types/react-syntax-highlighter": "15.5.7",
|
||||
"@types/redux-mock-store": "1.0.4",
|
||||
"@types/styled-components": "^5.1.4",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@types/webpack": "^5.28.0",
|
||||
@@ -192,6 +195,7 @@
|
||||
"react-hooks-testing-library": "0.6.0",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-resizable": "3.0.4",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"sass": "1.66.1",
|
||||
"sass-loader": "13.3.2",
|
||||
"ts-jest": "^27.1.5",
|
||||
|
||||
BIN
frontend/public/Logos/fluent-bit.png
Normal file
BIN
frontend/public/Logos/fluent-bit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
frontend/public/Logos/fluentd.png
Normal file
BIN
frontend/public/Logos/fluentd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
1
frontend/public/Logos/logstash.svg
Normal file
1
frontend/public/Logos/logstash.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" width="2500" height="2500"><style>.st0{fill:#f3bd19}.st1{fill:#231f20}.st2{fill:#3ebeb0}.st3{fill:#37a595}.st4{fill:none}</style><path class="st0" d="M41.1 41.9H15.6V12.5h7.7c9.9 0 17.8 8 17.8 17.8v11.6z"/><path class="st1" d="M41.1 67.5c-14.1 0-25.6-11.4-25.6-25.6h25.6v25.6z"/><path class="st2" d="M41.1 41.9h23.3v25.6H41.1z"/><path class="st3" d="M41.1 41.9h5.4v25.6h-5.4z"/><path class="st4" d="M0 0h80v80H0z"/></svg>
|
||||
|
After Width: | Height: | Size: 494 B |
@@ -34,7 +34,7 @@
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition1": "Send a notification when",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
@@ -109,5 +109,6 @@
|
||||
"traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.",
|
||||
"exceptions_based_alert": "Exceptions-based Alert",
|
||||
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
||||
"field_unit": "Threshold unit"
|
||||
"field_unit": "Threshold unit",
|
||||
"selected_query_placeholder": "Select query"
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
"variable_updated_successfully": "Variable updated successfully",
|
||||
"error_while_updating_variable": "Error while updating variable",
|
||||
"dashboard_has_been_updated": "Dashboard has been updated",
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?"
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
|
||||
"delete_dashboard_success": "{{name}} dashboard deleted successfully"
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info",
|
||||
"user_guide_headline": "Steps to create an Alert",
|
||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts"
|
||||
}
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info",
|
||||
"user_guide_headline": "Steps to create an Alert",
|
||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition1": "Send a notification when",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
@@ -109,5 +109,6 @@
|
||||
"traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.",
|
||||
"exceptions_based_alert": "Exceptions-based Alert",
|
||||
"exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data.",
|
||||
"field_unit": "Threshold unit"
|
||||
"field_unit": "Threshold unit",
|
||||
"selected_query_placeholder": "Select query"
|
||||
}
|
||||
|
||||
@@ -17,8 +17,12 @@
|
||||
"layout_saved_successfully": "Layout saved successfully",
|
||||
"add_panel": "Add Panel",
|
||||
"save_layout": "Save Layout",
|
||||
"full_view": "Full Screen View",
|
||||
"variable_updated_successfully": "Variable updated successfully",
|
||||
"error_while_updating_variable": "Error while updating variable",
|
||||
"dashboard_has_been_updated": "Dashboard has been updated",
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?"
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
|
||||
"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.",
|
||||
"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard.",
|
||||
"delete_dashboard_success": "{{name}} dashboard deleted successfully"
|
||||
}
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
"see_error_in_trace_graph": "See the error in trace graph",
|
||||
"stack_trace": "Stacktrace",
|
||||
"older": "Older",
|
||||
"newer": "Newer"
|
||||
"newer": "Newer",
|
||||
"something_went_wrong": "Oops !!! Something went wrong",
|
||||
"contact_if_issue_exists": "Don't worry, our team is here to help. Please contact support if the issue persists."
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"create": "Create",
|
||||
"reorder": "Reorder",
|
||||
"cancel": "Cancel",
|
||||
"learn_more": "Learn more about pipelines",
|
||||
"reorder_pipeline": "Do you want to reorder pipeline?",
|
||||
"reorder_pipeline_description": "Logs are processed sequentially in processors and pipelines. Reordering it may change how data is processed by them.",
|
||||
"delete_pipeline": "Do you want to delete pipeline",
|
||||
@@ -31,15 +32,15 @@
|
||||
"processor_name_placeholder": "Name",
|
||||
"processor_regex_placeholder": "Regex",
|
||||
"processor_parsefrom_placeholder": "Parse From",
|
||||
"processor_parseto_placeholder": "Parse From",
|
||||
"processor_parseto_placeholder": "Parse To",
|
||||
"processor_onerror_placeholder": "on Error",
|
||||
"processor_pattern_placeholder": "Pattern",
|
||||
"processor_field_placeholder": "Field",
|
||||
"processor_value_placeholder": "Value",
|
||||
"processor_description_placeholder": "example rule: %{word:first}",
|
||||
"processor_trace_id_placeholder": "Trace Id Parce From",
|
||||
"processor_span_id_placeholder": "Span id Parse From",
|
||||
"processor_trace_flags_placeholder": "Trace flags parse from",
|
||||
"processor_trace_id_placeholder": "Parse Trace ID from",
|
||||
"processor_span_id_placeholder": "Parse Span ID from",
|
||||
"processor_trace_flags_placeholder": "Parse Trace flags from",
|
||||
"processor_from_placeholder": "From",
|
||||
"processor_to_placeholder": "To"
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info",
|
||||
"user_guide_headline": "Steps to create an Alert",
|
||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts"
|
||||
}
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_4hours": "4 hours",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info",
|
||||
"user_guide_headline": "Steps to create an Alert",
|
||||
"user_guide_qb_step1": "Step 1 - Define the metric",
|
||||
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
|
||||
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
|
||||
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
|
||||
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
|
||||
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_qb_step2b": "Enter the Alert threshold",
|
||||
"user_guide_qb_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_guide_pql_step1": "Step 1 - Define the metric",
|
||||
"user_guide_pql_step1a": "Write a PromQL query for the metric",
|
||||
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
|
||||
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
|
||||
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
|
||||
"user_guide_pql_step2b": "Enter the Alert threshold",
|
||||
"user_guide_pql_step3": "Step 3 -Alert Configuration",
|
||||
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||
|
||||
3
frontend/public/locales/en/valueGraph.json
Normal file
3
frontend/public/locales/en/valueGraph.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"this_value_satisfies_multiple_thresholds": "This value satisfies multiple thresholds."
|
||||
}
|
||||
@@ -39,10 +39,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const { data: licensesData } = useLicense();
|
||||
const {
|
||||
data: licensesData,
|
||||
isFetching: isFetchingLicensesData,
|
||||
} = useLicense();
|
||||
|
||||
const {
|
||||
user,
|
||||
isUserFetching,
|
||||
isUserFetchingError,
|
||||
isLoggedIn: isLoggedInState,
|
||||
@@ -116,7 +118,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
if (
|
||||
localStorageUserAuthToken &&
|
||||
localStorageUserAuthToken.refreshJwt &&
|
||||
user?.userId === ''
|
||||
isUserFetching
|
||||
) {
|
||||
handleUserLoginIfTokenPresent(key);
|
||||
} else {
|
||||
@@ -131,28 +133,34 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
|
||||
if (path && path !== ROUTES.WORKSPACE_LOCKED) {
|
||||
history.push(ROUTES.WORKSPACE_LOCKED);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_USER_IS_FETCH,
|
||||
payload: {
|
||||
isUserFetching: false,
|
||||
},
|
||||
});
|
||||
dispatch({
|
||||
type: UPDATE_USER_IS_FETCH,
|
||||
payload: {
|
||||
isUserFetching: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingLicensesData) {
|
||||
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
|
||||
|
||||
if (shouldBlockWorkspace) {
|
||||
navigateToWorkSpaceBlocked(currentRoute);
|
||||
}
|
||||
}
|
||||
}, [isFetchingLicensesData]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
(async (): Promise<void> => {
|
||||
try {
|
||||
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
|
||||
|
||||
if (currentRoute) {
|
||||
const { isPrivate, key } = currentRoute;
|
||||
|
||||
if (shouldBlockWorkspace) {
|
||||
navigateToWorkSpaceBlocked(currentRoute);
|
||||
} else if (isPrivate) {
|
||||
if (isPrivate && key !== ROUTES.WORKSPACE_LOCKED) {
|
||||
handlePrivateRoutes(key);
|
||||
} else {
|
||||
// no need to fetch the user and make user fetching false
|
||||
|
||||
@@ -7,12 +7,14 @@ import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import AppLayout from 'container/AppLayout';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
|
||||
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||
import { NotificationProvider } from 'hooks/useNotifications';
|
||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||
import history from 'lib/history';
|
||||
import { identity, pickBy } from 'lodash-es';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
@@ -24,7 +26,6 @@ import AppActions from 'types/actions';
|
||||
import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app';
|
||||
import AppReducer, { User } from 'types/reducer/app';
|
||||
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
|
||||
import { trackPageView } from 'utils/segmentAnalytics';
|
||||
|
||||
import PrivateRoute from './Private';
|
||||
import defaultRoutes, { AppRoutes, SUPPORT_ROUTE } from './routes';
|
||||
@@ -40,6 +41,8 @@ function App(): JSX.Element {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const { trackPageView } = useAnalytics();
|
||||
|
||||
const { hostname, pathname } = window.location;
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
@@ -90,13 +93,19 @@ function App(): JSX.Element {
|
||||
const orgName =
|
||||
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
|
||||
|
||||
const { name, email } = user;
|
||||
|
||||
const identifyPayload = {
|
||||
email: user?.email,
|
||||
name: user?.name,
|
||||
email,
|
||||
name,
|
||||
company_name: orgName,
|
||||
role,
|
||||
source: 'signoz-ui',
|
||||
};
|
||||
const domain = extractDomain(user?.email);
|
||||
|
||||
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
|
||||
|
||||
const domain = extractDomain(email);
|
||||
|
||||
const hostNameParts = hostname.split('.');
|
||||
|
||||
@@ -106,13 +115,14 @@ function App(): JSX.Element {
|
||||
data_region: hostNameParts[1],
|
||||
tenant_url: hostname,
|
||||
company_domain: domain,
|
||||
source: 'signoz-ui',
|
||||
};
|
||||
|
||||
window.analytics.identify(user?.email, identifyPayload);
|
||||
window.analytics.identify(email, sanitizedIdentifyPayload);
|
||||
|
||||
window.analytics.group(domain, groupTraits);
|
||||
|
||||
window.clarity('identify', user.email, user.name);
|
||||
window.clarity('identify', email, name);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -148,6 +158,7 @@ function App(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
trackPageView(pathname);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -49,7 +49,8 @@ export const Onboarding = Loadable(
|
||||
);
|
||||
|
||||
export const DashboardPage = Loadable(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/Dashboard'),
|
||||
() =>
|
||||
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),
|
||||
);
|
||||
|
||||
export const NewDashboardPage = Loadable(
|
||||
|
||||
@@ -282,10 +282,10 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: false,
|
||||
},
|
||||
{
|
||||
path: ROUTES.PIPELINES,
|
||||
path: ROUTES.LOGS_PIPELINES,
|
||||
exact: true,
|
||||
component: PipelinePage,
|
||||
key: 'PIPELINES',
|
||||
key: 'LOGS_PIPELINES',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
@@ -299,7 +299,7 @@ const routes: AppRoutes[] = [
|
||||
path: ROUTES.WORKSPACE_LOCKED,
|
||||
exact: true,
|
||||
component: WorkspaceBlocked,
|
||||
isPrivate: false,
|
||||
isPrivate: true,
|
||||
key: 'WORKSPACE_LOCKED',
|
||||
},
|
||||
];
|
||||
|
||||
11
frontend/src/api/dashboard/lockDashboard.ts
Normal file
11
frontend/src/api/dashboard/lockDashboard.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
interface LockDashboardProps {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
const lockDashboard = (props: LockDashboardProps): Promise<AxiosResponse> =>
|
||||
axios.put(`/dashboards/${props.uuid}/lock`);
|
||||
|
||||
export default lockDashboard;
|
||||
11
frontend/src/api/dashboard/unlockDashboard.ts
Normal file
11
frontend/src/api/dashboard/unlockDashboard.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import axios from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
interface UnlockDashboardProps {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
const unlockDashboard = (props: UnlockDashboardProps): Promise<AxiosResponse> =>
|
||||
axios.put(`/dashboards/${props.uuid}/unlock`);
|
||||
|
||||
export default unlockDashboard;
|
||||
@@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
||||
|
||||
const update = async (
|
||||
const updateDashboard = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
@@ -23,4 +23,4 @@ const update = async (
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
export default updateDashboard;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
Props,
|
||||
VariableResponseProps,
|
||||
} from 'types/api/dashboard/variables/query';
|
||||
|
||||
const dashboardVariablesQuery = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`/variables/query`, props);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
const formattedError = ErrorResponseHandler(error as AxiosError);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||
throw { message: 'Error fetching data', details: formattedError };
|
||||
}
|
||||
};
|
||||
|
||||
export default dashboardVariablesQuery;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/variables/query';
|
||||
|
||||
const query = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`/variables/query`, props);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default query;
|
||||
@@ -3,10 +3,10 @@
|
||||
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="ant-table-wrapper css-dev-only-do-not-override-1i536d8"
|
||||
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<div
|
||||
class="ant-spin-nested-loading css-dev-only-do-not-override-1i536d8"
|
||||
class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<div
|
||||
class="ant-spin-container"
|
||||
@@ -28,7 +28,7 @@ exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
||||
class="ant-table-thead"
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
<td
|
||||
class="ant-table-cell"
|
||||
/>
|
||||
</tr>
|
||||
@@ -43,7 +43,7 @@ exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
||||
class="ant-table-cell"
|
||||
>
|
||||
<div
|
||||
class="css-dev-only-do-not-override-1i536d8 ant-empty ant-empty-normal"
|
||||
class="css-dev-only-do-not-override-2i2tap ant-empty ant-empty-normal"
|
||||
>
|
||||
<div
|
||||
class="ant-empty-image"
|
||||
|
||||
11
frontend/src/components/DropDown/DropDown.styles.scss
Normal file
11
frontend/src/components/DropDown/DropDown.styles.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.dropdown-button {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dropdown-button--dark {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
30
frontend/src/components/DropDown/DropDown.tsx
Normal file
30
frontend/src/components/DropDown/DropDown.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import './DropDown.styles.scss';
|
||||
|
||||
import { EllipsisOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
function DropDown({ element }: { element: JSX.Element[] }): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const items: MenuProps['items'] = element.map(
|
||||
(e: JSX.Element, index: number) => ({
|
||||
label: e,
|
||||
key: index,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items }}>
|
||||
<Button
|
||||
type="link"
|
||||
className={!isDarkMode ? 'dropdown-button--dark' : 'dropdown-button'}
|
||||
onClick={(e): void => e.preventDefault()}
|
||||
>
|
||||
<EllipsisOutlined className="dropdown-icon" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropDown;
|
||||
@@ -220,7 +220,11 @@ function ExplorerCard({
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<Button type={saveButtonType} icon={saveButtonIcon}>
|
||||
<Button
|
||||
type={saveButtonType}
|
||||
icon={saveButtonIcon}
|
||||
data-testid="traces-save-view-action"
|
||||
>
|
||||
{isQueryUpdated
|
||||
? SaveButtonText.SAVE_AS_NEW_VIEW
|
||||
: SaveButtonText.SAVE_VIEW}
|
||||
|
||||
@@ -51,11 +51,10 @@ function SaveViewWithName({
|
||||
return (
|
||||
<Card>
|
||||
<Typography>{t('name_of_the_view')}</Typography>
|
||||
<Form form={form} onFinish={onSaveHandler}>
|
||||
<Form form={form} onFinish={onSaveHandler} requiredMark>
|
||||
<Form.Item
|
||||
name={['viewName']}
|
||||
required
|
||||
requiredMark
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -65,7 +64,12 @@ function SaveViewWithName({
|
||||
>
|
||||
<Input placeholder="Enter Name" />
|
||||
</Form.Item>
|
||||
<SaveButton htmlType="submit" type="primary" loading={isLoading}>
|
||||
<SaveButton
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
loading={isLoading}
|
||||
data-testid="save-view-name-action-button"
|
||||
>
|
||||
Save
|
||||
</SaveButton>
|
||||
</Form>
|
||||
|
||||
@@ -153,7 +153,7 @@ export const deleteViewHandler = ({
|
||||
if (viewId === viewKey) {
|
||||
redirectWithQueryBuilderData(
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
initialQueriesMap[sourcePage],
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
sourcePage,
|
||||
),
|
||||
|
||||
@@ -35,7 +35,7 @@ export type GraphOnClickHandler = (
|
||||
) => void;
|
||||
|
||||
export type ToggleGraphProps = {
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void;
|
||||
toggleGraph(graphIndex: number, isVisible: boolean, reference?: string): void;
|
||||
};
|
||||
|
||||
export type CustomChartOptions = ChartOptions & {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const getYAxisFormattedValue = (
|
||||
return `${parseFloat(value)}`;
|
||||
};
|
||||
|
||||
export const getToolTipValue = (value: string, format: string): string => {
|
||||
export const getToolTipValue = (value: string, format?: string): string => {
|
||||
try {
|
||||
return formattedValueToString(
|
||||
getValueFormat(format)(parseFloat(value), undefined, undefined, undefined),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.code-snippet-container {
|
||||
position: relative;
|
||||
background-color: rgb(43, 43, 43);
|
||||
// background-color: rgb(43, 43, 43);
|
||||
background-color: #111a2c;
|
||||
border-color: #111a2c;
|
||||
}
|
||||
|
||||
.code-copy-btn {
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`MessageTip custom action 1`] = `
|
||||
}
|
||||
|
||||
<div
|
||||
class="ant-alert ant-alert-info ant-alert-with-description c0 css-dev-only-do-not-override-1i536d8"
|
||||
class="ant-alert ant-alert-info ant-alert-with-description c0 css-dev-only-do-not-override-2i2tap"
|
||||
data-show="true"
|
||||
role="alert"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
.DynamicColumnTable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.dynamicColumnTable-button {
|
||||
align-self: flex-end;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dynamicColumnsTable-items {
|
||||
display: flex;
|
||||
width: 10.625rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dynamicColumnsTable-items {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
112
frontend/src/components/ResizeTable/DynamicColumnTable.tsx
Normal file
112
frontend/src/components/ResizeTable/DynamicColumnTable.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import './DynamicColumnTable.syles.scss';
|
||||
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import ResizeTable from './ResizeTable';
|
||||
import { DynamicColumnTableProps } from './types';
|
||||
import {
|
||||
getNewColumnData,
|
||||
getVisibleColumns,
|
||||
setVisibleColumns,
|
||||
} from './utils';
|
||||
|
||||
function DynamicColumnTable({
|
||||
tablesource,
|
||||
columns,
|
||||
dynamicColumns,
|
||||
onDragColumn,
|
||||
...restProps
|
||||
}: DynamicColumnTableProps): JSX.Element {
|
||||
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
||||
columns,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const visibleColumns = getVisibleColumns({
|
||||
tablesource,
|
||||
columnsData: columns,
|
||||
dynamicColumns,
|
||||
});
|
||||
setColumnsData((prevColumns) =>
|
||||
prevColumns
|
||||
? [
|
||||
...prevColumns.slice(0, prevColumns.length - 1),
|
||||
...visibleColumns,
|
||||
prevColumns[prevColumns.length - 1],
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onToggleHandler = (index: number) => (
|
||||
checked: boolean,
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
setVisibleColumns({
|
||||
tablesource,
|
||||
dynamicColumns,
|
||||
index,
|
||||
checked,
|
||||
});
|
||||
setColumnsData((prevColumns) =>
|
||||
getNewColumnData({
|
||||
checked,
|
||||
index,
|
||||
prevColumns,
|
||||
dynamicColumns,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const items: MenuProps['items'] =
|
||||
dynamicColumns?.map((column, index) => ({
|
||||
label: (
|
||||
<div className="dynamicColumnsTable-items">
|
||||
<div>{column.title?.toString()}</div>
|
||||
<Switch
|
||||
checked={columnsData?.findIndex((c) => c.key === column.key) !== -1}
|
||||
onChange={onToggleHandler(index)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
key: index,
|
||||
type: 'checkbox',
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<div className="DynamicColumnTable">
|
||||
{dynamicColumns && (
|
||||
<Dropdown
|
||||
getPopupContainer={popupContainer}
|
||||
menu={{ items }}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button
|
||||
className="dynamicColumnTable-button"
|
||||
size="middle"
|
||||
icon={<SettingOutlined />}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
<ResizeTable
|
||||
columns={columnsData}
|
||||
onDragColumn={onDragColumn}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DynamicColumnTable.defaultProps = {
|
||||
onDragColumn: undefined,
|
||||
};
|
||||
|
||||
export default memo(DynamicColumnTable);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Typography } from 'antd';
|
||||
|
||||
import Time from './Time';
|
||||
|
||||
function DateComponent(
|
||||
CreatedOrUpdateTime: string | number | Date,
|
||||
): JSX.Element {
|
||||
if (CreatedOrUpdateTime === null) {
|
||||
return <Typography> - </Typography>;
|
||||
}
|
||||
|
||||
return <Time CreatedOrUpdateTime={CreatedOrUpdateTime} />;
|
||||
}
|
||||
|
||||
export default DateComponent;
|
||||
@@ -2,16 +2,15 @@ import { Typography } from 'antd';
|
||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
||||
import getFormattedDate from 'lib/getFormatedDate';
|
||||
|
||||
import { Data } from '..';
|
||||
|
||||
function DateComponent(lastUpdatedTime: Data['lastUpdatedTime']): JSX.Element {
|
||||
const time = new Date(lastUpdatedTime);
|
||||
|
||||
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
|
||||
const time = new Date(CreatedOrUpdateTime);
|
||||
const date = getFormattedDate(time);
|
||||
|
||||
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
|
||||
|
||||
return <Typography>{timeString}</Typography>;
|
||||
}
|
||||
|
||||
export default DateComponent;
|
||||
type DateProps = {
|
||||
CreatedOrUpdateTime: string | number | Date;
|
||||
};
|
||||
|
||||
export default Time;
|
||||
11
frontend/src/components/ResizeTable/contants.ts
Normal file
11
frontend/src/components/ResizeTable/contants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const TableDataSource = {
|
||||
Alert: 'alert',
|
||||
Dashboard: 'dashboard',
|
||||
} as const;
|
||||
|
||||
export const DynamicColumnsKey = {
|
||||
CreatedAt: 'createdAt',
|
||||
CreatedBy: 'createdBy',
|
||||
UpdatedAt: 'updatedAt',
|
||||
UpdatedBy: 'updatedBy',
|
||||
};
|
||||
@@ -1,6 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { TableProps } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
|
||||
|
||||
import { TableDataSource } from './contants';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface ResizeTableProps extends TableProps<any> {
|
||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||
}
|
||||
export interface DynamicColumnTableProps extends TableProps<any> {
|
||||
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
||||
dynamicColumns: TableProps<any>['columns'];
|
||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||
}
|
||||
|
||||
export type GetVisibleColumnsFunction = (
|
||||
props: GetVisibleColumnProps,
|
||||
) => (ColumnGroupType<any> | ColumnType<any>)[];
|
||||
|
||||
export type GetVisibleColumnProps = {
|
||||
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
||||
dynamicColumns?: ColumnsType<any>;
|
||||
columnsData?: ColumnsType;
|
||||
};
|
||||
|
||||
export type SetVisibleColumnsProps = {
|
||||
checked: boolean;
|
||||
index: number;
|
||||
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
||||
dynamicColumns?: ColumnsType<any>;
|
||||
};
|
||||
|
||||
type GetNewColumnDataProps = {
|
||||
prevColumns?: ColumnsType;
|
||||
checked: boolean;
|
||||
dynamicColumns?: ColumnsType<any>;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type GetNewColumnDataFunction = (
|
||||
props: GetNewColumnDataProps,
|
||||
) => ColumnsType | undefined;
|
||||
|
||||
77
frontend/src/components/ResizeTable/utils.ts
Normal file
77
frontend/src/components/ResizeTable/utils.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { DynamicColumnsKey } from './contants';
|
||||
import {
|
||||
GetNewColumnDataFunction,
|
||||
GetVisibleColumnsFunction,
|
||||
SetVisibleColumnsProps,
|
||||
} from './types';
|
||||
|
||||
export const getVisibleColumns: GetVisibleColumnsFunction = ({
|
||||
tablesource,
|
||||
dynamicColumns,
|
||||
columnsData,
|
||||
}) => {
|
||||
let columnVisibilityData: { [key: string]: boolean };
|
||||
try {
|
||||
const storedData = localStorage.getItem(tablesource);
|
||||
if (typeof storedData === 'string' && dynamicColumns) {
|
||||
columnVisibilityData = JSON.parse(storedData);
|
||||
return dynamicColumns.filter((column) => {
|
||||
if (column.key && !columnsData?.find((c) => c.key === column.key)) {
|
||||
return columnVisibilityData[column.key];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const initialColumnVisibility: Record<string, boolean> = {};
|
||||
Object.values(DynamicColumnsKey).forEach((key) => {
|
||||
initialColumnVisibility[key] = false;
|
||||
});
|
||||
|
||||
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const setVisibleColumns = ({
|
||||
checked,
|
||||
index,
|
||||
tablesource,
|
||||
dynamicColumns,
|
||||
}: SetVisibleColumnsProps): void => {
|
||||
try {
|
||||
const storedData = localStorage.getItem(tablesource);
|
||||
if (typeof storedData === 'string' && dynamicColumns) {
|
||||
const columnVisibilityData = JSON.parse(storedData);
|
||||
const { key } = dynamicColumns[index];
|
||||
if (key) {
|
||||
columnVisibilityData[key] = checked;
|
||||
}
|
||||
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getNewColumnData: GetNewColumnDataFunction = ({
|
||||
prevColumns,
|
||||
checked,
|
||||
dynamicColumns,
|
||||
index,
|
||||
}) => {
|
||||
if (checked && dynamicColumns) {
|
||||
return prevColumns
|
||||
? [
|
||||
...prevColumns.slice(0, prevColumns.length - 1),
|
||||
dynamicColumns[index],
|
||||
prevColumns[prevColumns.length - 1],
|
||||
]
|
||||
: undefined;
|
||||
}
|
||||
return prevColumns && dynamicColumns
|
||||
? prevColumns.filter((column) => dynamicColumns[index].title !== column.title)
|
||||
: undefined;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
.label-column {
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.label-column--tag {
|
||||
white-space: normal;
|
||||
margin: 0.2rem 0.2rem;
|
||||
}
|
||||
}
|
||||
54
frontend/src/components/TableRenderer/LabelColumn.tsx
Normal file
54
frontend/src/components/TableRenderer/LabelColumn.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import './LabelColumn.styles.scss';
|
||||
|
||||
import { Popover, Tag } from 'antd';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { LabelColumnProps } from './TableRenderer.types';
|
||||
import TagWithToolTip from './TagWithToolTip';
|
||||
|
||||
function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
|
||||
const newLabels = labels.length > 3 ? labels.slice(0, 3) : labels;
|
||||
const remainingLabels = labels.length > 3 ? labels.slice(3) : [];
|
||||
|
||||
return (
|
||||
<div className="label-column">
|
||||
{newLabels.map(
|
||||
(label: string): JSX.Element => (
|
||||
<TagWithToolTip key={label} label={label} color={color} value={value} />
|
||||
),
|
||||
)}
|
||||
{remainingLabels.length > 0 && (
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottomRight"
|
||||
showArrow={false}
|
||||
content={
|
||||
<div>
|
||||
{labels.map(
|
||||
(label: string): JSX.Element => (
|
||||
<TagWithToolTip
|
||||
key={label}
|
||||
label={label}
|
||||
color={color}
|
||||
value={value}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
trigger="hover"
|
||||
>
|
||||
<Tag className="label-column--tag" color={color}>
|
||||
+{remainingLabels.length}
|
||||
</Tag>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LabelColumn.defaultProps = {
|
||||
value: {},
|
||||
};
|
||||
|
||||
export default LabelColumn;
|
||||
@@ -0,0 +1,5 @@
|
||||
export type LabelColumnProps = {
|
||||
labels: string[];
|
||||
color?: string;
|
||||
value?: { [key: string]: string };
|
||||
};
|
||||
36
frontend/src/components/TableRenderer/TagWithToolTip.tsx
Normal file
36
frontend/src/components/TableRenderer/TagWithToolTip.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
|
||||
import { getLabelRenderingValue } from './utils';
|
||||
|
||||
function TagWithToolTip({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
}: TagWithToolTipProps): JSX.Element {
|
||||
const tooltipTitle =
|
||||
value && value[label] ? `${label}: ${value[label]}` : label;
|
||||
return (
|
||||
<div key={label}>
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<Tag className="label-column--tag" color={color}>
|
||||
{getLabelRenderingValue(label, value && value[label])}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type TagWithToolTipProps = {
|
||||
label: string;
|
||||
color?: string;
|
||||
value?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
TagWithToolTip.defaultProps = {
|
||||
value: undefined,
|
||||
color: undefined,
|
||||
};
|
||||
|
||||
export default TagWithToolTip;
|
||||
@@ -16,6 +16,28 @@ export const generatorResizeTableColumns = <T>({
|
||||
};
|
||||
});
|
||||
|
||||
export const getLabelRenderingValue = (
|
||||
label: string,
|
||||
value?: string,
|
||||
): string => {
|
||||
const maxLength = 20;
|
||||
|
||||
if (label.length > maxLength) {
|
||||
return `${label.slice(0, maxLength)}...`;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
const remainingSpace = maxLength - label.length;
|
||||
let newValue = value;
|
||||
if (value.length > remainingSpace) {
|
||||
newValue = `${value.slice(0, remainingSpace)}...`;
|
||||
}
|
||||
return `${label}: ${newValue}`;
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
interface GeneratorResizeTableColumnsProp<T> {
|
||||
baseColumnOptions: ColumnsType<T>;
|
||||
dynamicColumnOption: { key: string; columnOption: ColumnType<T> }[];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import TimeItems, {
|
||||
timePreferance,
|
||||
@@ -33,7 +34,9 @@ function TimePreference({
|
||||
return (
|
||||
<TextContainer noButtonMargin>
|
||||
<Dropdown menu={menu}>
|
||||
<Button>{selectedTime.name}</Button>
|
||||
<Button>
|
||||
{selectedTime.name} <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</TextContainer>
|
||||
);
|
||||
|
||||
152
frontend/src/components/Uplot/Uplot.tsx
Normal file
152
frontend/src/components/Uplot/Uplot.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './uplot.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import UPlot from 'uplot';
|
||||
|
||||
import { dataMatch, optionsUpdateState } from './utils';
|
||||
|
||||
export interface UplotProps {
|
||||
options: uPlot.Options;
|
||||
data: uPlot.AlignedData;
|
||||
onDelete?: (chart: uPlot) => void;
|
||||
onCreate?: (chart: uPlot) => void;
|
||||
resetScales?: boolean;
|
||||
}
|
||||
|
||||
const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
(
|
||||
{ options, data, onDelete, onCreate, resetScales = true },
|
||||
ref,
|
||||
): JSX.Element | null => {
|
||||
const chartRef = useRef<uPlot | null>(null);
|
||||
const propOptionsRef = useRef(options);
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
const propDataRef = useRef(data);
|
||||
const onCreateRef = useRef(onCreate);
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
chartRef.current?.setSeries(graphIndex, { show: isVisible });
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onCreateRef.current = onCreate;
|
||||
onDeleteRef.current = onDelete;
|
||||
});
|
||||
|
||||
const destroy = useCallback((chart: uPlot | null) => {
|
||||
if (chart) {
|
||||
onDeleteRef.current?.(chart);
|
||||
chart.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
|
||||
// remove chart tooltip on cleanup
|
||||
const overlay = document.getElementById('overlay');
|
||||
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const create = useCallback(() => {
|
||||
if (targetRef.current === null) return;
|
||||
|
||||
// If data is empty, hide cursor
|
||||
if (data && data[0] && data[0]?.length === 0) {
|
||||
propOptionsRef.current = {
|
||||
...propOptionsRef.current,
|
||||
cursor: { show: false },
|
||||
};
|
||||
}
|
||||
|
||||
const newChart = new UPlot(
|
||||
propOptionsRef.current,
|
||||
propDataRef.current,
|
||||
targetRef.current,
|
||||
);
|
||||
|
||||
chartRef.current = newChart;
|
||||
onCreateRef.current?.(newChart);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
create();
|
||||
return (): void => {
|
||||
destroy(chartRef.current);
|
||||
};
|
||||
}, [create, destroy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (propOptionsRef.current !== options) {
|
||||
const optionsState = optionsUpdateState(propOptionsRef.current, options);
|
||||
propOptionsRef.current = options;
|
||||
if (!chartRef.current || optionsState === 'create') {
|
||||
destroy(chartRef.current);
|
||||
create();
|
||||
} else if (optionsState === 'update') {
|
||||
chartRef.current.setSize({
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [options, create, destroy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (propDataRef.current !== data) {
|
||||
if (!chartRef.current) {
|
||||
propDataRef.current = data;
|
||||
create();
|
||||
} else if (!dataMatch(propDataRef.current, data)) {
|
||||
if (resetScales) {
|
||||
chartRef.current.setData(data, true);
|
||||
} else {
|
||||
chartRef.current.setData(data, false);
|
||||
chartRef.current.redraw();
|
||||
}
|
||||
}
|
||||
propDataRef.current = data;
|
||||
}
|
||||
}, [data, resetScales, create]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<div className="uplot-graph-container" ref={targetRef}>
|
||||
{data && data[0] && data[0]?.length === 0 ? (
|
||||
<div className="not-found">
|
||||
<Typography>No Data</Typography>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Uplot.displayName = 'Uplot';
|
||||
|
||||
Uplot.defaultProps = {
|
||||
onDelete: undefined,
|
||||
onCreate: undefined,
|
||||
resetScales: true,
|
||||
};
|
||||
|
||||
export default memo(Uplot);
|
||||
3
frontend/src/components/Uplot/index.ts
Normal file
3
frontend/src/components/Uplot/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Uplot from './Uplot';
|
||||
|
||||
export default Uplot;
|
||||
15
frontend/src/components/Uplot/uplot.scss
Normal file
15
frontend/src/components/Uplot/uplot.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
.not-found {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 0;
|
||||
height: 85%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.uplot-graph-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
48
frontend/src/components/Uplot/utils.ts
Normal file
48
frontend/src/components/Uplot/utils.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import uPlot from 'uplot';
|
||||
|
||||
type OptionsUpdateState = 'keep' | 'update' | 'create';
|
||||
|
||||
export const optionsUpdateState = (
|
||||
_lhs: uPlot.Options,
|
||||
_rhs: uPlot.Options,
|
||||
): OptionsUpdateState => {
|
||||
const { width: lhsWidth, height: lhsHeight, ...lhs } = _lhs;
|
||||
const { width: rhsWidth, height: rhsHeight, ...rhs } = _rhs;
|
||||
|
||||
let state: OptionsUpdateState = 'keep';
|
||||
|
||||
if (lhsHeight !== rhsHeight || lhsWidth !== rhsWidth) {
|
||||
state = 'update';
|
||||
}
|
||||
if (Object.keys(lhs)?.length !== Object.keys(rhs)?.length) {
|
||||
return 'create';
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const k of Object.keys(lhs)) {
|
||||
if (!Object.is((lhs as any)[k], (rhs as any)[k])) {
|
||||
state = 'create';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const dataMatch = (
|
||||
lhs: uPlot.AlignedData,
|
||||
rhs: uPlot.AlignedData,
|
||||
): boolean => {
|
||||
if (lhs?.length !== rhs?.length) {
|
||||
return false;
|
||||
}
|
||||
return lhs.every((lhsOneSeries, seriesIdx) => {
|
||||
const rhsOneSeries = rhs[seriesIdx];
|
||||
if (lhsOneSeries?.length !== rhsOneSeries?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// compare each value in the series
|
||||
return (lhsOneSeries as number[])?.every(
|
||||
(value, valueIdx) => value === rhsOneSeries[valueIdx],
|
||||
);
|
||||
});
|
||||
};
|
||||
31
frontend/src/components/ValueGraph/ValueGraph.styles.scss
Normal file
31
frontend/src/components/ValueGraph/ValueGraph.styles.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.value-graph-container {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.value-graph-text {
|
||||
font-size: 2.5vw;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.value-graph-bgconflict {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.value-graph-textconflict {
|
||||
margin-left: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.value-graph-icon {
|
||||
color: #E89A3C;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,66 @@
|
||||
import { Value } from './styles';
|
||||
import './ValueGraph.styles.scss';
|
||||
|
||||
function ValueGraph({ value }: ValueGraphProps): JSX.Element {
|
||||
return <Value>{value}</Value>;
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getBackgroundColorAndThresholdCheck } from './utils';
|
||||
|
||||
function ValueGraph({
|
||||
value,
|
||||
rawValue,
|
||||
thresholds,
|
||||
}: ValueGraphProps): JSX.Element {
|
||||
const { t } = useTranslation(['valueGraph']);
|
||||
|
||||
const {
|
||||
threshold,
|
||||
isConflictingThresholds,
|
||||
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="value-graph-container"
|
||||
style={{
|
||||
backgroundColor:
|
||||
threshold.thresholdFormat === 'Background'
|
||||
? threshold.thresholdColor
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Typography.Text
|
||||
className="value-graph-text"
|
||||
style={{
|
||||
color:
|
||||
threshold.thresholdFormat === 'Text'
|
||||
? threshold.thresholdColor
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
{isConflictingThresholds && (
|
||||
<div
|
||||
className={
|
||||
threshold.thresholdFormat === 'Background'
|
||||
? 'value-graph-bgconflict'
|
||||
: 'value-graph-textconflict'
|
||||
}
|
||||
>
|
||||
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||
<ExclamationCircleFilled className="value-graph-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ValueGraphProps {
|
||||
value: string;
|
||||
rawValue: number;
|
||||
thresholds: ThresholdProps[];
|
||||
}
|
||||
|
||||
export default ValueGraph;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Value = styled(Typography)`
|
||||
font-size: 2.5vw;
|
||||
text-align: center;
|
||||
`;
|
||||
97
frontend/src/components/ValueGraph/utils.ts
Normal file
97
frontend/src/components/ValueGraph/utils.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
|
||||
function compareThreshold(
|
||||
rawValue: number,
|
||||
threshold: ThresholdProps,
|
||||
): boolean {
|
||||
if (
|
||||
threshold.thresholdOperator === undefined ||
|
||||
threshold.thresholdValue === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
switch (threshold.thresholdOperator) {
|
||||
case '>':
|
||||
return rawValue > threshold.thresholdValue;
|
||||
case '>=':
|
||||
return rawValue >= threshold.thresholdValue;
|
||||
case '<':
|
||||
return rawValue < threshold.thresholdValue;
|
||||
case '<=':
|
||||
return rawValue <= threshold.thresholdValue;
|
||||
case '=':
|
||||
return rawValue === threshold.thresholdValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractNumbersFromString(inputString: string): number[] {
|
||||
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||
const matches = inputString.match(regex);
|
||||
|
||||
if (matches) {
|
||||
return matches.map(Number);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getHighestPrecedenceThreshold(
|
||||
matchingThresholds: ThresholdProps[],
|
||||
thresholds: ThresholdProps[],
|
||||
): ThresholdProps | null {
|
||||
if (matchingThresholds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// whichever threshold from matchingThresholds is found first in thresholds array return the threshold from thresholds array
|
||||
let highestPrecedenceThreshold = matchingThresholds[0];
|
||||
for (let i = 1; i < matchingThresholds.length; i += 1) {
|
||||
if (
|
||||
thresholds.indexOf(matchingThresholds[i]) <
|
||||
thresholds.indexOf(highestPrecedenceThreshold)
|
||||
) {
|
||||
highestPrecedenceThreshold = matchingThresholds[i];
|
||||
}
|
||||
}
|
||||
|
||||
return highestPrecedenceThreshold;
|
||||
}
|
||||
|
||||
export function getBackgroundColorAndThresholdCheck(
|
||||
thresholds: ThresholdProps[],
|
||||
rawValue: number,
|
||||
): {
|
||||
threshold: ThresholdProps;
|
||||
isConflictingThresholds: boolean;
|
||||
} {
|
||||
const matchingThresholds = thresholds.filter((threshold) =>
|
||||
compareThreshold(
|
||||
extractNumbersFromString(
|
||||
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
|
||||
)[0],
|
||||
threshold,
|
||||
),
|
||||
);
|
||||
|
||||
if (matchingThresholds.length === 0) {
|
||||
return {
|
||||
threshold: {} as ThresholdProps,
|
||||
isConflictingThresholds: false,
|
||||
};
|
||||
}
|
||||
|
||||
const highestPrecedenceThreshold = getHighestPrecedenceThreshold(
|
||||
matchingThresholds,
|
||||
thresholds,
|
||||
);
|
||||
|
||||
const isConflictingThresholds = matchingThresholds.length > 1;
|
||||
|
||||
return {
|
||||
threshold: highestPrecedenceThreshold || ({} as ThresholdProps),
|
||||
isConflictingThresholds,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum Events {
|
||||
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Graph from 'components/Graph';
|
||||
import Uplot from 'components/Uplot';
|
||||
import GridTableComponent from 'container/GridTableComponent';
|
||||
import GridValueComponent from 'container/GridValueComponent';
|
||||
|
||||
import { PANEL_TYPES } from './queryBuilder';
|
||||
|
||||
export const PANEL_TYPES_COMPONENT_MAP = {
|
||||
[PANEL_TYPES.TIME_SERIES]: Graph,
|
||||
[PANEL_TYPES.TIME_SERIES]: Uplot,
|
||||
[PANEL_TYPES.VALUE]: GridValueComponent,
|
||||
[PANEL_TYPES.TABLE]: GridTableComponent,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
|
||||
@@ -26,4 +26,6 @@ export enum QueryParams {
|
||||
linesPerRow = 'linesPerRow',
|
||||
viewName = 'viewName',
|
||||
viewKey = 'viewKey',
|
||||
expandedWidgetId = 'expandedWidgetId',
|
||||
pagination = 'pagination',
|
||||
}
|
||||
|
||||
@@ -31,13 +31,12 @@ const ROUTES = {
|
||||
LOGS: '/logs',
|
||||
LOGS_EXPLORER: '/logs-explorer',
|
||||
LIVE_LOGS: '/logs-explorer/live',
|
||||
LOGS_PIPELINES: '/pipelines',
|
||||
HOME_PAGE: '/',
|
||||
PASSWORD_RESET: '/password-reset',
|
||||
LIST_LICENSES: '/licenses',
|
||||
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
|
||||
LOGS_PIPELINE: '/logs-explorer/pipeline',
|
||||
TRACE_EXPLORER: '/trace-explorer',
|
||||
PIPELINES: '/pipelines',
|
||||
BILLING: '/billing',
|
||||
SUPPORT: '/support',
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
|
||||
@@ -9,7 +9,6 @@ const themeColors = {
|
||||
silver: '#BDBDBD',
|
||||
outrageousOrange: '#FF6633',
|
||||
roseBud: '#FFB399',
|
||||
magentaPink: '#FF33FF',
|
||||
canary: '#FFFF99',
|
||||
deepSkyBlue: '#00B3E6',
|
||||
goldTips: '#E6B333',
|
||||
@@ -31,6 +30,52 @@ const themeColors = {
|
||||
hemlock: '#66664D',
|
||||
vidaLoca: '#4D8000',
|
||||
rust: '#B33300',
|
||||
red: '#FF0000', // Adding more colors, we need to get better colors from design team
|
||||
blue: '#0000FF',
|
||||
green: '#00FF00',
|
||||
yellow: '#FFFF00',
|
||||
purple: '#800080',
|
||||
cyan: '#00FFFF',
|
||||
magenta: '#FF00FF',
|
||||
orange: '#FFA500',
|
||||
pink: '#FFC0CB',
|
||||
brown: '#A52A2A',
|
||||
teal: '#008080',
|
||||
lime: '#00FF00',
|
||||
maroon: '#800000',
|
||||
navy: '#000080',
|
||||
aquamarine: '#7FFFD4',
|
||||
gold: '#FFD700',
|
||||
gray: '#808080',
|
||||
skyBlue: '#87CEEB',
|
||||
indigo: '#4B0082',
|
||||
slateGray: '#708090',
|
||||
chocolate: '#D2691E',
|
||||
tomato: '#FF6347',
|
||||
steelBlue: '#4682B4',
|
||||
peru: '#CD853F',
|
||||
darkOliveGreen: '#556B2F',
|
||||
indianRed: '#CD5C5C',
|
||||
mediumSlateBlue: '#7B68EE',
|
||||
rosyBrown: '#BC8F8F',
|
||||
darkSlateGray: '#2F4F4F',
|
||||
mediumAquamarine: '#66CDAA',
|
||||
lavender: '#E6E6FA',
|
||||
thistle: '#D8BFD8',
|
||||
salmon: '#FA8072',
|
||||
darkSalmon: '#E9967A',
|
||||
paleVioletRed: '#DB7093',
|
||||
mediumPurple: '#9370DB',
|
||||
darkOrchid: '#9932CC',
|
||||
lawnGreen: '#7CFC00',
|
||||
mediumSeaGreen: '#3CB371',
|
||||
lightCoral: '#F08080',
|
||||
darkSeaGreen: '#8FBC8F',
|
||||
sandyBrown: '#F4A460',
|
||||
darkKhaki: '#BDB76B',
|
||||
cornflowerBlue: '#6495ED',
|
||||
mediumVioletRed: '#C71585',
|
||||
paleGreen: '#98FB98',
|
||||
},
|
||||
errorColor: '#d32f2f',
|
||||
royalGrey: '#888888',
|
||||
|
||||
@@ -6,7 +6,9 @@ import Header from 'container/Header';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ReactNode, useEffect, useMemo, useRef } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
@@ -203,12 +205,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
{isToDisplayLayout && <Header />}
|
||||
<Layout>
|
||||
{isToDisplayLayout && !renderFullScreen && <SideNav />}
|
||||
<LayoutContent>
|
||||
<ChildrenContainer>
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
</LayoutContent>
|
||||
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<LayoutContent>
|
||||
<ChildrenContainer>
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
</LayoutContent>
|
||||
</ErrorBoundary>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ export const Layout = styled(LayoutComponent)`
|
||||
&&& {
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: calc(100vh - 4rem);
|
||||
min-height: calc(100vh - 8rem);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { billingSuccessResponse } from 'mocks-server/__mockdata__/billing';
|
||||
import {
|
||||
notOfTrailResponse,
|
||||
trialConvertedToSubscriptionResponse,
|
||||
} from 'mocks-server/__mockdata__/licenses';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { act, render, screen } from 'tests/test-utils';
|
||||
import { getFormattedDate } from 'utils/timeUtils';
|
||||
|
||||
import BillingContainer from './BillingContainer';
|
||||
|
||||
const lisenceUrl = 'http://localhost/api/v2/licenses';
|
||||
|
||||
describe('BillingContainer', () => {
|
||||
test('Component should render', async () => {
|
||||
act(() => {
|
||||
render(<BillingContainer />);
|
||||
});
|
||||
const unit = screen.getAllByText(/unit/i);
|
||||
expect(unit[1]).toBeInTheDocument();
|
||||
const dataInjection = screen.getByRole('columnheader', {
|
||||
name: /data ingested/i,
|
||||
});
|
||||
expect(dataInjection).toBeInTheDocument();
|
||||
const pricePerUnit = screen.getByRole('columnheader', {
|
||||
name: /price per unit/i,
|
||||
});
|
||||
expect(pricePerUnit).toBeInTheDocument();
|
||||
const cost = screen.getByRole('columnheader', {
|
||||
name: /cost \(billing period to date\)/i,
|
||||
});
|
||||
expect(cost).toBeInTheDocument();
|
||||
|
||||
const total = screen.getByRole('cell', {
|
||||
name: /total/i,
|
||||
});
|
||||
expect(total).toBeInTheDocument();
|
||||
|
||||
const manageBilling = screen.getByRole('button', {
|
||||
name: /manage billing/i,
|
||||
});
|
||||
expect(manageBilling).toBeInTheDocument();
|
||||
|
||||
const dollar = screen.getByRole('cell', {
|
||||
name: /\$0/i,
|
||||
});
|
||||
expect(dollar).toBeInTheDocument();
|
||||
|
||||
const currentBill = screen.getByRole('heading', {
|
||||
name: /current bill total/i,
|
||||
});
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('OnTrail', async () => {
|
||||
act(() => {
|
||||
render(<BillingContainer />);
|
||||
});
|
||||
|
||||
const freeTrailText = await screen.findByText('Free Trial');
|
||||
expect(freeTrailText).toBeInTheDocument();
|
||||
|
||||
const currentBill = await screen.findByRole('heading', {
|
||||
name: /current bill total/i,
|
||||
});
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
|
||||
const dollar0 = await screen.findByText(/\$0/i);
|
||||
expect(dollar0).toBeInTheDocument();
|
||||
const onTrail = await screen.findByText(
|
||||
/You are in free trial period. Your free trial will end on 20 Oct 2023/i,
|
||||
);
|
||||
expect(onTrail).toBeInTheDocument();
|
||||
|
||||
const numberOfDayRemaining = await screen.findByText(
|
||||
/1 days remaining in your billing period./i,
|
||||
);
|
||||
expect(numberOfDayRemaining).toBeInTheDocument();
|
||||
const upgradeButton = await screen.findAllByRole('button', {
|
||||
name: /upgrade/i,
|
||||
});
|
||||
expect(upgradeButton[1]).toBeInTheDocument();
|
||||
expect(upgradeButton.length).toBe(2);
|
||||
const checkPaidPlan = await screen.findByText(
|
||||
/Check out features in paid plans/i,
|
||||
);
|
||||
expect(checkPaidPlan).toBeInTheDocument();
|
||||
|
||||
const link = screen.getByRole('link', { name: /here/i });
|
||||
expect(link).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('OnTrail but trialConvertedToSubscription', async () => {
|
||||
server.use(
|
||||
rest.get(lisenceUrl, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(trialConvertedToSubscriptionResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
render(<BillingContainer />);
|
||||
});
|
||||
|
||||
const currentBill = await screen.findByRole('heading', {
|
||||
name: /current bill total/i,
|
||||
});
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
|
||||
const dollar0 = await screen.findByText(/\$0/i);
|
||||
expect(dollar0).toBeInTheDocument();
|
||||
|
||||
const onTrail = await screen.findByText(
|
||||
/You are in free trial period. Your free trial will end on 20 Oct 2023/i,
|
||||
);
|
||||
expect(onTrail).toBeInTheDocument();
|
||||
|
||||
const receivedCardDetails = await screen.findByText(
|
||||
/We have received your card details, your billing will only start after the end of your free trial period./i,
|
||||
);
|
||||
expect(receivedCardDetails).toBeInTheDocument();
|
||||
|
||||
const manageBillingButton = await screen.findByRole('button', {
|
||||
name: /manage billing/i,
|
||||
});
|
||||
expect(manageBillingButton).toBeInTheDocument();
|
||||
|
||||
const dayRemainingInBillingPeriod = await screen.findByText(
|
||||
/1 days remaining in your billing period./i,
|
||||
);
|
||||
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Not on ontrail', async () => {
|
||||
server.use(
|
||||
rest.get(lisenceUrl, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(notOfTrailResponse)),
|
||||
),
|
||||
);
|
||||
render(<BillingContainer />);
|
||||
|
||||
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
|
||||
billingSuccessResponse.data.billingPeriodStart,
|
||||
)} to ${getFormattedDate(billingSuccessResponse.data.billingPeriodEnd)}`;
|
||||
|
||||
const billingPeriod = await screen.findByRole('heading', {
|
||||
name: new RegExp(billingPeriodText, 'i'),
|
||||
});
|
||||
expect(billingPeriod).toBeInTheDocument();
|
||||
|
||||
const currentBill = await screen.findByRole('heading', {
|
||||
name: /current bill total/i,
|
||||
});
|
||||
expect(currentBill).toBeInTheDocument();
|
||||
|
||||
const dollar0 = await screen.findAllByText(/\$1278.3/i);
|
||||
expect(dollar0[0]).toBeInTheDocument();
|
||||
expect(dollar0.length).toBe(2);
|
||||
|
||||
const metricsRow = await screen.findByRole('row', {
|
||||
name: /metrics Million 4012 0.1 \$ 401.2/i,
|
||||
});
|
||||
expect(metricsRow).toBeInTheDocument();
|
||||
|
||||
const logRow = await screen.findByRole('row', {
|
||||
name: /Logs GB 497 0.4 \$ 198.8/i,
|
||||
});
|
||||
expect(logRow).toBeInTheDocument();
|
||||
|
||||
const totalBill = await screen.findByRole('cell', {
|
||||
name: /\$1278/i,
|
||||
});
|
||||
expect(totalBill).toBeInTheDocument();
|
||||
|
||||
const totalBillRow = await screen.findByRole('row', {
|
||||
name: /total \$1278/i,
|
||||
});
|
||||
expect(totalBillRow).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should render corrent day remaining in billing period', async () => {
|
||||
server.use(
|
||||
rest.get(lisenceUrl, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(notOfTrailResponse)),
|
||||
),
|
||||
);
|
||||
render(<BillingContainer />);
|
||||
const dayRemainingInBillingPeriod = await screen.findByText(
|
||||
/11 days remaining in your billing period./i,
|
||||
);
|
||||
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import getUsage from 'api/billing/getUsage';
|
||||
import manageCreditCardApi from 'api/billing/manage';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -20,7 +21,7 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { License } from 'types/api/licenses/def';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { getFormattedDate } from 'utils/timeUtils';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
|
||||
interface DataType {
|
||||
key: string;
|
||||
@@ -98,19 +99,6 @@ const dummyColumns: ColumnsType<DataType> = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getRemainingDays = (billingEndDate: number): number => {
|
||||
// Convert Epoch timestamps to Date objects
|
||||
const startDate = new Date(); // Convert seconds to milliseconds
|
||||
const endDate = new Date(billingEndDate * 1000); // Convert seconds to milliseconds
|
||||
|
||||
// Calculate the time difference in milliseconds
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const timeDifference = endDate - startDate;
|
||||
|
||||
return Math.ceil(timeDifference / (1000 * 60 * 60 * 24));
|
||||
};
|
||||
|
||||
export default function BillingContainer(): JSX.Element {
|
||||
const daysRemainingStr = 'days remaining in your billing period.';
|
||||
const [headerText, setHeaderText] = useState('');
|
||||
@@ -122,9 +110,11 @@ export default function BillingContainer(): JSX.Element {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const billCurrency = '$';
|
||||
|
||||
const { trackEvent } = useAnalytics();
|
||||
|
||||
const { isFetching, data: licensesData, error: licenseError } = useLicense();
|
||||
|
||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const handleError = useAxiosError();
|
||||
@@ -314,18 +304,29 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
const handleBilling = useCallback(async () => {
|
||||
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
||||
trackEvent('Billing : Upgrade Plan', {
|
||||
user,
|
||||
org,
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
licenseKey: activeLicense?.key || '',
|
||||
successURL: window.location.href,
|
||||
cancelURL: window.location.href,
|
||||
});
|
||||
} else {
|
||||
trackEvent('Billing : Manage Billing', {
|
||||
user,
|
||||
org,
|
||||
});
|
||||
|
||||
manageCreditCard({
|
||||
licenseKey: activeLicense?.key || '',
|
||||
successURL: window.location.href,
|
||||
cancelURL: window.location.href,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
activeLicense?.key,
|
||||
isFreeTrial,
|
||||
@@ -445,7 +446,12 @@ export default function BillingContainer(): JSX.Element {
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={4} style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
loading={isLoadingBilling || isLoadingManageBilling}
|
||||
onClick={handleBilling}
|
||||
>
|
||||
Upgrade Plan
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
4
frontend/src/container/Download/Download.styles.scss
Normal file
4
frontend/src/container/Download/Download.styles.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.download-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
77
frontend/src/container/Download/Download.tsx
Normal file
77
frontend/src/container/Download/Download.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import './Download.styles.scss';
|
||||
|
||||
import { CloudDownloadOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import { Excel } from 'antd-table-saveas-excel';
|
||||
import { unparse } from 'papaparse';
|
||||
|
||||
import { DownloadProps } from './Download.types';
|
||||
|
||||
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
|
||||
const downloadExcelFile = (): void => {
|
||||
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
|
||||
const updatedTitle = item
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return {
|
||||
title: updatedTitle,
|
||||
dataIndex: item,
|
||||
};
|
||||
});
|
||||
const excel = new Excel();
|
||||
excel
|
||||
.addSheet(fileName)
|
||||
.addColumns(headers)
|
||||
.addDataSource(data, {
|
||||
str2Percent: true,
|
||||
})
|
||||
.saveAs(`${fileName}.xlsx`);
|
||||
};
|
||||
|
||||
const downloadCsvFile = (): void => {
|
||||
const csv = unparse(data);
|
||||
const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const csvUrl = URL.createObjectURL(csvBlob);
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = csvUrl;
|
||||
downloadLink.download = `${fileName}.csv`;
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
};
|
||||
|
||||
const menu: MenuProps = {
|
||||
items: [
|
||||
{
|
||||
key: 'download-as-excel',
|
||||
label: 'Excel',
|
||||
onClick: downloadExcelFile,
|
||||
},
|
||||
{
|
||||
key: 'download-as-csv',
|
||||
label: 'CSV',
|
||||
onClick: downloadCsvFile,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} trigger={['click']}>
|
||||
<Button
|
||||
className="download-button"
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
>
|
||||
<CloudDownloadOutlined />
|
||||
Download
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
Download.defaultProps = {
|
||||
isLoading: undefined,
|
||||
};
|
||||
|
||||
export default Download;
|
||||
10
frontend/src/container/Download/Download.types.ts
Normal file
10
frontend/src/container/Download/Download.types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type DownloadOptions = {
|
||||
isDownloadEnabled: boolean;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
export type DownloadProps = {
|
||||
data: Record<string, string>[];
|
||||
isLoading?: boolean;
|
||||
fileName: string;
|
||||
};
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
@@ -57,6 +57,12 @@ function EditAlertChannels({
|
||||
setType(value as ChannelType);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
formInstance.setFieldsValue({
|
||||
...initialValue,
|
||||
});
|
||||
}, [formInstance, initialValue]);
|
||||
|
||||
const prepareSlackRequest = useCallback(
|
||||
() => ({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Form, Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import ChannelSelect from './ChannelSelect';
|
||||
import LabelSelect from './labels';
|
||||
@@ -36,6 +37,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
name={['labels', 'severity']}
|
||||
>
|
||||
<SeveritySelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue="critical"
|
||||
onChange={(value: unknown | string): void => {
|
||||
const s = (value as string) || 'critical';
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { StaticLineProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useMemo } from 'react';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||
import { covertIntoDataFormats } from './utils';
|
||||
import { getThresholdLabel } from './utils';
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
@@ -41,26 +47,9 @@ function ChartPreview({
|
||||
}: ChartPreviewProps): JSX.Element | null {
|
||||
const { t } = useTranslation('alerts');
|
||||
const threshold = alertDef?.condition.target || 0;
|
||||
|
||||
const thresholdValue = covertIntoDataFormats({
|
||||
value: threshold,
|
||||
sourceUnit: alertDef?.condition.targetUnit,
|
||||
targetUnit: query?.unit,
|
||||
});
|
||||
|
||||
const staticLine: StaticLineProps | undefined =
|
||||
threshold !== undefined
|
||||
? {
|
||||
yMin: thresholdValue,
|
||||
yMax: thresholdValue,
|
||||
borderColor: '#f14',
|
||||
borderWidth: 1,
|
||||
lineText: `${t('preview_chart_threshold_label')} (y=${thresholdValue} ${
|
||||
query?.unit || ''
|
||||
})`,
|
||||
textColor: '#f14',
|
||||
}
|
||||
: undefined;
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const canQuery = useMemo((): boolean => {
|
||||
if (!query || query == null) {
|
||||
@@ -100,21 +89,63 @@ function ChartPreview({
|
||||
'chartPreview',
|
||||
userQueryKey || JSON.stringify(query),
|
||||
selectedInterval,
|
||||
minTime,
|
||||
maxTime,
|
||||
],
|
||||
retry: false,
|
||||
enabled: canQuery,
|
||||
},
|
||||
);
|
||||
|
||||
const chartDataSet = queryResponse.isError
|
||||
? null
|
||||
: getChartData({
|
||||
queryData: [
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const optionName =
|
||||
getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || '';
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: 'alert_legend_widget',
|
||||
yAxisUnit: query?.unit,
|
||||
apiResponse: queryResponse?.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
thresholds: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result ?? [],
|
||||
index: '0', // no impact
|
||||
keyIndex: 0,
|
||||
moveThreshold: (): void => {},
|
||||
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
|
||||
thresholdValue: threshold,
|
||||
thresholdLabel: `${t(
|
||||
'preview_chart_threshold_label',
|
||||
)} (y=${getThresholdLabel(
|
||||
optionName,
|
||||
threshold,
|
||||
alertDef?.condition.targetUnit,
|
||||
query?.unit,
|
||||
)})`,
|
||||
thresholdUnit: alertDef?.condition.targetUnit,
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
[
|
||||
query?.unit,
|
||||
queryResponse?.data?.payload,
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
threshold,
|
||||
t,
|
||||
optionName,
|
||||
alertDef?.condition.targetUnit,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
@@ -128,18 +159,18 @@ function ChartPreview({
|
||||
{queryResponse.isLoading && (
|
||||
<Spinner size="large" tip="Loading..." height="70vh" />
|
||||
)}
|
||||
{chartDataSet && !queryResponse.isError && (
|
||||
<GridPanelSwitch
|
||||
panelType={graphType}
|
||||
title={name}
|
||||
data={chartDataSet.data}
|
||||
isStacked
|
||||
name={name || 'Chart Preview'}
|
||||
staticLine={staticLine}
|
||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
||||
query={query || initialQueriesMap.metrics}
|
||||
yAxisUnit={query?.unit}
|
||||
/>
|
||||
{chartData && !queryResponse.isError && (
|
||||
<div ref={graphRef} style={{ height: '100%' }}>
|
||||
<GridPanelSwitch
|
||||
options={options}
|
||||
panelType={graphType}
|
||||
data={chartData}
|
||||
name={name || 'Chart Preview'}
|
||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
||||
query={query || initialQueriesMap.metrics}
|
||||
yAxisUnit={query?.unit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ChartContainer>
|
||||
);
|
||||
|
||||
@@ -51,6 +51,21 @@ export function covertIntoDataFormats({
|
||||
return Number.isNaN(result) ? 0 : result;
|
||||
}
|
||||
|
||||
export const getThresholdLabel = (
|
||||
optionName: string,
|
||||
value: number,
|
||||
unit?: string,
|
||||
yAxisUnit?: string,
|
||||
): string => {
|
||||
if (
|
||||
unit === MiscellaneousFormats.PercentUnit ||
|
||||
yAxisUnit === MiscellaneousFormats.PercentUnit
|
||||
) {
|
||||
return `${value * 100}%`;
|
||||
}
|
||||
return `${value} ${optionName}`;
|
||||
};
|
||||
|
||||
interface IUnit {
|
||||
value: number;
|
||||
sourceUnit?: string;
|
||||
|
||||
@@ -5,12 +5,16 @@ function PromqlSection(): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
return (
|
||||
<PromQLQueryBuilder
|
||||
key="A"
|
||||
queryIndex={0}
|
||||
queryData={currentQuery.promql[0]}
|
||||
deletable={false}
|
||||
/>
|
||||
<>
|
||||
{currentQuery.promql.map((query, index) => (
|
||||
<PromQLQueryBuilder
|
||||
key={query.name}
|
||||
queryIndex={index}
|
||||
queryData={query}
|
||||
deletable={false}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import {
|
||||
getCategoryByOptionId,
|
||||
getCategorySelectOptionByName,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { FormContainer, InlineSelect, StepHeading } from './styles';
|
||||
|
||||
@@ -27,6 +29,7 @@ function RuleOptions({
|
||||
alertDef,
|
||||
setAlertDef,
|
||||
queryCategory,
|
||||
queryOptions,
|
||||
}: RuleOptionsProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
@@ -43,8 +46,21 @@ function RuleOptions({
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeSelectedQueryName = (value: string | unknown): void => {
|
||||
if (typeof value !== 'string') return;
|
||||
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
selectedQueryName: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderCompareOps = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue={defaultCompareOp}
|
||||
value={alertDef.condition?.op}
|
||||
style={{ minWidth: '120px' }}
|
||||
@@ -69,6 +85,7 @@ function RuleOptions({
|
||||
|
||||
const renderThresholdMatchOpts = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
@@ -83,6 +100,7 @@ function RuleOptions({
|
||||
|
||||
const renderPromMatchOpts = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
@@ -94,6 +112,7 @@ function RuleOptions({
|
||||
|
||||
const renderEvalWindows = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue={defaultEvalWindow}
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
@@ -117,16 +136,38 @@ function RuleOptions({
|
||||
const renderThresholdRuleOpts = (): JSX.Element => (
|
||||
<Form.Item>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
|
||||
{t('text_condition1')}
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={queryOptions}
|
||||
placeholder={t('selected_query_placeholder')}
|
||||
value={alertDef.condition.selectedQueryName}
|
||||
onChange={onChangeSelectedQueryName}
|
||||
/>
|
||||
<Typography.Text>is</Typography.Text>
|
||||
{renderCompareOps()} {t('text_condition2')} {renderThresholdMatchOpts()}{' '}
|
||||
{t('text_condition3')} {renderEvalWindows()}
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const renderPromRuleOptions = (): JSX.Element => (
|
||||
<Form.Item>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderPromMatchOpts()}
|
||||
{t('text_condition1')}
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={queryOptions}
|
||||
placeholder={t('selected_query_placeholder')}
|
||||
value={alertDef.condition.selectedQueryName}
|
||||
onChange={onChangeSelectedQueryName}
|
||||
/>
|
||||
<Typography.Text>is</Typography.Text>
|
||||
{renderCompareOps()} {t('text_condition2')} {renderPromMatchOpts()}
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
);
|
||||
@@ -167,7 +208,7 @@ function RuleOptions({
|
||||
? renderPromRuleOptions()
|
||||
: renderThresholdRuleOpts()}
|
||||
|
||||
<Space align="start">
|
||||
<Space direction="horizontal" align="center">
|
||||
<Form.Item noStyle name={['condition', 'target']}>
|
||||
<InputNumber
|
||||
addonBefore={t('field_threshold')}
|
||||
@@ -178,8 +219,9 @@ function RuleOptions({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Form.Item noStyle>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
allowClear
|
||||
showSearch
|
||||
options={categorySelectOptions}
|
||||
@@ -198,5 +240,6 @@ interface RuleOptionsProps {
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
queryCategory: EQueryType;
|
||||
queryOptions: DefaultOptionType[];
|
||||
}
|
||||
export default RuleOptions;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { Col, FormInstance, Modal, Tooltip, Typography } from 'antd';
|
||||
import {
|
||||
Col,
|
||||
FormInstance,
|
||||
Modal,
|
||||
SelectProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import testAlertApi from 'api/alerts/testAlert';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -44,7 +51,7 @@ import {
|
||||
StyledLeftContainer,
|
||||
} from './styles';
|
||||
import UserGuide from './UserGuide';
|
||||
import { getUpdatedStepInterval, toChartInterval } from './utils';
|
||||
import { getSelectedQueryOptions } from './utils';
|
||||
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
@@ -55,9 +62,10 @@ function FormAlertRules({
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
@@ -80,6 +88,20 @@ function FormAlertRules({
|
||||
initialValue,
|
||||
]);
|
||||
|
||||
const queryOptions = useMemo(() => {
|
||||
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
|
||||
[EQueryType.QUERY_BUILDER]: () => [
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
|
||||
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
|
||||
],
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
}, [currentQuery]);
|
||||
|
||||
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
||||
|
||||
useShareBuilderUrl(sq);
|
||||
@@ -88,6 +110,18 @@ function FormAlertRules({
|
||||
setAlertDef(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set selectedQueryName based on the length of queryOptions
|
||||
setAlertDef((def) => ({
|
||||
...def,
|
||||
condition: {
|
||||
...def.condition,
|
||||
selectedQueryName:
|
||||
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
|
||||
},
|
||||
}));
|
||||
}, [currentQuery?.queryType, queryOptions]);
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, []);
|
||||
@@ -354,16 +388,6 @@ function FormAlertRules({
|
||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||
);
|
||||
|
||||
const updatedStagedQuery = useMemo((): Query | null => {
|
||||
const newQuery: Query | null = stagedQuery;
|
||||
if (newQuery) {
|
||||
newQuery.builder.queryData[0].stepInterval = getUpdatedStepInterval(
|
||||
alertDef.evalWindow,
|
||||
);
|
||||
}
|
||||
return newQuery;
|
||||
}, [alertDef.evalWindow, stagedQuery]);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={
|
||||
@@ -373,14 +397,13 @@ function FormAlertRules({
|
||||
/>
|
||||
}
|
||||
name=""
|
||||
query={updatedStagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
alertDef={alertDef}
|
||||
allowSelectedIntervalForStepGen
|
||||
/>
|
||||
);
|
||||
|
||||
const renderPromChartPreview = (): JSX.Element => (
|
||||
const renderPromAndChQueryChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={
|
||||
<PlotTag
|
||||
@@ -391,21 +414,7 @@ function FormAlertRules({
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderChQueryChartPreview = (): JSX.Element => (
|
||||
<ChartPreview
|
||||
headline={
|
||||
<PlotTag
|
||||
queryType={currentQuery.queryType}
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -442,9 +451,10 @@ function FormAlertRules({
|
||||
>
|
||||
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||
renderQBChartPreview()}
|
||||
{currentQuery.queryType === EQueryType.PROM && renderPromChartPreview()}
|
||||
{currentQuery.queryType === EQueryType.PROM &&
|
||||
renderPromAndChQueryChartPreview()}
|
||||
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
|
||||
renderChQueryChartPreview()}
|
||||
renderPromAndChQueryChartPreview()}
|
||||
|
||||
<StepContainer>
|
||||
<BuilderUnitsFilter onChange={onUnitChangeHandler} />
|
||||
@@ -461,6 +471,7 @@ function FormAlertRules({
|
||||
queryCategory={currentQuery.queryType}
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
queryOptions={queryOptions}
|
||||
/>
|
||||
|
||||
{renderBasicInfo()}
|
||||
|
||||
@@ -59,8 +59,8 @@ export const StepHeading = styled.p`
|
||||
export const InlineSelect = styled(Select)`
|
||||
display: inline-block;
|
||||
width: 10% !important;
|
||||
margin-left: 0.2em;
|
||||
margin-right: 0.2em;
|
||||
margin-left: 0.3em;
|
||||
margin-right: 0.3em;
|
||||
`;
|
||||
|
||||
export const SeveritySelect = styled(Select)`
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import getStep from 'lib/getStep';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
IBuilderQuery,
|
||||
IClickHouseQuery,
|
||||
IPromQLQuery,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
// toChartInterval converts eval window to chart selection time interval
|
||||
export const toChartInterval = (evalWindow: string | undefined): Time => {
|
||||
@@ -35,3 +42,15 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => {
|
||||
inputFormat: 'ns',
|
||||
});
|
||||
};
|
||||
|
||||
export const getSelectedQueryOptions = (
|
||||
queries: Array<
|
||||
IBuilderQuery | IBuilderFormula | IClickHouseQuery | IPromQLQuery
|
||||
>,
|
||||
): SelectProps['options'] =>
|
||||
queries
|
||||
.filter((query) => !query.disabled)
|
||||
.map((query) => ({
|
||||
label: 'queryName' in query ? query.queryName : query.name,
|
||||
value: 'queryName' in query ? query.queryName : query.name,
|
||||
}));
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
.graph-manager-container {
|
||||
margin-top: 1.25rem;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
overflow-x: scroll;
|
||||
|
||||
.filter-table-container {
|
||||
flex-basis: 80%;
|
||||
}
|
||||
|
||||
.save-cancel-container {
|
||||
flex-basis: 20%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.save-cancel-button {
|
||||
margin: 0 0.313rem;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import './GraphManager.styles.scss';
|
||||
import './WidgetFullView.styles.scss';
|
||||
|
||||
import { Button, Input } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
|
||||
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||
@@ -19,34 +20,37 @@ function GraphManager({
|
||||
yAxisUnit,
|
||||
onToggleModelHandler,
|
||||
setGraphsVisibilityStates,
|
||||
graphsVisibilityStates = [],
|
||||
graphsVisibilityStates = [], // not trimed
|
||||
lineChartRef,
|
||||
parentChartRef,
|
||||
options,
|
||||
}: GraphManagerProps): JSX.Element {
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
|
||||
getDefaultTableDataSet(data),
|
||||
getDefaultTableDataSet(options, data),
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
|
||||
const checkBoxOnChangeHandler = useCallback(
|
||||
(e: CheckboxChangeEvent, index: number): void => {
|
||||
const newStates = [...graphsVisibilityStates];
|
||||
|
||||
newStates[index] = e.target.checked;
|
||||
|
||||
lineChartRef?.current?.toggleGraph(index, e.target.checked);
|
||||
|
||||
parentChartRef?.current?.toggleGraph(index, e.target.checked);
|
||||
setGraphsVisibilityStates([...newStates]);
|
||||
},
|
||||
[graphsVisibilityStates, setGraphsVisibilityStates, lineChartRef],
|
||||
[
|
||||
graphsVisibilityStates,
|
||||
lineChartRef,
|
||||
parentChartRef,
|
||||
setGraphsVisibilityStates,
|
||||
],
|
||||
);
|
||||
|
||||
const labelClickedHandler = useCallback(
|
||||
(labelIndex: number): void => {
|
||||
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||
false,
|
||||
);
|
||||
const newGraphVisibilityStates = Array<boolean>(data.length).fill(false);
|
||||
newGraphVisibilityStates[labelIndex] = true;
|
||||
|
||||
newGraphVisibilityStates.forEach((state, index) => {
|
||||
@@ -55,20 +59,16 @@ function GraphManager({
|
||||
});
|
||||
setGraphsVisibilityStates(newGraphVisibilityStates);
|
||||
},
|
||||
[
|
||||
data.datasets.length,
|
||||
setGraphsVisibilityStates,
|
||||
lineChartRef,
|
||||
parentChartRef,
|
||||
],
|
||||
[data.length, lineChartRef, parentChartRef, setGraphsVisibilityStates],
|
||||
);
|
||||
|
||||
const columns = getGraphManagerTableColumns({
|
||||
data,
|
||||
tableDataSet,
|
||||
checkBoxOnChangeHandler,
|
||||
graphVisibilityState: graphsVisibilityStates || [],
|
||||
graphVisibilityState: graphsVisibilityStates,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
isGraphDisabled: isDashboardLocked,
|
||||
});
|
||||
|
||||
const filterHandler = useCallback(
|
||||
@@ -87,7 +87,7 @@ function GraphManager({
|
||||
|
||||
const saveHandler = useCallback((): void => {
|
||||
saveLegendEntriesToLocalStorage({
|
||||
data,
|
||||
options,
|
||||
graphVisibilityState: graphsVisibilityStates || [],
|
||||
name,
|
||||
});
|
||||
@@ -97,34 +97,49 @@ function GraphManager({
|
||||
if (onToggleModelHandler) {
|
||||
onToggleModelHandler();
|
||||
}
|
||||
}, [data, graphsVisibilityStates, name, notifications, onToggleModelHandler]);
|
||||
}, [
|
||||
graphsVisibilityStates,
|
||||
name,
|
||||
notifications,
|
||||
onToggleModelHandler,
|
||||
options,
|
||||
]);
|
||||
|
||||
const dataSource = tableDataSet.filter((item) => item.show);
|
||||
const dataSource = tableDataSet.filter(
|
||||
(item, index) => index !== 0 && item.show,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="graph-manager-container">
|
||||
<div className="filter-table-container">
|
||||
<div className="graph-manager-header">
|
||||
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||
<div className="save-cancel-container">
|
||||
<span className="save-cancel-button">
|
||||
<Button type="default" onClick={onToggleModelHandler}>
|
||||
Cancel
|
||||
</Button>
|
||||
</span>
|
||||
<span className="save-cancel-button">
|
||||
<Button type="primary" onClick={saveHandler}>
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="legends-list-container">
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
rowKey="index"
|
||||
pagination={false}
|
||||
scroll={{ y: 240 }}
|
||||
style={{
|
||||
maxHeight: 200,
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="save-cancel-container">
|
||||
<span className="save-cancel-button">
|
||||
<Button type="default" onClick={onToggleModelHandler}>
|
||||
Cancel
|
||||
</Button>
|
||||
</span>
|
||||
<span className="save-cancel-button">
|
||||
<Button onClick={saveHandler} type="primary">
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,14 +9,13 @@ function CustomCheckBox({
|
||||
index,
|
||||
graphVisibilityState = [],
|
||||
checkBoxOnChangeHandler,
|
||||
disabled = false,
|
||||
}: CheckBoxProps): JSX.Element {
|
||||
const { datasets } = data;
|
||||
|
||||
const onChangeHandler = (e: CheckboxChangeEvent): void => {
|
||||
checkBoxOnChangeHandler(e, index);
|
||||
};
|
||||
|
||||
const color = datasets[index]?.borderColor?.toString() || grey[0];
|
||||
const color = data[index]?.stroke?.toString() || grey[0];
|
||||
|
||||
const isChecked = graphVisibilityState[index] || false;
|
||||
|
||||
@@ -30,7 +29,11 @@ function CustomCheckBox({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Checkbox onChange={onChangeHandler} checked={isChecked} />
|
||||
<Checkbox
|
||||
onChange={onChangeHandler}
|
||||
checked={isChecked}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user