Compare commits
169 Commits
v0.34.0
...
unit-testi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee4086dce5 | ||
|
|
d688399b91 | ||
|
|
cfc239e3c9 | ||
|
|
3572baa5eb | ||
|
|
ff26c5f69c | ||
|
|
9230f2442f | ||
|
|
7fed80b145 | ||
|
|
a268bb910c | ||
|
|
fbbe0bef86 | ||
|
|
bcd6ac47f7 | ||
|
|
ec27916fa5 | ||
|
|
263ac9fa5a | ||
|
|
7d73b144c9 | ||
|
|
e3b2882811 | ||
|
|
5fd919a369 | ||
|
|
63efb2b25a | ||
|
|
3159121929 | ||
|
|
788a38d39c | ||
|
|
bff39daef0 | ||
|
|
f2521b4c49 | ||
|
|
16b846006a | ||
|
|
0ea8e8e6b8 | ||
|
|
da0ea7eb49 | ||
|
|
ca41d7011e | ||
|
|
f15e8f2fed | ||
|
|
2e0fdbb498 | ||
|
|
585d6e2a21 | ||
|
|
d93cc767a6 | ||
|
|
a363b98657 | ||
|
|
2031a014a7 | ||
|
|
43d5ee9651 | ||
|
|
f8bb42a13c | ||
|
|
1be4731710 | ||
|
|
90b8959045 | ||
|
|
f487c1956b | ||
|
|
6b2f03d43f | ||
|
|
581bd07b35 | ||
|
|
c5cba68b53 | ||
|
|
a6b6abf1a7 | ||
|
|
7526888886 | ||
|
|
ce8fdd509b | ||
|
|
2baa6028b5 | ||
|
|
8e653f9500 | ||
|
|
cb1a823f91 | ||
|
|
c0b0920901 | ||
|
|
77b4e71543 | ||
|
|
9d44ce3ee2 | ||
|
|
1d014ab4f7 | ||
|
|
418ab67d50 | ||
|
|
7efe907757 | ||
|
|
1d1154aa8c | ||
|
|
a16fca6376 | ||
|
|
d5c3760dc9 | ||
|
|
9c1ea0cde9 | ||
|
|
ec500831ef | ||
|
|
fcbf82c2f3 | ||
|
|
a805eb7533 | ||
|
|
a8edc4fd95 | ||
|
|
c66c8c2823 | ||
|
|
c7b59d4405 | ||
|
|
f56b5cb971 | ||
|
|
29b1344557 | ||
|
|
55664872bd | ||
|
|
221861230a | ||
|
|
8b1a781f58 | ||
|
|
b557ca5519 | ||
|
|
e557ff273f | ||
|
|
3c284fc9ee | ||
|
|
bcebe050b1 | ||
|
|
9360c61dca | ||
|
|
fb1dbdc05e | ||
|
|
6170b2c5dc | ||
|
|
9826ab04b3 | ||
|
|
fd9566d471 | ||
|
|
3a1e8d523a | ||
|
|
6dd34a7f29 | ||
|
|
170e5e1686 | ||
|
|
16502feaad | ||
|
|
09d579311e | ||
|
|
950584af36 | ||
|
|
fb5d4475e2 | ||
|
|
6771857941 | ||
|
|
1e16df65ac | ||
|
|
933b70134c | ||
|
|
8072fede85 | ||
|
|
581e80cd37 | ||
|
|
112783d618 | ||
|
|
50836593a4 | ||
|
|
4644b1c200 | ||
|
|
f647e828dd | ||
|
|
bb09c84679 | ||
|
|
fc5f0fbf9e | ||
|
|
56fb58abed | ||
|
|
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 |
7
.github/CODEOWNERS
vendored
@@ -1,13 +1,10 @@
|
||||
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
|
||||
# Owners are automatically requested for review for PRs that changes code
|
||||
# that they own.
|
||||
* @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
|
||||
Makefile @srikanthccv
|
||||
go.* @srikanthccv
|
||||
.git* @srikanthccv
|
||||
.github @prashant-shahi
|
||||
|
||||
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
@@ -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
|
||||
|
||||
2
.github/workflows/staging-deployment.yaml
vendored
@@ -29,7 +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/signoz-schema-migrator:main
|
||||
docker pull signoz/signoz-schema-migrator:main
|
||||
cd ~/signoz
|
||||
git status
|
||||
git add .
|
||||
|
||||
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
|
||||
@@ -199,10 +199,13 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
|
||||
#### Frontend
|
||||
|
||||
- [Palash Gupta](https://github.com/palashgdev)
|
||||
- [Yunus M](https://github.com/YounixM)
|
||||
- [Rajat Dabade](https://github.com/Rajat-Dabade)
|
||||
|
||||
#### DevOps
|
||||
|
||||
- [Prashant Shahi](https://github.com/prashant-shahi)
|
||||
- [Dhawal Sanghvi](https://github.com/dhawal1248)
|
||||
|
||||
<br /><br />
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: "3.9"
|
||||
|
||||
x-clickhouse-defaults: &clickhouse-defaults
|
||||
image: clickhouse/clickhouse-server:23.7.3-alpine
|
||||
image: clickhouse/clickhouse-server:23.11.1-alpine
|
||||
tty: true
|
||||
deploy:
|
||||
restart_policy:
|
||||
@@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.34.0
|
||||
image: signoz/query-service:0.36.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.34.0
|
||||
image: signoz/frontend:0.36.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -199,7 +199,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.88.0
|
||||
image: signoz/signoz-otel-collector:0.88.4
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.88.0
|
||||
image: signoz/signoz-schema-migrator:0.88.4
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -250,7 +250,7 @@ services:
|
||||
# - clickhouse-3
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.88.0
|
||||
image: signoz/signoz-otel-collector:0.88.4
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.0}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.4}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.88.0
|
||||
image: signoz/signoz-otel-collector:0.88.4
|
||||
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.88.0
|
||||
image: signoz/signoz-otel-collector:0.88.4
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "2.4"
|
||||
x-clickhouse-defaults: &clickhouse-defaults
|
||||
restart: on-failure
|
||||
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
|
||||
image: clickhouse/clickhouse-server:23.7.3-alpine
|
||||
image: clickhouse/clickhouse-server:23.11.1-alpine
|
||||
tty: true
|
||||
depends_on:
|
||||
- zookeeper-1
|
||||
@@ -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.34.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.36.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.34.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.36.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.0}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.4}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.0}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.4}
|
||||
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.88.0}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.4}
|
||||
container_name: signoz-otel-collector-metrics
|
||||
command:
|
||||
[
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.18.3
|
||||
FROM alpine:3.18.5
|
||||
|
||||
# Add Maintainer Info
|
||||
LABEL maintainer="signoz"
|
||||
@@ -18,6 +18,7 @@ COPY ee/query-service/bin/query-service-${TARGETOS}-${TARGETARCH} /root/query-se
|
||||
|
||||
# copy prometheus YAML config
|
||||
COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml
|
||||
COPY pkg/query-service/templates /root/templates
|
||||
|
||||
# Make query-service executable for non-root users
|
||||
RUN chmod 755 /root /root/query-service
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -47,8 +48,18 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
||||
req.CreatedAt = time.Now().Unix()
|
||||
req.Token = generatePATToken()
|
||||
|
||||
// default expiry is 30 days
|
||||
if req.ExpiresAt == 0 {
|
||||
req.ExpiresAt = time.Now().AddDate(0, 0, 30).Unix()
|
||||
}
|
||||
// max expiry is 1 year
|
||||
if req.ExpiresAt > time.Now().AddDate(1, 0, 0).Unix() {
|
||||
req.ExpiresAt = time.Now().AddDate(1, 0, 0).Unix()
|
||||
}
|
||||
|
||||
zap.S().Debugf("Got PAT request: %+v", req)
|
||||
if apierr := ah.AppDao().CreatePAT(ctx, &req); apierr != nil {
|
||||
var apierr basemodel.BaseApiError
|
||||
if req, apierr = ah.AppDao().CreatePAT(ctx, req); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -480,7 +480,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := telemetry.IgnoredPaths()[path]; !ok {
|
||||
if _, ok := telemetry.EnabledPaths()[path]; ok {
|
||||
userEmail, err := auth.GetEmailFromJwt(r.Context())
|
||||
if err == nil {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail)
|
||||
|
||||
@@ -33,7 +33,7 @@ type ModelDao interface {
|
||||
DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
|
||||
GetDomainByEmail(ctx context.Context, email string) (*model.OrgDomain, basemodel.BaseApiError)
|
||||
|
||||
CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseApiError
|
||||
CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError)
|
||||
GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError)
|
||||
GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError)
|
||||
GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError)
|
||||
|
||||
@@ -3,14 +3,15 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (m *modelDao) CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseApiError {
|
||||
_, err := m.DB().ExecContext(ctx,
|
||||
func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError) {
|
||||
result, err := m.DB().ExecContext(ctx,
|
||||
"INSERT INTO personal_access_tokens (user_id, token, name, created_at, expires_at) VALUES ($1, $2, $3, $4, $5)",
|
||||
p.UserID,
|
||||
p.Token,
|
||||
@@ -19,9 +20,15 @@ func (m *modelDao) CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseAp
|
||||
p.ExpiresAt)
|
||||
if err != nil {
|
||||
zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err))
|
||||
return model.InternalError(fmt.Errorf("PAT insertion failed"))
|
||||
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
|
||||
}
|
||||
return nil
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
zap.S().Errorf("Failed to get last inserted id, err: %v", zap.Error(err))
|
||||
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
|
||||
}
|
||||
p.Id = strconv.Itoa(int(id))
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (m *modelDao) ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) {
|
||||
@@ -90,7 +97,7 @@ func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*basemodel.U
|
||||
u.org_id,
|
||||
u.group_id
|
||||
FROM users u, personal_access_tokens p
|
||||
WHERE u.id = p.user_id and p.token=?;`
|
||||
WHERE u.id = p.user_id and p.token=? and p.expires_at >= strftime('%s', 'now');`
|
||||
|
||||
if err := m.DB().Select(&users, query, token); err != nil {
|
||||
return nil, model.InternalError(fmt.Errorf("failed to fetch user from PAT, err: %v", err))
|
||||
|
||||
@@ -6,5 +6,5 @@ type PAT struct {
|
||||
Token string `json:"token" db:"token"`
|
||||
Name string `json:"name" db:"name"`
|
||||
CreatedAt int64 `json:"createdAt" db:"created_at"`
|
||||
ExpiresAt int64 `json:"expiresAt" db:"expires_at"` // unused as of now
|
||||
ExpiresAt int64 `json:"expiresAt" db:"expires_at"`
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
Name: basemodel.QueryBuilderPanels,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: 5,
|
||||
UsageLimit: 20,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderAlerts,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: 5,
|
||||
UsageLimit: 10,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
|
||||
@@ -86,6 +86,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
'no-plusplus': 'off',
|
||||
'jsx-a11y/label-has-associated-control': [
|
||||
'error',
|
||||
{
|
||||
@@ -109,7 +110,6 @@ module.exports = {
|
||||
// eslint rules need to remove
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{},
|
||||
|
||||
@@ -2,3 +2,19 @@
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && yarn run commitlint --edit $1
|
||||
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
|
||||
color_red="$(tput setaf 1)"
|
||||
bold="$(tput bold)"
|
||||
reset="$(tput sgr0)"
|
||||
|
||||
if [ "$branch" = "main" ]; then
|
||||
echo "${color_red}${bold}You can't commit directly to the main branch${reset}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$branch" = "develop" ]; then
|
||||
echo "${color_red}${bold}You can't commit directly to the develop branch${reset}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -22,7 +22,7 @@ const config: Config.InitialOptions = {
|
||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend)/)',
|
||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios)/)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "6.0.0",
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@dnd-kit/core": "6.1.0",
|
||||
"@dnd-kit/modifiers": "7.0.0",
|
||||
"@dnd-kit/sortable": "8.0.0",
|
||||
"@grafana/data": "^9.5.2",
|
||||
"@mdx-js/loader": "2.3.0",
|
||||
"@mdx-js/react": "2.3.0",
|
||||
@@ -38,7 +41,7 @@
|
||||
"ansi-to-html": "0.7.2",
|
||||
"antd": "5.11.0",
|
||||
"antd-table-saveas-excel": "2.2.1",
|
||||
"axios": "^0.21.0",
|
||||
"axios": "1.6.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^29.6.4",
|
||||
"babel-loader": "9.1.3",
|
||||
@@ -82,11 +85,12 @@
|
||||
"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-markdown": "8.0.7",
|
||||
"react-query": "^3.34.19",
|
||||
"react-query": "3.39.3",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-syntax-highlighter": "15.5.0",
|
||||
|
||||
BIN
frontend/public/Logos/cloudwatch.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
frontend/public/Logos/dotnet.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/public/Logos/fluent-bit.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
frontend/public/Logos/fluentd.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
frontend/public/Logos/heroku.png
Normal file
|
After Width: | Height: | Size: 957 B |
BIN
frontend/public/Logos/http.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
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 |
BIN
frontend/public/Logos/vercel.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
@@ -20,5 +20,10 @@
|
||||
"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",
|
||||
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
|
||||
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||
"your_graph_build_with": "Your graph built with",
|
||||
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
"delete_domain_message": "Are you sure you want to delete this domain?",
|
||||
"delete_domain": "Delete Domain",
|
||||
"add_domain": "Add Domains",
|
||||
"saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
|
||||
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly",
|
||||
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
|
||||
}
|
||||
|
||||
3
frontend/public/locales/en-GB/services.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support."
|
||||
}
|
||||
@@ -17,10 +17,16 @@
|
||||
"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?",
|
||||
"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."
|
||||
"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",
|
||||
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
|
||||
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
|
||||
"your_graph_build_with": "Your graph built with",
|
||||
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
"delete_domain_message": "Are you sure you want to delete this domain?",
|
||||
"delete_domain": "Delete Domain",
|
||||
"add_domain": "Add Domains",
|
||||
"saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
|
||||
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly",
|
||||
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
|
||||
}
|
||||
|
||||
@@ -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,7 +32,7 @@
|
||||
"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",
|
||||
|
||||
3
frontend/public/locales/en/services.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support."
|
||||
}
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -25,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';
|
||||
@@ -41,6 +41,8 @@ function App(): JSX.Element {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const { trackPageView } = useAnalytics();
|
||||
|
||||
const { hostname, pathname } = window.location;
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
@@ -156,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(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AxiosError } from 'axios';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { ErrorResponse } from 'types/api';
|
||||
import { ErrorStatusCode } from 'types/common';
|
||||
|
||||
@@ -10,7 +10,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
|
||||
const statusCode = response.status as ErrorStatusCode;
|
||||
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
const { data } = response;
|
||||
const { data } = response as AxiosResponse;
|
||||
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
|
||||
@@ -3,9 +3,9 @@ import { ApiResponse } from 'types/api';
|
||||
import { Props } from 'types/api/dashboard/get';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const get = (props: Props): Promise<Dashboard> =>
|
||||
const getDashboard = (props: Props): Promise<Dashboard> =>
|
||||
axios
|
||||
.get<ApiResponse<Dashboard>>(`/dashboards/${props.uuid}`)
|
||||
.then((res) => res.data.data);
|
||||
|
||||
export default get;
|
||||
export default getDashboard;
|
||||
|
||||
15
frontend/src/api/dashboard/queryRangeFormat.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ApiResponse } from 'types/api';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { QueryRangePayload } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
interface IQueryRangeFormat {
|
||||
compositeQuery: ICompositeMetricQuery;
|
||||
}
|
||||
|
||||
export const getQueryRangeFormat = (
|
||||
props?: Partial<QueryRangePayload>,
|
||||
): Promise<IQueryRangeFormat> =>
|
||||
axios
|
||||
.post<ApiResponse<IQueryRangeFormat>>('/query_range/format', props)
|
||||
.then((res) => res.data.data);
|
||||
@@ -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;
|
||||
@@ -4,7 +4,7 @@
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import loginApi from 'api/user/login';
|
||||
import afterLogin from 'AppRoutes/utils';
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import store from 'store';
|
||||
@@ -17,14 +17,16 @@ const interceptorsResponse = (
|
||||
): Promise<AxiosResponse<any>> => Promise.resolve(value);
|
||||
|
||||
const interceptorsRequestResponse = (
|
||||
value: AxiosRequestConfig,
|
||||
): AxiosRequestConfig => {
|
||||
value: InternalAxiosRequestConfig,
|
||||
): InternalAxiosRequestConfig => {
|
||||
const token =
|
||||
store.getState().app.user?.accessJwt ||
|
||||
getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) ||
|
||||
'';
|
||||
|
||||
value.headers.Authorization = token ? `Bearer ${token}` : '';
|
||||
if (value && value.headers) {
|
||||
value.headers.Authorization = token ? `Bearer ${token}` : '';
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
@@ -92,8 +94,8 @@ const instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
|
||||
instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
|
||||
|
||||
export const AxiosAlertManagerInstance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${apiAlertManager}`,
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
|
||||
export const getMetricsQueryRange = async (
|
||||
props: QueryRangePayload,
|
||||
signal: AbortSignal,
|
||||
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/query_range', props);
|
||||
const response = await axios.post('/query_range', props, { signal });
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -64,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>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
DeleteViewHandlerProps,
|
||||
@@ -35,6 +36,45 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const omitIdFromQuery = (query: Query | null): any => ({
|
||||
...query,
|
||||
builder: {
|
||||
...query?.builder,
|
||||
queryData: query?.builder.queryData.map((queryData) => {
|
||||
const { id, ...rest } = queryData.aggregateAttribute;
|
||||
const newAggregateAttribute = rest;
|
||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||
const { id, ...rest } = groupByAttribute;
|
||||
return rest;
|
||||
});
|
||||
const newItems = queryData.filters.items.map((item) => {
|
||||
const { id, ...newItem } = item;
|
||||
if (item.key) {
|
||||
const { id, ...rest } = item.key;
|
||||
return {
|
||||
...newItem,
|
||||
key: rest,
|
||||
};
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
return {
|
||||
...queryData,
|
||||
aggregateAttribute: newAggregateAttribute,
|
||||
groupBy: newGroupByAttributes,
|
||||
filters: {
|
||||
...queryData.filters,
|
||||
items: newItems,
|
||||
},
|
||||
limit: queryData.limit ? queryData.limit : 0,
|
||||
offset: queryData.offset ? queryData.offset : 0,
|
||||
pageSize: queryData.pageSize ? queryData.pageSize : 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const isQueryUpdatedInView = ({
|
||||
viewKey,
|
||||
data,
|
||||
@@ -48,43 +88,7 @@ export const isQueryUpdatedInView = ({
|
||||
const { query, panelType } = currentViewDetails;
|
||||
|
||||
// Omitting id from aggregateAttribute and groupBy
|
||||
const updatedCurrentQuery = {
|
||||
...stagedQuery,
|
||||
builder: {
|
||||
...stagedQuery?.builder,
|
||||
queryData: stagedQuery?.builder.queryData.map((queryData) => {
|
||||
const { id, ...rest } = queryData.aggregateAttribute;
|
||||
const newAggregateAttribute = rest;
|
||||
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
|
||||
const { id, ...rest } = groupByAttribute;
|
||||
return rest;
|
||||
});
|
||||
const newItems = queryData.filters.items.map((item) => {
|
||||
const { id, ...newItem } = item;
|
||||
if (item.key) {
|
||||
const { id, ...rest } = item.key;
|
||||
return {
|
||||
...newItem,
|
||||
key: rest,
|
||||
};
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
return {
|
||||
...queryData,
|
||||
aggregateAttribute: newAggregateAttribute,
|
||||
groupBy: newGroupByAttributes,
|
||||
filters: {
|
||||
...queryData.filters,
|
||||
items: newItems,
|
||||
},
|
||||
limit: queryData.limit ? queryData.limit : 0,
|
||||
offset: queryData.offset ? queryData.offset : 0,
|
||||
pageSize: queryData.pageSize ? queryData.pageSize : 0,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
|
||||
|
||||
return (
|
||||
panelType !== currentPanelType ||
|
||||
@@ -153,7 +157,7 @@ export const deleteViewHandler = ({
|
||||
if (viewId === viewKey) {
|
||||
redirectWithQueryBuilderData(
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
initialQueriesMap[sourcePage],
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
sourcePage,
|
||||
),
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, Divider, Row, Typography } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
@@ -95,11 +94,15 @@ function LogSelectedField({
|
||||
type ListLogViewProps = {
|
||||
logData: ILog;
|
||||
selectedFields: IField[];
|
||||
onSetActiveLog: (log: ILog) => void;
|
||||
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
|
||||
};
|
||||
|
||||
function ListLogView({
|
||||
logData,
|
||||
selectedFields,
|
||||
onSetActiveLog,
|
||||
onAddToQuery,
|
||||
}: ListLogViewProps): JSX.Element {
|
||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||
|
||||
@@ -113,12 +116,6 @@ function ListLogView({
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
} = useActiveLog();
|
||||
const {
|
||||
activeLog,
|
||||
onSetActiveLog,
|
||||
onClearActiveLog,
|
||||
onAddToQuery,
|
||||
} = useActiveLog();
|
||||
|
||||
const handleDetailedView = useCallback(() => {
|
||||
onSetActiveLog(logData);
|
||||
@@ -223,12 +220,6 @@ function ListLogView({
|
||||
onClose={handleClearActiveContextLog}
|
||||
/>
|
||||
)}
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -27,6 +27,7 @@ function DynamicColumnTable({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setColumnsData(columns);
|
||||
const visibleColumns = getVisibleColumns({
|
||||
tablesource,
|
||||
columnsData: columns,
|
||||
@@ -42,7 +43,7 @@ function DynamicColumnTable({
|
||||
: undefined,
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [columns]);
|
||||
|
||||
const onToggleHandler = (index: number) => (
|
||||
checked: boolean,
|
||||
|
||||
@@ -13,3 +13,11 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.uplot-no-data {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './uplot.scss';
|
||||
import './Uplot.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { LineChart } from 'lucide-react';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import UPlot from 'uplot';
|
||||
|
||||
import { dataMatch, optionsUpdateState } from './utils';
|
||||
@@ -55,6 +58,13 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
chart.destroy();
|
||||
chartRef.current = null;
|
||||
}
|
||||
|
||||
// remove chart tooltip on cleanup
|
||||
const overlay = document.getElementById('overlay');
|
||||
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}, []);
|
||||
|
||||
const create = useCallback(() => {
|
||||
@@ -118,14 +128,26 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
|
||||
}
|
||||
}, [data, resetScales, create]);
|
||||
|
||||
if (data && data[0] && data[0]?.length === 0) {
|
||||
return (
|
||||
<div className="uplot-no-data not-found">
|
||||
<LineChart size={48} strokeWidth={0.5} />
|
||||
|
||||
<Typography>No Data</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const optionsUpdateState = (
|
||||
if (lhsHeight !== rhsHeight || lhsWidth !== rhsWidth) {
|
||||
state = 'update';
|
||||
}
|
||||
if (Object.keys(lhs).length !== Object.keys(rhs).length) {
|
||||
if (Object.keys(lhs)?.length !== Object.keys(rhs)?.length) {
|
||||
return 'create';
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@@ -31,12 +31,12 @@ export const dataMatch = (
|
||||
lhs: uPlot.AlignedData,
|
||||
rhs: uPlot.AlignedData,
|
||||
): boolean => {
|
||||
if (lhs.length !== rhs.length) {
|
||||
if (lhs?.length !== rhs?.length) {
|
||||
return false;
|
||||
}
|
||||
return lhs.every((lhsOneSeries, seriesIdx) => {
|
||||
const rhsOneSeries = rhs[seriesIdx];
|
||||
if (lhsOneSeries.length !== rhsOneSeries.length) {
|
||||
if (lhsOneSeries?.length !== rhsOneSeries?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
2
frontend/src/constants/global.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
const MAX_RPS_LIMIT = 100;
|
||||
export { MAX_RPS_LIMIT };
|
||||
@@ -26,4 +26,6 @@ export enum QueryParams {
|
||||
linesPerRow = 'linesPerRow',
|
||||
viewName = 'viewName',
|
||||
viewKey = 'viewKey',
|
||||
expandedWidgetId = 'expandedWidgetId',
|
||||
pagination = 'pagination',
|
||||
}
|
||||
|
||||
@@ -30,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',
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -109,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();
|
||||
@@ -301,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,
|
||||
@@ -432,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>
|
||||
|
||||
@@ -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,14 +2,15 @@ import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
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 { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -17,9 +18,10 @@ 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 { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||
import { covertIntoDataFormats } from './utils';
|
||||
import { getThresholdLabel } from './utils';
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
@@ -31,6 +33,7 @@ export interface ChartPreviewProps {
|
||||
alertDef?: AlertDef;
|
||||
userQueryKey?: string;
|
||||
allowSelectedIntervalForStepGen?: boolean;
|
||||
yAxisUnit: string;
|
||||
}
|
||||
|
||||
function ChartPreview({
|
||||
@@ -43,18 +46,17 @@ function ChartPreview({
|
||||
userQueryKey,
|
||||
allowSelectedIntervalForStepGen = false,
|
||||
alertDef,
|
||||
yAxisUnit,
|
||||
}: ChartPreviewProps): JSX.Element | null {
|
||||
const { t } = useTranslation('alerts');
|
||||
const threshold = alertDef?.condition.target || 0;
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
const thresholdValue = covertIntoDataFormats({
|
||||
value: threshold,
|
||||
sourceUnit: alertDef?.condition.targetUnit,
|
||||
targetUnit: query?.unit,
|
||||
});
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const canQuery = useMemo((): boolean => {
|
||||
if (!query || query == null) {
|
||||
@@ -104,19 +106,31 @@ function ChartPreview({
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
|
||||
|
||||
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,
|
||||
yAxisUnit,
|
||||
apiResponse: queryResponse?.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
thresholds: [
|
||||
{
|
||||
@@ -124,20 +138,30 @@ function ChartPreview({
|
||||
keyIndex: 0,
|
||||
moveThreshold: (): void => {},
|
||||
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
|
||||
thresholdValue,
|
||||
thresholdValue: threshold,
|
||||
thresholdLabel: `${t(
|
||||
'preview_chart_threshold_label',
|
||||
)} (y=${thresholdValue} ${query?.unit || ''})`,
|
||||
)} (y=${getThresholdLabel(
|
||||
optionName,
|
||||
threshold,
|
||||
alertDef?.condition.targetUnit,
|
||||
yAxisUnit,
|
||||
)})`,
|
||||
thresholdUnit: alertDef?.condition.targetUnit,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[
|
||||
query?.unit,
|
||||
yAxisUnit,
|
||||
queryResponse?.data?.payload,
|
||||
containerDimensions,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
isDarkMode,
|
||||
threshold,
|
||||
t,
|
||||
thresholdValue,
|
||||
optionName,
|
||||
alertDef?.condition.targetUnit,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -162,7 +186,7 @@ function ChartPreview({
|
||||
name={name || 'Chart Preview'}
|
||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
||||
query={query || initialQueriesMap.metrics}
|
||||
yAxisUnit={query?.unit}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -51,6 +51,33 @@ 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
|
||||
) {
|
||||
if (unit === MiscellaneousFormats.Percent) {
|
||||
return `${value}%`;
|
||||
}
|
||||
return `${value * 100}%`;
|
||||
}
|
||||
if (
|
||||
unit === MiscellaneousFormats.Percent ||
|
||||
yAxisUnit === MiscellaneousFormats.Percent
|
||||
) {
|
||||
if (unit === MiscellaneousFormats.PercentUnit) {
|
||||
return `${value * 100}%`;
|
||||
}
|
||||
return `${value}%`;
|
||||
}
|
||||
return `${value} ${optionName}`;
|
||||
};
|
||||
|
||||
interface IUnit {
|
||||
value: number;
|
||||
sourceUnit?: string;
|
||||
|
||||
@@ -82,6 +82,7 @@ function FormAlertRules({
|
||||
|
||||
// alertDef holds the form values to be posted
|
||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
|
||||
|
||||
// initQuery contains initial query when component was mounted
|
||||
const initQuery = useMemo(() => initialValue.condition.compositeQuery, [
|
||||
@@ -400,6 +401,7 @@ function FormAlertRules({
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
alertDef={alertDef}
|
||||
yAxisUnit={yAxisUnit || ''}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -415,6 +417,7 @@ function FormAlertRules({
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
yAxisUnit={yAxisUnit || ''}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -427,7 +430,8 @@ function FormAlertRules({
|
||||
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||
alertType !== AlertTypes.METRICS_BASED_ALERT;
|
||||
|
||||
const onUnitChangeHandler = (): void => {
|
||||
const onUnitChangeHandler = (value: string): void => {
|
||||
setYAxisUnit(value);
|
||||
// reset target unit
|
||||
setAlertDef((def) => ({
|
||||
...def,
|
||||
@@ -457,7 +461,10 @@ function FormAlertRules({
|
||||
renderPromAndChQueryChartPreview()}
|
||||
|
||||
<StepContainer>
|
||||
<BuilderUnitsFilter onChange={onUnitChangeHandler} />
|
||||
<BuilderUnitsFilter
|
||||
onChange={onUnitChangeHandler}
|
||||
yAxisUnit={yAxisUnit}
|
||||
/>
|
||||
</StepContainer>
|
||||
|
||||
<QuerySection
|
||||
|
||||
16
frontend/src/container/GantChart/GantChart.styles.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.span-container {
|
||||
.spanDetails {
|
||||
position: absolute;
|
||||
height: 50px;
|
||||
padding: 8px;
|
||||
min-width: 150px;
|
||||
background: lightcyan;
|
||||
color: black;
|
||||
bottom: 24px;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
96
frontend/src/container/GantChart/Span/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import '../GantChart.styles.scss';
|
||||
|
||||
import { Popover, Typography } from 'antd';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useEffect } from 'react';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
|
||||
|
||||
interface SpanLengthProps {
|
||||
globalStart: number;
|
||||
startTime: number;
|
||||
name: string;
|
||||
width: string;
|
||||
leftOffset: string;
|
||||
bgColor: string;
|
||||
inMsCount: number;
|
||||
}
|
||||
|
||||
function Span(props: SpanLengthProps): JSX.Element {
|
||||
const {
|
||||
width,
|
||||
leftOffset,
|
||||
bgColor,
|
||||
inMsCount,
|
||||
startTime,
|
||||
name,
|
||||
globalStart,
|
||||
} = props;
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.scrollTop = document.documentElement.clientHeight;
|
||||
document.documentElement.scrollLeft = document.documentElement.clientWidth;
|
||||
}, []);
|
||||
|
||||
const getContent = (): JSX.Element => {
|
||||
const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A');
|
||||
const startTimeInMs = startTime - globalStart;
|
||||
return (
|
||||
<div>
|
||||
<Typography.Text style={{ marginBottom: '8px' }}>
|
||||
{' '}
|
||||
Duration : {inMsCount}
|
||||
</Typography.Text>
|
||||
<br />
|
||||
<Typography.Text style={{ marginBottom: '8px' }}>
|
||||
Start Time: {startTimeInMs}ms [{timeStamp}]{' '}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SpanWrapper className="span-container">
|
||||
<SpanLine
|
||||
className="spanLine"
|
||||
isDarkMode={isDarkMode}
|
||||
bgColor={bgColor}
|
||||
leftOffset={leftOffset}
|
||||
width={width}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
style={{
|
||||
left: `${leftOffset}%`,
|
||||
}}
|
||||
title={name}
|
||||
content={getContent()}
|
||||
trigger="hover"
|
||||
placement="left"
|
||||
autoAdjustOverflow
|
||||
>
|
||||
<SpanBorder
|
||||
className="spanTrack"
|
||||
isDarkMode={isDarkMode}
|
||||
bgColor={bgColor}
|
||||
leftOffset={leftOffset}
|
||||
width={width}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
|
||||
time,
|
||||
2,
|
||||
)} ${timeUnitName}`}</SpanText>
|
||||
</SpanWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Span;
|
||||
@@ -1,40 +0,0 @@
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
|
||||
|
||||
interface SpanLengthProps {
|
||||
width: string;
|
||||
leftOffset: string;
|
||||
bgColor: string;
|
||||
inMsCount: number;
|
||||
}
|
||||
|
||||
function SpanLength(props: SpanLengthProps): JSX.Element {
|
||||
const { width, leftOffset, bgColor, inMsCount } = props;
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
||||
return (
|
||||
<SpanWrapper>
|
||||
<SpanLine
|
||||
isDarkMode={isDarkMode}
|
||||
bgColor={bgColor}
|
||||
leftOffset={leftOffset}
|
||||
width={width}
|
||||
/>
|
||||
<SpanBorder
|
||||
isDarkMode={isDarkMode}
|
||||
bgColor={bgColor}
|
||||
leftOffset={leftOffset}
|
||||
width={width}
|
||||
/>
|
||||
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
|
||||
time,
|
||||
2,
|
||||
)} ${timeUnitName}`}</SpanText>
|
||||
</SpanWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpanLength;
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
import { ITraceMetaData } from '..';
|
||||
import SpanLength from '../SpanLength';
|
||||
import Span from '../Span';
|
||||
import SpanName from '../SpanName';
|
||||
import { getMetaDataFromSpanTree, getTopLeftFromBody } from '../utils';
|
||||
import {
|
||||
@@ -158,7 +158,7 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
isDarkMode={isDarkMode}
|
||||
onClick={onClickTreeExpansion}
|
||||
>
|
||||
<Typography>{totalSpans}</Typography>
|
||||
<Typography style={{ wordBreak: 'normal' }}>{totalSpans}</Typography>
|
||||
<CaretContainer>{icon}</CaretContainer>
|
||||
</CardComponent>
|
||||
)}
|
||||
@@ -169,7 +169,10 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
</StyledRow>
|
||||
</StyledCol>
|
||||
<Col flex="1">
|
||||
<SpanLength
|
||||
<Span
|
||||
globalStart={globalStart}
|
||||
startTime={startTime}
|
||||
name={name}
|
||||
leftOffset={nodeLeftOffset.toString()}
|
||||
width={width.toString()}
|
||||
bgColor={serviceColour}
|
||||
|
||||
@@ -41,7 +41,11 @@ function GanttChart(props: GanttChartProps): JSX.Element {
|
||||
onClick={handleCollapse}
|
||||
title={isExpandAll ? 'Collapse All' : 'Expand All'}
|
||||
>
|
||||
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
|
||||
{isExpandAll ? (
|
||||
<MinusSquareOutlined style={{ fontSize: '16px', color: '#08c' }} />
|
||||
) : (
|
||||
<PlusSquareOutlined style={{ fontSize: '16px', color: '#08c' }} />
|
||||
)}
|
||||
</CollapseButton>
|
||||
<CardWrapper>
|
||||
<Trace
|
||||
|
||||
@@ -11,6 +11,7 @@ export const Wrapper = styled.ul`
|
||||
border-left: 1px solid #434343;
|
||||
padding-left: 1rem;
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
ul li {
|
||||
@@ -44,6 +45,4 @@ export const CardContainer = styled.li`
|
||||
export const CollapseButton = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-size: 1.2rem;
|
||||
`;
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -29,6 +30,7 @@ function GraphManager({
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
|
||||
const checkBoxOnChangeHandler = useCallback(
|
||||
(e: CheckboxChangeEvent, index: number): void => {
|
||||
@@ -66,6 +68,7 @@ function GraphManager({
|
||||
graphVisibilityState: graphsVisibilityStates,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
isGraphDisabled: isDashboardLocked,
|
||||
});
|
||||
|
||||
const filterHandler = useCallback(
|
||||
|
||||
@@ -9,6 +9,7 @@ function CustomCheckBox({
|
||||
index,
|
||||
graphVisibilityState = [],
|
||||
checkBoxOnChangeHandler,
|
||||
disabled = false,
|
||||
}: CheckBoxProps): JSX.Element {
|
||||
const onChangeHandler = (e: CheckboxChangeEvent): void => {
|
||||
checkBoxOnChangeHandler(e, index);
|
||||
@@ -28,7 +29,11 @@ function CustomCheckBox({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Checkbox onChange={onChangeHandler} checked={isChecked} />
|
||||
<Checkbox
|
||||
onChange={onChangeHandler}
|
||||
checked={isChecked}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import Label from './Label';
|
||||
|
||||
export const getLabel = (
|
||||
labelClickedHandler: (labelIndex: number) => void,
|
||||
disabled?: boolean,
|
||||
): ColumnType<DataSetProps> => ({
|
||||
render: (label: string, record): JSX.Element => (
|
||||
<Label
|
||||
label={label}
|
||||
labelIndex={record.index}
|
||||
labelClickedHandler={labelClickedHandler}
|
||||
disabled={disabled}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ export const getGraphManagerTableColumns = ({
|
||||
graphVisibilityState,
|
||||
labelClickedHandler,
|
||||
yAxisUnit,
|
||||
isGraphDisabled,
|
||||
}: GetGraphManagerTableColumnsProps): ColumnType<DataSetProps>[] => [
|
||||
{
|
||||
title: '',
|
||||
@@ -25,6 +26,7 @@ export const getGraphManagerTableColumns = ({
|
||||
index={record.index}
|
||||
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
|
||||
graphVisibilityState={graphVisibilityState}
|
||||
disabled={isGraphDisabled}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -33,7 +35,7 @@ export const getGraphManagerTableColumns = ({
|
||||
width: 300,
|
||||
dataIndex: ColumnsKeyAndDataIndex.Label,
|
||||
key: ColumnsKeyAndDataIndex.Label,
|
||||
...getLabel(labelClickedHandler),
|
||||
...getLabel(labelClickedHandler, isGraphDisabled),
|
||||
},
|
||||
{
|
||||
title: getGraphManagerTableHeaderTitle(
|
||||
@@ -79,4 +81,5 @@ interface GetGraphManagerTableColumnsProps {
|
||||
labelClickedHandler: (labelIndex: number) => void;
|
||||
graphVisibilityState: boolean[];
|
||||
yAxisUnit?: string;
|
||||
isGraphDisabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import { LabelContainer } from '../styles';
|
||||
@@ -8,6 +9,7 @@ function Label({
|
||||
labelClickedHandler,
|
||||
labelIndex,
|
||||
label,
|
||||
disabled = false,
|
||||
}: LabelProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -19,9 +21,12 @@ function Label({
|
||||
<LabelContainer
|
||||
isDarkMode={isDarkMode}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
{getAbbreviatedLabel(label)}
|
||||
<Tooltip title={label} placement="topLeft">
|
||||
{getAbbreviatedLabel(label)}
|
||||
</Tooltip>
|
||||
</LabelContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
height: calc(100% - 65px);
|
||||
}
|
||||
|
||||
.graph-manager-container {
|
||||
height: calc(40% - 40px);
|
||||
|
||||
|
||||
@@ -15,14 +15,15 @@ import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||
import GraphManager from './GraphManager';
|
||||
@@ -52,7 +53,7 @@ function FullView({
|
||||
|
||||
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
|
||||
const getSelectedTime = useCallback(
|
||||
() =>
|
||||
@@ -92,6 +93,21 @@ function FullView({
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(response);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, response]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!response.isFetching && fullViewRef.current) {
|
||||
const width = fullViewRef.current?.clientWidth
|
||||
@@ -114,6 +130,8 @@ function FullView({
|
||||
graphsVisibilityStates,
|
||||
setGraphsVisibilityStates,
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
});
|
||||
|
||||
setChartOptions(newChartOptions);
|
||||
@@ -155,7 +173,12 @@ function FullView({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="graph-container" ref={fullViewRef}>
|
||||
<div
|
||||
className={
|
||||
isDashboardLocked ? 'graph-container disabled' : 'graph-container'
|
||||
}
|
||||
ref={fullViewRef}
|
||||
>
|
||||
{chartOptions && (
|
||||
<GraphContainer
|
||||
style={{ height: '90%' }}
|
||||
@@ -178,7 +201,7 @@ function FullView({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canModifyChart && chartOptions && (
|
||||
{canModifyChart && chartOptions && !isDashboardLocked && (
|
||||
<GraphManager
|
||||
data={chartData}
|
||||
name={name}
|
||||
|
||||
@@ -31,9 +31,12 @@ export const GraphContainer = styled.div<GraphContainerProps>`
|
||||
isGraphLegendToggleAvailable ? '50%' : '100%'};
|
||||
`;
|
||||
|
||||
export const LabelContainer = styled.button<{ isDarkMode?: boolean }>`
|
||||
export const LabelContainer = styled.button<{
|
||||
isDarkMode?: boolean;
|
||||
disabled?: boolean;
|
||||
}>`
|
||||
max-width: 18.75rem;
|
||||
cursor: pointer;
|
||||
cursor: ${(props): string => (props.disabled ? 'no-drop' : 'pointer')};
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: ${(props): string =>
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface LabelProps {
|
||||
labelClickedHandler: (labelIndex: number) => void;
|
||||
labelIndex: number;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface FullViewProps {
|
||||
@@ -74,6 +75,7 @@ export interface CheckBoxProps {
|
||||
index: number;
|
||||
graphVisibilityState: boolean[];
|
||||
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SaveLegendEntriesToLocalStoreProps {
|
||||
|
||||
@@ -25,19 +25,26 @@ export const getDefaultTableDataSet = (
|
||||
data: uPlot.AlignedData,
|
||||
): ExtendedChartDataset[] =>
|
||||
options.series.map(
|
||||
(item: uPlot.Series, index: number): ExtendedChartDataset => ({
|
||||
...item,
|
||||
index,
|
||||
show: true,
|
||||
sum: convertToTwoDecimalsOrZero(
|
||||
(data[index] as number[]).reduce((a, b) => a + b, 0),
|
||||
),
|
||||
avg: convertToTwoDecimalsOrZero(
|
||||
(data[index] as number[]).reduce((a, b) => a + b, 0) / data[index].length,
|
||||
),
|
||||
max: convertToTwoDecimalsOrZero(Math.max(...(data[index] as number[]))),
|
||||
min: convertToTwoDecimalsOrZero(Math.min(...(data[index] as number[]))),
|
||||
}),
|
||||
(item: uPlot.Series, index: number): ExtendedChartDataset => {
|
||||
let arr: number[];
|
||||
if (data[index]) {
|
||||
arr = data[index] as number[];
|
||||
} else {
|
||||
arr = [];
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
index,
|
||||
show: true,
|
||||
sum: convertToTwoDecimalsOrZero(arr.reduce((a, b) => a + b, 0) || 0),
|
||||
avg: convertToTwoDecimalsOrZero(
|
||||
(arr.reduce((a, b) => a + b, 0) || 0) / (arr.length || 1),
|
||||
),
|
||||
max: convertToTwoDecimalsOrZero(Math.max(...arr)),
|
||||
min: convertToTwoDecimalsOrZero(Math.min(...arr)),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const getAbbreviatedLabel = (label: string): string => {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import '../GridCardLayout.styles.scss';
|
||||
|
||||
import { Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
@@ -43,10 +48,13 @@ function WidgetGraphComponent({
|
||||
onDragSelect,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [modal, setModal] = useState<boolean>(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { notifications } = useNotifications();
|
||||
const { pathname } = useLocation();
|
||||
const { pathname, search } = useLocation();
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id;
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
@@ -175,7 +183,24 @@ function WidgetGraphComponent({
|
||||
};
|
||||
|
||||
const handleOnView = (): void => {
|
||||
onToggleModal(setModal);
|
||||
const queryParams = {
|
||||
[QueryParams.expandedWidgetId]: widget.id,
|
||||
};
|
||||
const updatedSearch = createQueryParams(queryParams);
|
||||
const existingSearch = new URLSearchParams(search);
|
||||
const isExpandedWidgetIdPresent = existingSearch.has(
|
||||
QueryParams.expandedWidgetId,
|
||||
);
|
||||
if (isExpandedWidgetIdPresent) {
|
||||
existingSearch.delete(QueryParams.expandedWidgetId);
|
||||
}
|
||||
const separator = existingSearch.toString() ? '&' : '';
|
||||
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
|
||||
|
||||
history.push({
|
||||
pathname,
|
||||
search: newSearch,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnDelete = (): void => {
|
||||
@@ -187,7 +212,13 @@ function WidgetGraphComponent({
|
||||
};
|
||||
|
||||
const onToggleModelHandler = (): void => {
|
||||
onToggleModal(setModal);
|
||||
const existingSearchParams = new URLSearchParams(search);
|
||||
existingSearchParams.delete(QueryParams.expandedWidgetId);
|
||||
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
|
||||
history.push({
|
||||
pathname,
|
||||
search: createQueryParams(updatedQueryParams),
|
||||
});
|
||||
};
|
||||
|
||||
if (queryResponse.isLoading || queryResponse.status === 'idle') {
|
||||
@@ -236,7 +267,7 @@ function WidgetGraphComponent({
|
||||
title={widget?.title || 'View'}
|
||||
footer={[]}
|
||||
centered
|
||||
open={modal}
|
||||
open={isFullViewOpen}
|
||||
onCancel={onToggleModelHandler}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
@@ -270,7 +301,10 @@ function WidgetGraphComponent({
|
||||
</div>
|
||||
{queryResponse.isLoading && <Skeleton />}
|
||||
{queryResponse.isSuccess && (
|
||||
<div style={{ height: '90%' }} ref={graphRef}>
|
||||
<div
|
||||
className={cx('widget-graph-container', widget.panelTypes)}
|
||||
ref={graphRef}
|
||||
>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={data}
|
||||
|
||||
@@ -5,15 +5,17 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartData';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getChartData';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import _noop from 'lodash-es/noop';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import EmptyWidget from '../EmptyWidget';
|
||||
import { MenuItemKeys } from '../WidgetHeader/contants';
|
||||
@@ -28,9 +30,13 @@ function GridCardGraph({
|
||||
isQueryEnabled,
|
||||
threshold,
|
||||
variables,
|
||||
fillSpans = false,
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
@@ -48,16 +54,27 @@ function GridCardGraph({
|
||||
|
||||
const isVisible = useIntersectionObserver(graphRef, undefined, true);
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
useEffect(() => {
|
||||
if (toScrollWidgetId === widget.id) {
|
||||
graphRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
graphRef.current?.focus();
|
||||
setToScrollWidgetId('');
|
||||
}
|
||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
|
||||
const isEmptyWidget =
|
||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
selectedTime: widget?.timePreferance,
|
||||
@@ -89,7 +106,14 @@ function GridCardGraph({
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
|
||||
|
||||
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -109,6 +133,8 @@ function GridCardGraph({
|
||||
yAxisUnit: widget?.yAxisUnit,
|
||||
onClickHandler,
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
}),
|
||||
[
|
||||
widget?.id,
|
||||
@@ -119,6 +145,8 @@ function GridCardGraph({
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface GridCardGraphProps {
|
||||
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
||||
isQueryEnabled: boolean;
|
||||
variables?: Dashboard['data']['variables'];
|
||||
fillSpans?: boolean;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.fullscreen-grid-container {
|
||||
overflow: auto;
|
||||
|
||||
.react-grid-layout {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-graph-container {
|
||||
height: 100%;
|
||||
|
||||
&.graph {
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import './GridCardLayout.styles.scss';
|
||||
|
||||
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { FullscreenIcon } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -34,6 +39,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
isDashboardLocked,
|
||||
} = useDashboard();
|
||||
const { data } = selectedDashboard || {};
|
||||
const handle = useFullScreenHandle();
|
||||
|
||||
const { widgets, variables } = data || {};
|
||||
|
||||
@@ -104,67 +110,81 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isDashboardLocked && (
|
||||
<ButtonContainer>
|
||||
{saveLayoutPermission && (
|
||||
<Button
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveFilled />}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
{t('dashboard:save_layout')}
|
||||
</Button>
|
||||
)}
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
onClick={handle.enter}
|
||||
icon={<FullscreenIcon size={16} />}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
{t('dashboard:full_view')}
|
||||
</Button>
|
||||
|
||||
{addPanelPermission && (
|
||||
<Button onClick={onAddPanelHandler} icon={<PlusOutlined />}>
|
||||
{t('dashboard:add_panel')}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
)}
|
||||
{!isDashboardLocked && saveLayoutPermission && (
|
||||
<Button
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveFilled />}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
{t('dashboard:save_layout')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ReactGridLayout
|
||||
cols={12}
|
||||
rowHeight={100}
|
||||
autoSize
|
||||
width={100}
|
||||
useCSSTransforms
|
||||
isDraggable={!isDashboardLocked && addPanelPermission}
|
||||
isDroppable={!isDashboardLocked && addPanelPermission}
|
||||
isResizable={!isDashboardLocked && addPanelPermission}
|
||||
allowOverlap={false}
|
||||
onLayoutChange={setLayouts}
|
||||
draggableHandle=".drag-handle"
|
||||
layout={layouts}
|
||||
>
|
||||
{layouts.map((layout) => {
|
||||
const { i: id } = layout;
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
onClick={onAddPanelHandler}
|
||||
icon={<PlusOutlined />}
|
||||
data-testid="add-panel"
|
||||
>
|
||||
{t('dashboard:add_panel')}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||
isDarkMode={isDarkMode}
|
||||
key={id}
|
||||
data-grid={layout}
|
||||
>
|
||||
<Card
|
||||
className="grid-item"
|
||||
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
|
||||
<FullScreen handle={handle} className="fullscreen-grid-container">
|
||||
<ReactGridLayout
|
||||
cols={12}
|
||||
rowHeight={100}
|
||||
autoSize
|
||||
width={100}
|
||||
useCSSTransforms
|
||||
isDraggable={!isDashboardLocked && addPanelPermission}
|
||||
isDroppable={!isDashboardLocked && addPanelPermission}
|
||||
isResizable={!isDashboardLocked && addPanelPermission}
|
||||
allowOverlap={false}
|
||||
onLayoutChange={setLayouts}
|
||||
draggableHandle=".drag-handle"
|
||||
layout={layouts}
|
||||
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
|
||||
>
|
||||
{layouts.map((layout) => {
|
||||
const { i: id } = layout;
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||
isDarkMode={isDarkMode}
|
||||
key={id}
|
||||
data-grid={JSON.stringify(currentWidget)}
|
||||
>
|
||||
<GridCard
|
||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
||||
name={currentWidget?.id || ''}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
);
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
<Card
|
||||
className="grid-item"
|
||||
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
|
||||
>
|
||||
<GridCard
|
||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
||||
name={currentWidget?.id || ''}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
);
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
</FullScreen>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
.widget-header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.widget-header-title {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.widget-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.widget-header-more-options {
|
||||
visibility: hidden;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
font: 14px;
|
||||
font-weight: 600;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.widget-header-hover {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.widget-api-actions {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
import './WidgetHeader.styles.scss';
|
||||
|
||||
import {
|
||||
AlertOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
EditFilled,
|
||||
ExclamationCircleOutlined,
|
||||
FullscreenOutlined,
|
||||
MoreOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import history from 'lib/history';
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { ReactNode, useCallback, useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -23,23 +25,9 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import {
|
||||
errorTooltipPosition,
|
||||
overlayStyles,
|
||||
spinnerStyles,
|
||||
tooltipStyles,
|
||||
WARNING_MESSAGE,
|
||||
} from './config';
|
||||
import { errorTooltipPosition, WARNING_MESSAGE } from './config';
|
||||
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
|
||||
import {
|
||||
ArrowContainer,
|
||||
HeaderContainer,
|
||||
HeaderContentContainer,
|
||||
ThesholdContainer,
|
||||
WidgetHeaderContainer,
|
||||
} from './styles';
|
||||
import { MenuItem } from './types';
|
||||
import { generateMenuList, isTWidgetOptions } from './utils';
|
||||
|
||||
@@ -72,9 +60,6 @@ function WidgetHeader({
|
||||
headerMenuList,
|
||||
isWarning,
|
||||
}: IWidgetHeaderProps): JSX.Element | null {
|
||||
const [localHover, setLocalHover] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const onEditHandler = useCallback((): void => {
|
||||
const widgetId = widget.id;
|
||||
history.push(
|
||||
@@ -86,13 +71,7 @@ function WidgetHeader({
|
||||
);
|
||||
}, [widget.id, widget.panelTypes, widget.query]);
|
||||
|
||||
const onCreateAlertsHandler = useCallback(() => {
|
||||
history.push(
|
||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||
JSON.stringify(widget.query),
|
||||
)}`,
|
||||
);
|
||||
}, [widget]);
|
||||
const onCreateAlertsHandler = useCreateAlerts(widget);
|
||||
|
||||
const keyMethodMapping = useMemo(
|
||||
() => ({
|
||||
@@ -112,7 +91,6 @@ function WidgetHeader({
|
||||
|
||||
if (functionToCall) {
|
||||
functionToCall();
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -169,10 +147,6 @@ function WidgetHeader({
|
||||
|
||||
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
||||
|
||||
const onClickHandler = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: updatedMenuList,
|
||||
@@ -186,49 +160,47 @@ function WidgetHeader({
|
||||
}
|
||||
|
||||
return (
|
||||
<WidgetHeaderContainer>
|
||||
<Dropdown
|
||||
getPopupContainer={popupContainer}
|
||||
destroyPopupOnHide
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
menu={menu}
|
||||
trigger={['click']}
|
||||
overlayStyle={overlayStyles}
|
||||
<div className="widget-header-container">
|
||||
<Typography.Text
|
||||
ellipsis
|
||||
data-testid={title}
|
||||
className="widget-header-title"
|
||||
>
|
||||
<HeaderContainer
|
||||
onMouseOver={(): void => setLocalHover(true)}
|
||||
onMouseOut={(): void => setLocalHover(false)}
|
||||
hover={localHover}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
<HeaderContentContainer>
|
||||
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<ArrowContainer hover={parentHover}>
|
||||
<DownOutlined />
|
||||
</ArrowContainer>
|
||||
</HeaderContentContainer>
|
||||
</HeaderContainer>
|
||||
</Dropdown>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<div className="widget-header-actions">
|
||||
<div className="widget-api-actions">{threshold}</div>
|
||||
{queryResponse.isFetching && !queryResponse.isError && (
|
||||
<Spinner style={{ paddingRight: '0.25rem' }} />
|
||||
)}
|
||||
{queryResponse.isError && (
|
||||
<Tooltip
|
||||
title={errorMessage}
|
||||
placement={errorTooltipPosition}
|
||||
className="widget-api-actions"
|
||||
>
|
||||
<ExclamationCircleOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<ThesholdContainer>{threshold}</ThesholdContainer>
|
||||
{queryResponse.isFetching && !queryResponse.isError && (
|
||||
<Spinner height="5vh" style={spinnerStyles} />
|
||||
)}
|
||||
{queryResponse.isError && (
|
||||
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
|
||||
<ExclamationCircleOutlined style={tooltipStyles} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isWarning && (
|
||||
<Tooltip title={WARNING_MESSAGE} placement={errorTooltipPosition}>
|
||||
<WarningOutlined style={tooltipStyles} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</WidgetHeaderContainer>
|
||||
{isWarning && (
|
||||
<Tooltip
|
||||
title={WARNING_MESSAGE}
|
||||
placement={errorTooltipPosition}
|
||||
className="widget-api-actions"
|
||||
>
|
||||
<WarningOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
|
||||
<MoreOutlined
|
||||
className={`widget-header-more-options ${
|
||||
parentHover ? 'widget-header-hover' : ''
|
||||
}`}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,6 @@ export const WidgetHeaderContainer = styled.div`
|
||||
|
||||
export const ArrowContainer = styled.span<{ hover: boolean }>`
|
||||
visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')};
|
||||
position: absolute;
|
||||
right: -1rem;
|
||||
`;
|
||||
|
||||
export const Typography = styled(TypographyComponent)`
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { StyledCSS } from 'container/GantChart/Trace/styles';
|
||||
import RGL, { WidthProvider } from 'react-grid-layout';
|
||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const ReactGridLayoutComponent = WidthProvider(RGL);
|
||||
|
||||
@@ -17,14 +17,8 @@ export const Card = styled(CardComponent)<CardProps>`
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
height: 90%;
|
||||
height: calc(100% - 40px);
|
||||
padding: 0;
|
||||
${({ $panelType }): FlattenSimpleInterpolation =>
|
||||
$panelType === PANEL_TYPES.TABLE
|
||||
? css`
|
||||
padding-top: 1.8rem;
|
||||
`
|
||||
: css``}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -26,7 +26,12 @@ const GridPanelSwitch = forwardRef<
|
||||
yAxisUnit,
|
||||
thresholds,
|
||||
},
|
||||
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
|
||||
[PANEL_TYPES.TABLE]: {
|
||||
...GRID_TABLE_CONFIG,
|
||||
data: panelData,
|
||||
query,
|
||||
thresholds,
|
||||
},
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
|
||||
@@ -1,20 +1,86 @@
|
||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||
import { Space, Tooltip } from 'antd';
|
||||
import { Events } from 'constants/events';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { memo } from 'react';
|
||||
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { memo, ReactNode, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
import { WrapperStyled } from './styles';
|
||||
import { GridTableComponentProps } from './types';
|
||||
import { findMatchingThreshold } from './utils';
|
||||
|
||||
function GridTableComponent({
|
||||
data,
|
||||
query,
|
||||
thresholds,
|
||||
...props
|
||||
}: GridTableComponentProps): JSX.Element {
|
||||
const { t } = useTranslation(['valueGraph']);
|
||||
const { columns, dataSource } = useMemo(
|
||||
() =>
|
||||
createTableColumnsFromQuery({
|
||||
query,
|
||||
queryTableData: data,
|
||||
}),
|
||||
[data, query],
|
||||
);
|
||||
|
||||
const newColumnData = columns.map((e) => ({
|
||||
...e,
|
||||
render: (text: string): ReactNode => {
|
||||
const isNumber = !Number.isNaN(Number(text));
|
||||
if (thresholds && isNumber) {
|
||||
const { hasMultipleMatches, threshold } = findMatchingThreshold(
|
||||
thresholds,
|
||||
e.title as string,
|
||||
Number(text),
|
||||
);
|
||||
|
||||
const idx = thresholds.findIndex(
|
||||
(t) => t.thresholdTableOptions === e.title,
|
||||
);
|
||||
if (threshold && idx !== -1) {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
threshold.thresholdFormat === 'Background'
|
||||
? { backgroundColor: threshold.thresholdColor }
|
||||
: { color: threshold.thresholdColor }
|
||||
}
|
||||
>
|
||||
<Space>
|
||||
{text}
|
||||
{hasMultipleMatches && (
|
||||
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||
<ExclamationCircleFilled className="value-graph-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <div>{text}</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
eventEmitter.emit(Events.TABLE_COLUMNS_DATA, {
|
||||
columns: newColumnData,
|
||||
dataSource,
|
||||
});
|
||||
}, [dataSource, newColumnData]);
|
||||
|
||||
return (
|
||||
<WrapperStyled>
|
||||
<QueryTable
|
||||
query={query}
|
||||
queryTableData={data}
|
||||
loading={false}
|
||||
columns={newColumnData}
|
||||
dataSource={dataSource}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -19,5 +19,13 @@ export const WrapperStyled = styled.div`
|
||||
& .ant-table {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
> .ant-table-container {
|
||||
> .ant-table-content {
|
||||
> table {
|
||||
min-width: 99% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { TableProps } from 'antd';
|
||||
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
|
||||
import {
|
||||
ThresholdOperators,
|
||||
ThresholdProps,
|
||||
} from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type GridTableComponentProps = { query: Query } & Pick<
|
||||
LogsExplorerTableProps,
|
||||
'data'
|
||||
> &
|
||||
export type GridTableComponentProps = {
|
||||
query: Query;
|
||||
thresholds?: ThresholdProps[];
|
||||
} & Pick<LogsExplorerTableProps, 'data'> &
|
||||
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
||||
|
||||
export type RequiredThresholdProps = Omit<
|
||||
ThresholdProps,
|
||||
'thresholdTableOptions' | 'thresholdOperator' | 'thresholdValue'
|
||||
> & {
|
||||
thresholdTableOptions: string;
|
||||
thresholdOperator: ThresholdOperators;
|
||||
thresholdValue: number;
|
||||
};
|
||||
|
||||