Compare commits
85 Commits
v0.34.3-d1
...
jest-githu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ced74603c0 | ||
|
|
f59fb81109 | ||
|
|
507e68a0c1 | ||
|
|
4ad8a1f3ad | ||
|
|
19faf6a584 | ||
|
|
3978ada811 | ||
|
|
0a04fc04a5 | ||
|
|
7c9e333b84 | ||
|
|
dd78afb20f | ||
|
|
237d765376 | ||
|
|
85e865fb1b | ||
|
|
975e5daf03 | ||
|
|
8a532cca17 | ||
|
|
b9c908719f | ||
|
|
63c7b5e9e1 | ||
|
|
32eeb3d106 | ||
|
|
1a4ec2bf00 | ||
|
|
1d014ab4f7 | ||
|
|
418ab67d50 | ||
|
|
7efe907757 | ||
|
|
1d1154aa8c | ||
|
|
a16fca6376 | ||
|
|
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 | ||
|
|
8072fede85 | ||
|
|
112783d618 | ||
|
|
4644b1c200 | ||
|
|
bb09c84679 | ||
|
|
fc5f0fbf9e | ||
|
|
d6f0559adc | ||
|
|
0d7f7df76c | ||
|
|
7104d8e0f5 | ||
|
|
a20693fa9f | ||
|
|
0b991331d7 | ||
|
|
aad44a1037 | ||
|
|
3e29161fea | ||
|
|
b616dca52d | ||
|
|
be519666a3 | ||
|
|
a48edac13b | ||
|
|
0a77c7ab85 | ||
|
|
9fb32acf6d | ||
|
|
b2d6d75eef | ||
|
|
07d126c669 | ||
|
|
50d584cc89 | ||
|
|
1b6b3c2fdf | ||
|
|
1f0fdfd403 | ||
|
|
ae3b604cdc | ||
|
|
381f497b95 | ||
|
|
8045c4e5ae | ||
|
|
7451e885c3 | ||
|
|
01df53074c | ||
|
|
b6a79ab22c | ||
|
|
dae817640b | ||
|
|
16839eb7d3 | ||
|
|
780a863943 | ||
|
|
5e0b6366cc | ||
|
|
8eb2b9e3d0 | ||
|
|
97ed163002 | ||
|
|
e18bb7d5bc | ||
|
|
1e4cf2513c | ||
|
|
988ede7776 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -8,8 +8,4 @@
|
||||
/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
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-->
|
||||
32
.github/workflows/jest-code-coverage.yml
vendored
Normal file
32
.github/workflows/jest-code-coverage.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Code Coverage
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
- release/v*
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
- release/v*
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
- uses: ArtiomTr/jest-coverage-report-action@v2
|
||||
with:
|
||||
package-manager: yarn
|
||||
working-directory: frontend
|
||||
test-script: yarn jest:coverage
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
output: comment
|
||||
prnumber: ${{ steps.findPr.outputs.number }}
|
||||
8
.github/workflows/push.yaml
vendored
8
.github/workflows/push.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v5.1
|
||||
uses: tj-actions/branch-names@v7.0.7
|
||||
- name: Set docker tag environment
|
||||
run: |
|
||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v5.1
|
||||
uses: tj-actions/branch-names@v7.0.7
|
||||
- name: Set docker tag environment
|
||||
run: |
|
||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v5.1
|
||||
uses: tj-actions/branch-names@v7.0.7
|
||||
- name: Set docker tag environment
|
||||
run: |
|
||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
id: short-sha
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v5.1
|
||||
uses: tj-actions/branch-names@v7.0.7
|
||||
- name: Set docker tag environment
|
||||
run: |
|
||||
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
|
||||
|
||||
2
.github/workflows/staging-deployment.yaml
vendored
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 .
|
||||
|
||||
@@ -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.3
|
||||
image: signoz/query-service:0.35.1
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.34.3
|
||||
image: signoz/frontend:0.35.1
|
||||
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.1
|
||||
image: signoz/signoz-otel-collector:0.88.3
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.88.1
|
||||
image: signoz/signoz-schema-migrator:0.88.3
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -250,7 +250,7 @@ services:
|
||||
# - clickhouse-3
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.88.1
|
||||
image: signoz/signoz-otel-collector:0.88.3
|
||||
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.1}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.3}
|
||||
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.1
|
||||
image: signoz/signoz-otel-collector:0.88.3
|
||||
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.1
|
||||
image: signoz/signoz-otel-collector:0.88.3
|
||||
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.3}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.35.1}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.34.3}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.35.1}
|
||||
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.1}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.3}
|
||||
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.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.3}
|
||||
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.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.3}
|
||||
container_name: signoz-otel-collector-metrics
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
@@ -87,7 +90,7 @@
|
||||
"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",
|
||||
|
||||
@@ -21,5 +21,9 @@
|
||||
"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?",
|
||||
"delete_dashboard_success": "{{name}} dashboard deleted successfully"
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -24,5 +24,9 @@
|
||||
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
|
||||
"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.",
|
||||
"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard.",
|
||||
"delete_dashboard_success": "{{name}} dashboard deleted successfully"
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -32,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",
|
||||
|
||||
@@ -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,10 +1,9 @@
|
||||
import cacheBursting from 'i18n-translations-hash.json';
|
||||
import i18n from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import cacheBursting from '../../i18n-translations-hash.json';
|
||||
|
||||
i18n
|
||||
// load translation using http -> see /public/locales
|
||||
.use(Backend)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,9 @@
|
||||
/* 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,
|
||||
@@ -127,6 +128,16 @@ 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 (
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<div className="uplot-graph-container" ref={targetRef}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
@@ -57,6 +57,12 @@ function EditAlertChannels({
|
||||
setType(value as ChannelType);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
formInstance.setFieldsValue({
|
||||
...initialValue,
|
||||
});
|
||||
}, [formInstance, initialValue]);
|
||||
|
||||
const prepareSlackRequest = useCallback(
|
||||
() => ({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { 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';
|
||||
@@ -9,7 +10,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ 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,12 @@
|
||||
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 +46,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 +181,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 +210,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 +265,7 @@ function WidgetGraphComponent({
|
||||
title={widget?.title || 'View'}
|
||||
footer={[]}
|
||||
centered
|
||||
open={modal}
|
||||
open={isFullViewOpen}
|
||||
onCancel={onToggleModelHandler}
|
||||
width="85%"
|
||||
destroyOnClose
|
||||
@@ -270,7 +299,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}
|
||||
|
||||
@@ -9,11 +9,13 @@ 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,10 +30,13 @@ function GridCardGraph({
|
||||
isQueryEnabled,
|
||||
threshold,
|
||||
variables,
|
||||
filterNaN,
|
||||
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 => {
|
||||
@@ -49,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,
|
||||
@@ -90,11 +106,14 @@ function GridCardGraph({
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const chartData = getUPlotChartData(
|
||||
queryResponse?.data?.payload,
|
||||
undefined,
|
||||
filterNaN,
|
||||
);
|
||||
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();
|
||||
|
||||
@@ -114,6 +133,8 @@ function GridCardGraph({
|
||||
yAxisUnit: widget?.yAxisUnit,
|
||||
onClickHandler,
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
}),
|
||||
[
|
||||
widget?.id,
|
||||
@@ -124,6 +145,8 @@ function GridCardGraph({
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
onClickHandler,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface GridCardGraphProps {
|
||||
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
|
||||
isQueryEnabled: boolean;
|
||||
variables?: Dashboard['data']['variables'];
|
||||
filterNaN?: boolean;
|
||||
fillSpans?: boolean;
|
||||
}
|
||||
|
||||
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||
|
||||
@@ -5,3 +5,11 @@
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-graph-container {
|
||||
height: 100%;
|
||||
|
||||
&.graph {
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -155,6 +156,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
onLayoutChange={setLayouts}
|
||||
draggableHandle=".drag-handle"
|
||||
layout={layouts}
|
||||
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
|
||||
>
|
||||
{layouts.map((layout) => {
|
||||
const { i: id } = layout;
|
||||
@@ -165,7 +167,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||
isDarkMode={isDarkMode}
|
||||
key={id}
|
||||
data-grid={layout}
|
||||
data-grid={JSON.stringify(currentWidget)}
|
||||
>
|
||||
<Card
|
||||
className="grid-item"
|
||||
|
||||
@@ -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,11 +1,13 @@
|
||||
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';
|
||||
@@ -15,7 +17,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
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(
|
||||
@@ -112,7 +97,6 @@ function WidgetHeader({
|
||||
|
||||
if (functionToCall) {
|
||||
functionToCall();
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -169,10 +153,6 @@ function WidgetHeader({
|
||||
|
||||
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
||||
|
||||
const onClickHandler = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: updatedMenuList,
|
||||
@@ -186,49 +166,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 data-testid={title}>
|
||||
{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``}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -8,5 +8,18 @@
|
||||
.upgrade-link {
|
||||
padding: 0px;
|
||||
padding-right: 4px;
|
||||
display: inline !important;
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: white;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 2px;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: white;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import './Header.styles.scss';
|
||||
|
||||
import {
|
||||
@@ -135,16 +138,17 @@ function HeaderContainer(): JSX.Element {
|
||||
<>
|
||||
{showTrialExpiryBanner && (
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
<span>
|
||||
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
|
||||
</span>
|
||||
{role === 'ADMIN' ? (
|
||||
<span>
|
||||
Please
|
||||
<Button className="upgrade-link" type="link" onClick={handleUpgrade}>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||
upgrade
|
||||
</Button>
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
|
||||
378
frontend/src/container/ListOfDashboard/DashboardsList.tsx
Normal file
378
frontend/src/container/ListOfDashboard/DashboardsList.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
|
||||
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
DynamicColumnsKey,
|
||||
TableDataSource,
|
||||
} from 'components/ResizeTable/contants';
|
||||
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
||||
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import history from 'lib/history';
|
||||
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
|
||||
import ImportJSON from './ImportJSON';
|
||||
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
|
||||
import DeleteButton from './TableComponents/DeleteButton';
|
||||
import Name from './TableComponents/Name';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
function DashboardsList(): JSX.Element {
|
||||
const {
|
||||
data: dashboardListResponse = [],
|
||||
isLoading: isDashboardListLoading,
|
||||
refetch: refetchDashboardList,
|
||||
} = useGetAllDashboard();
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [action, createNewDashboard] = useComponentPermission(
|
||||
['action', 'create_new_dashboards'],
|
||||
role,
|
||||
);
|
||||
|
||||
const { t } = useTranslation('dashboard');
|
||||
|
||||
const [
|
||||
isImportJSONModalVisible,
|
||||
setIsImportJSONModalVisible,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
||||
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
|
||||
|
||||
const [dashboards, setDashboards] = useState<Dashboard[]>();
|
||||
|
||||
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
||||
const sortedDashboards = dashboards.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||
);
|
||||
setDashboards(sortedDashboards);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
sortDashboardsByCreatedAt(dashboardListResponse);
|
||||
}, [dashboardListResponse]);
|
||||
|
||||
const [newDashboardState, setNewDashboardState] = useState({
|
||||
loading: false,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
const dynamicColumns: TableColumnProps<Data>[] = [
|
||||
{
|
||||
title: 'Created At',
|
||||
dataIndex: 'createdAt',
|
||||
width: 30,
|
||||
key: DynamicColumnsKey.CreatedAt,
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
console.log({ a });
|
||||
const prev = new Date(a.createdAt).getTime();
|
||||
const next = new Date(b.createdAt).getTime();
|
||||
|
||||
return prev - next;
|
||||
},
|
||||
render: DateComponent,
|
||||
},
|
||||
{
|
||||
title: 'Created By',
|
||||
dataIndex: 'createdBy',
|
||||
width: 30,
|
||||
key: DynamicColumnsKey.CreatedBy,
|
||||
},
|
||||
{
|
||||
title: 'Last Updated Time',
|
||||
width: 30,
|
||||
dataIndex: 'lastUpdatedTime',
|
||||
key: DynamicColumnsKey.UpdatedAt,
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
const prev = new Date(a.lastUpdatedTime).getTime();
|
||||
const next = new Date(b.lastUpdatedTime).getTime();
|
||||
|
||||
return prev - next;
|
||||
},
|
||||
render: DateComponent,
|
||||
},
|
||||
{
|
||||
title: 'Last Updated By',
|
||||
dataIndex: 'lastUpdatedBy',
|
||||
width: 30,
|
||||
key: DynamicColumnsKey.UpdatedBy,
|
||||
},
|
||||
];
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const tableColumns: TableColumnProps<Data>[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
width: 40,
|
||||
render: Name,
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
width: 50,
|
||||
dataIndex: 'description',
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
dataIndex: 'tags',
|
||||
width: 50,
|
||||
render: (value): JSX.Element => <LabelColumn labels={value} />,
|
||||
},
|
||||
];
|
||||
|
||||
if (action) {
|
||||
tableColumns.push({
|
||||
title: 'Action',
|
||||
dataIndex: '',
|
||||
width: 40,
|
||||
render: DeleteButton,
|
||||
});
|
||||
}
|
||||
|
||||
return tableColumns;
|
||||
}, [action]);
|
||||
|
||||
const data: Data[] =
|
||||
dashboards?.map((e) => ({
|
||||
createdAt: e.created_at,
|
||||
description: e.data.description || '',
|
||||
id: e.uuid,
|
||||
lastUpdatedTime: e.updated_at,
|
||||
name: e.data.title,
|
||||
tags: e.data.tags || [],
|
||||
key: e.uuid,
|
||||
createdBy: e.created_by,
|
||||
isLocked: !!e.isLocked || false,
|
||||
lastUpdatedBy: e.updated_by,
|
||||
refetchDashboardList,
|
||||
})) || [];
|
||||
|
||||
const onNewDashboardHandler = useCallback(async () => {
|
||||
try {
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: true,
|
||||
});
|
||||
const response = await createDashboard({
|
||||
title: t('new_dashboard_title', {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
history.push(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.payload.uuid,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
error: true,
|
||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||
});
|
||||
}
|
||||
}, [newDashboardState, t]);
|
||||
|
||||
const getText = useCallback(() => {
|
||||
if (!newDashboardState.error && !newDashboardState.loading) {
|
||||
return 'New Dashboard';
|
||||
}
|
||||
|
||||
if (newDashboardState.loading) {
|
||||
return 'Loading';
|
||||
}
|
||||
|
||||
return newDashboardState.errorMessage;
|
||||
}, [
|
||||
newDashboardState.error,
|
||||
newDashboardState.errorMessage,
|
||||
newDashboardState.loading,
|
||||
]);
|
||||
|
||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||
setIsImportJSONModalVisible((state) => !state);
|
||||
setUploadedGrafana(uploadedGrafana);
|
||||
};
|
||||
|
||||
const getMenuItems = useMemo(() => {
|
||||
const menuItems: ItemType[] = [
|
||||
{
|
||||
key: t('import_json').toString(),
|
||||
label: t('import_json'),
|
||||
onClick: (): void => onModalHandler(false),
|
||||
},
|
||||
{
|
||||
key: t('import_grafana_json').toString(),
|
||||
label: t('import_grafana_json'),
|
||||
onClick: (): void => onModalHandler(true),
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (createNewDashboard) {
|
||||
menuItems.unshift({
|
||||
key: t('create_dashboard').toString(),
|
||||
label: t('create_dashboard'),
|
||||
disabled: isDashboardListLoading,
|
||||
onClick: onNewDashboardHandler,
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
|
||||
|
||||
const searchArrayOfObjects = (searchValue: string): any[] => {
|
||||
// Convert the searchValue to lowercase for case-insensitive search
|
||||
const searchValueLowerCase = searchValue.toLowerCase();
|
||||
|
||||
// Use the filter method to find matching objects
|
||||
return dashboardListResponse.filter((item: any) => {
|
||||
// Convert each property value to lowercase for case-insensitive search
|
||||
const itemValues = Object.values(item?.data).map((value: any) =>
|
||||
value.toString().toLowerCase(),
|
||||
);
|
||||
|
||||
// Check if any property value contains the searchValue
|
||||
return itemValues.some((value) => value.includes(searchValueLowerCase));
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = useDebouncedFn((event: unknown): void => {
|
||||
setIsFilteringDashboards(true);
|
||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||
const filteredDashboards = searchArrayOfObjects(searchText);
|
||||
setDashboards(filteredDashboards);
|
||||
setIsFilteringDashboards(false);
|
||||
}, 500);
|
||||
|
||||
const GetHeader = useMemo(
|
||||
() => (
|
||||
<Row gutter={16} align="middle">
|
||||
<Col span={18}>
|
||||
<Search
|
||||
disabled={isDashboardListLoading}
|
||||
placeholder="Search by Name, Description, Tags"
|
||||
onChange={handleSearch}
|
||||
loading={isFilteringDashboards}
|
||||
style={{ marginBottom: 16, marginTop: 16 }}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col
|
||||
span={6}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<ButtonContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to create dashboards`,
|
||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||
}}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
|
||||
<Dropdown
|
||||
menu={{ items: getMenuItems }}
|
||||
disabled={isDashboardListLoading}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<NewDashboardButton
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
data-testid="create-new-dashboard"
|
||||
loading={newDashboardState.loading}
|
||||
danger={newDashboardState.error}
|
||||
>
|
||||
{getText()}
|
||||
</NewDashboardButton>
|
||||
</Dropdown>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
[
|
||||
isDashboardListLoading,
|
||||
handleSearch,
|
||||
isFilteringDashboards,
|
||||
getMenuItems,
|
||||
newDashboardState.loading,
|
||||
newDashboardState.error,
|
||||
getText,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{GetHeader}
|
||||
|
||||
<TableContainer>
|
||||
<ImportJSON
|
||||
isImportJSONModalVisible={isImportJSONModalVisible}
|
||||
uploadedGrafana={uploadedGrafana}
|
||||
onModalHandler={(): void => onModalHandler(false)}
|
||||
/>
|
||||
<DynamicColumnTable
|
||||
tablesource={TableDataSource.Dashboard}
|
||||
dynamicColumns={dynamicColumns}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
defaultPageSize: 10,
|
||||
total: data?.length || 0,
|
||||
}}
|
||||
showHeader
|
||||
bordered
|
||||
sticky
|
||||
loading={isDashboardListLoading}
|
||||
dataSource={data}
|
||||
showSorterTooltip
|
||||
/>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
key: Key;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
lastUpdatedTime: string;
|
||||
lastUpdatedBy: string;
|
||||
isLocked: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default DashboardsList;
|
||||
@@ -2,7 +2,7 @@ import { Typography } from 'antd';
|
||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
||||
import getFormattedDate from 'lib/getFormatedDate';
|
||||
|
||||
import { Data } from '..';
|
||||
import { Data } from '../DashboardsList';
|
||||
|
||||
function Created(createdBy: Data['createdBy']): JSX.Element {
|
||||
const time = new Date(createdBy);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import { Data } from '..';
|
||||
import { Data } from '../DashboardsList';
|
||||
import { TableLinkText } from './styles';
|
||||
|
||||
interface DeleteButtonProps {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LockFilled } from '@ant-design/icons';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
|
||||
import { Data } from '..';
|
||||
import { Data } from '../DashboardsList';
|
||||
import { TableLinkText } from './styles';
|
||||
|
||||
function Name(name: Data['name'], data: Data): JSX.Element {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import { Tag } from 'antd';
|
||||
|
||||
import { Data } from '../index';
|
||||
import { Data } from '../DashboardsList';
|
||||
|
||||
function Tags(data: Data['tags']): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -1,379 +1,3 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
|
||||
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
DynamicColumnsKey,
|
||||
TableDataSource,
|
||||
} from 'components/ResizeTable/contants';
|
||||
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
|
||||
import LabelColumn from 'components/TableRenderer/LabelColumn';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import history from 'lib/history';
|
||||
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardsList from './DashboardsList';
|
||||
|
||||
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
|
||||
import ImportJSON from './ImportJSON';
|
||||
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
|
||||
import DeleteButton from './TableComponents/DeleteButton';
|
||||
import Name from './TableComponents/Name';
|
||||
|
||||
function ListOfAllDashboard(): JSX.Element {
|
||||
const { Search } = Input;
|
||||
|
||||
const {
|
||||
data: dashboardListResponse = [],
|
||||
isLoading: isDashboardListLoading,
|
||||
refetch: refetchDashboardList,
|
||||
} = useGetAllDashboard();
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [action, createNewDashboard] = useComponentPermission(
|
||||
['action', 'create_new_dashboards'],
|
||||
role,
|
||||
);
|
||||
|
||||
const { t } = useTranslation('dashboard');
|
||||
|
||||
const [
|
||||
isImportJSONModalVisible,
|
||||
setIsImportJSONModalVisible,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
||||
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
|
||||
|
||||
const [dashboards, setDashboards] = useState<Dashboard[]>();
|
||||
|
||||
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
||||
const sortedDashboards = dashboards.sort(
|
||||
(a, b) =>
|
||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
||||
);
|
||||
setDashboards(sortedDashboards);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
sortDashboardsByCreatedAt(dashboardListResponse);
|
||||
}, [dashboardListResponse]);
|
||||
|
||||
const [newDashboardState, setNewDashboardState] = useState({
|
||||
loading: false,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
const dynamicColumns: TableColumnProps<Data>[] = [
|
||||
{
|
||||
title: 'Created At',
|
||||
dataIndex: 'createdAt',
|
||||
width: 30,
|
||||
key: DynamicColumnsKey.CreatedAt,
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
console.log({ a });
|
||||
const prev = new Date(a.createdAt).getTime();
|
||||
const next = new Date(b.createdAt).getTime();
|
||||
|
||||
return prev - next;
|
||||
},
|
||||
render: DateComponent,
|
||||
},
|
||||
{
|
||||
title: 'Created By',
|
||||
dataIndex: 'createdBy',
|
||||
width: 30,
|
||||
key: DynamicColumnsKey.CreatedBy,
|
||||
},
|
||||
{
|
||||
title: 'Last Updated Time',
|
||||
width: 30,
|
||||
dataIndex: 'lastUpdatedTime',
|
||||
key: DynamicColumnsKey.UpdatedAt,
|
||||
sorter: (a: Data, b: Data): number => {
|
||||
const prev = new Date(a.lastUpdatedTime).getTime();
|
||||
const next = new Date(b.lastUpdatedTime).getTime();
|
||||
|
||||
return prev - next;
|
||||
},
|
||||
render: DateComponent,
|
||||
},
|
||||
{
|
||||
title: 'Last Updated By',
|
||||
dataIndex: 'lastUpdatedBy',
|
||||
width: 30,
|
||||
key: DynamicColumnsKey.UpdatedBy,
|
||||
},
|
||||
];
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const tableColumns: TableColumnProps<Data>[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
width: 40,
|
||||
render: Name,
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
width: 50,
|
||||
dataIndex: 'description',
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
dataIndex: 'tags',
|
||||
width: 50,
|
||||
render: (value): JSX.Element => <LabelColumn labels={value} />,
|
||||
},
|
||||
];
|
||||
|
||||
if (action) {
|
||||
tableColumns.push({
|
||||
title: 'Action',
|
||||
dataIndex: '',
|
||||
width: 40,
|
||||
render: DeleteButton,
|
||||
});
|
||||
}
|
||||
|
||||
return tableColumns;
|
||||
}, [action]);
|
||||
|
||||
const data: Data[] =
|
||||
dashboards?.map((e) => ({
|
||||
createdAt: e.created_at,
|
||||
description: e.data.description || '',
|
||||
id: e.uuid,
|
||||
lastUpdatedTime: e.updated_at,
|
||||
name: e.data.title,
|
||||
tags: e.data.tags || [],
|
||||
key: e.uuid,
|
||||
createdBy: e.created_by,
|
||||
isLocked: !!e.isLocked || false,
|
||||
lastUpdatedBy: e.updated_by,
|
||||
refetchDashboardList,
|
||||
})) || [];
|
||||
|
||||
const onNewDashboardHandler = useCallback(async () => {
|
||||
try {
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: true,
|
||||
});
|
||||
const response = await createDashboard({
|
||||
title: t('new_dashboard_title', {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
history.push(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.payload.uuid,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
error: true,
|
||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||
});
|
||||
}
|
||||
}, [newDashboardState, t]);
|
||||
|
||||
const getText = useCallback(() => {
|
||||
if (!newDashboardState.error && !newDashboardState.loading) {
|
||||
return 'New Dashboard';
|
||||
}
|
||||
|
||||
if (newDashboardState.loading) {
|
||||
return 'Loading';
|
||||
}
|
||||
|
||||
return newDashboardState.errorMessage;
|
||||
}, [
|
||||
newDashboardState.error,
|
||||
newDashboardState.errorMessage,
|
||||
newDashboardState.loading,
|
||||
]);
|
||||
|
||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||
setIsImportJSONModalVisible((state) => !state);
|
||||
setUploadedGrafana(uploadedGrafana);
|
||||
};
|
||||
|
||||
const getMenuItems = useMemo(() => {
|
||||
const menuItems: ItemType[] = [
|
||||
{
|
||||
key: t('import_json').toString(),
|
||||
label: t('import_json'),
|
||||
onClick: (): void => onModalHandler(false),
|
||||
},
|
||||
{
|
||||
key: t('import_grafana_json').toString(),
|
||||
label: t('import_grafana_json'),
|
||||
onClick: (): void => onModalHandler(true),
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (createNewDashboard) {
|
||||
menuItems.unshift({
|
||||
key: t('create_dashboard').toString(),
|
||||
label: t('create_dashboard'),
|
||||
disabled: isDashboardListLoading,
|
||||
onClick: onNewDashboardHandler,
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
|
||||
|
||||
const searchArrayOfObjects = (searchValue: string): any[] => {
|
||||
// Convert the searchValue to lowercase for case-insensitive search
|
||||
const searchValueLowerCase = searchValue.toLowerCase();
|
||||
|
||||
// Use the filter method to find matching objects
|
||||
return dashboardListResponse.filter((item: any) => {
|
||||
// Convert each property value to lowercase for case-insensitive search
|
||||
const itemValues = Object.values(item?.data).map((value: any) =>
|
||||
value.toString().toLowerCase(),
|
||||
);
|
||||
|
||||
// Check if any property value contains the searchValue
|
||||
return itemValues.some((value) => value.includes(searchValueLowerCase));
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = useDebouncedFn((event: unknown): void => {
|
||||
setIsFilteringDashboards(true);
|
||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||
const filteredDashboards = searchArrayOfObjects(searchText);
|
||||
setDashboards(filteredDashboards);
|
||||
setIsFilteringDashboards(false);
|
||||
}, 500);
|
||||
|
||||
const GetHeader = useMemo(
|
||||
() => (
|
||||
<Row gutter={16} align="middle">
|
||||
<Col span={18}>
|
||||
<Search
|
||||
disabled={isDashboardListLoading}
|
||||
placeholder="Search by Name, Description, Tags"
|
||||
onChange={handleSearch}
|
||||
loading={isFilteringDashboards}
|
||||
style={{ marginBottom: 16, marginTop: 16 }}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col
|
||||
span={6}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<ButtonContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to create dashboards`,
|
||||
url: 'https://signoz.io/docs/userguide/dashboards',
|
||||
}}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
|
||||
<Dropdown
|
||||
menu={{ items: getMenuItems }}
|
||||
disabled={isDashboardListLoading}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<NewDashboardButton
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
data-testid="create-new-dashboard"
|
||||
loading={newDashboardState.loading}
|
||||
danger={newDashboardState.error}
|
||||
>
|
||||
{getText()}
|
||||
</NewDashboardButton>
|
||||
</Dropdown>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
[
|
||||
Search,
|
||||
isDashboardListLoading,
|
||||
handleSearch,
|
||||
isFilteringDashboards,
|
||||
getMenuItems,
|
||||
newDashboardState.loading,
|
||||
newDashboardState.error,
|
||||
getText,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{GetHeader}
|
||||
|
||||
<TableContainer>
|
||||
<ImportJSON
|
||||
isImportJSONModalVisible={isImportJSONModalVisible}
|
||||
uploadedGrafana={uploadedGrafana}
|
||||
onModalHandler={(): void => onModalHandler(false)}
|
||||
/>
|
||||
<DynamicColumnTable
|
||||
tablesource={TableDataSource.Dashboard}
|
||||
dynamicColumns={dynamicColumns}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
defaultPageSize: 10,
|
||||
total: data?.length || 0,
|
||||
}}
|
||||
showHeader
|
||||
bordered
|
||||
sticky
|
||||
loading={isDashboardListLoading}
|
||||
dataSource={data}
|
||||
showSorterTooltip
|
||||
/>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
key: Key;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
lastUpdatedTime: string;
|
||||
lastUpdatedBy: string;
|
||||
isLocked: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default ListOfAllDashboard;
|
||||
export default DashboardsList;
|
||||
|
||||
@@ -8,9 +8,10 @@ export const getWidgetQueryBuilder = ({
|
||||
title = '',
|
||||
panelTypes,
|
||||
yAxisUnit = '',
|
||||
id,
|
||||
}: GetWidgetQueryBuilderProps): Widgets => ({
|
||||
description: '',
|
||||
id: v4(),
|
||||
id: id || v4(),
|
||||
isStacked: false,
|
||||
nullZeroValues: '',
|
||||
opacity: '0',
|
||||
|
||||
@@ -16,7 +16,7 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { GraphTitle, MENU_ITEMS } from '../constant';
|
||||
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, Row } from '../styles';
|
||||
import { Button } from './styles';
|
||||
@@ -66,6 +66,7 @@ function DBCall(): JSX.Element {
|
||||
title: GraphTitle.DATABASE_CALLS_RPS,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: 'reqps',
|
||||
id: SERVICE_CHART_ID.dbCallsRPS,
|
||||
}),
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
@@ -85,6 +86,7 @@ function DBCall(): JSX.Element {
|
||||
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: 'ms',
|
||||
id: SERVICE_CHART_ID.dbCallsAvgDuration,
|
||||
}),
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
@@ -107,7 +109,7 @@ function DBCall(): JSX.Element {
|
||||
<Card data-testid="database_call_rps">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
filterNaN
|
||||
fillSpans={false}
|
||||
name="database_call_rps"
|
||||
widget={databaseCallsRPSWidget}
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
@@ -141,7 +143,7 @@ function DBCall(): JSX.Element {
|
||||
<Card data-testid="database_call_avg_duration">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
filterNaN
|
||||
fillSpans
|
||||
name="database_call_avg_duration"
|
||||
widget={databaseCallsAverageDurationWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useParams } from 'react-router-dom';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
|
||||
import { GraphTitle, legend, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, Row } from '../styles';
|
||||
import { Button } from './styles';
|
||||
@@ -57,6 +57,7 @@ function External(): JSX.Element {
|
||||
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: '%',
|
||||
id: SERVICE_CHART_ID.externalCallErrorPercentage,
|
||||
}),
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
@@ -82,6 +83,7 @@ function External(): JSX.Element {
|
||||
title: GraphTitle.EXTERNAL_CALL_DURATION,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: 'ms',
|
||||
id: SERVICE_CHART_ID.externalCallDuration,
|
||||
}),
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
@@ -103,6 +105,7 @@ function External(): JSX.Element {
|
||||
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: 'reqps',
|
||||
id: SERVICE_CHART_ID.externalCallRPSByAddress,
|
||||
}),
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
@@ -124,6 +127,7 @@ function External(): JSX.Element {
|
||||
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: 'ms',
|
||||
id: SERVICE_CHART_ID.externalCallDurationByAddress,
|
||||
}),
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
@@ -148,7 +152,7 @@ function External(): JSX.Element {
|
||||
<Card data-testid="external_call_error_percentage">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
filterNaN
|
||||
fillSpans={false}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
name="external_call_error_percentage"
|
||||
widget={externalCallErrorWidget}
|
||||
@@ -184,7 +188,7 @@ function External(): JSX.Element {
|
||||
<Card data-testid="external_call_duration">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
filterNaN
|
||||
fillSpans
|
||||
name="external_call_duration"
|
||||
headerMenuList={MENU_ITEMS}
|
||||
widget={externalCallDurationWidget}
|
||||
@@ -221,7 +225,7 @@ function External(): JSX.Element {
|
||||
<Card data-testid="external_call_rps_by_address">
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
filterNaN
|
||||
fillSpans
|
||||
name="external_call_rps_by_address"
|
||||
widget={externalCallRPSWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
@@ -260,7 +264,7 @@ function External(): JSX.Element {
|
||||
name="external_call_duration_by_address"
|
||||
widget={externalCallDurationAddressWidget}
|
||||
headerMenuList={MENU_ITEMS}
|
||||
filterNaN
|
||||
fillSpans
|
||||
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
|
||||
onGraphClickHandler(setSelectedTimeStamp)(
|
||||
xValue,
|
||||
|
||||
@@ -26,7 +26,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { GraphTitle } from '../constant';
|
||||
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import {
|
||||
errorPercentage,
|
||||
@@ -131,6 +131,7 @@ function Application(): JSX.Element {
|
||||
title: GraphTitle.RATE_PER_OPS,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: 'ops',
|
||||
id: SERVICE_CHART_ID.rps,
|
||||
}),
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute],
|
||||
);
|
||||
@@ -152,6 +153,7 @@ function Application(): JSX.Element {
|
||||
title: GraphTitle.ERROR_PERCENTAGE,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: '%',
|
||||
id: SERVICE_CHART_ID.errorPercentage,
|
||||
}),
|
||||
[servicename, tagFilterItems, topLevelOperationsRoute],
|
||||
);
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold';
|
||||
import { GraphTitle } from 'container/MetricsApplication/constant';
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
} from 'container/MetricsApplication/constant';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
@@ -59,6 +62,7 @@ function ApDexMetrics({
|
||||
</Space>
|
||||
),
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
id: SERVICE_CHART_ID.apdex,
|
||||
}),
|
||||
[
|
||||
delta,
|
||||
@@ -84,7 +88,7 @@ function ApDexMetrics({
|
||||
return (
|
||||
<Graph
|
||||
name="apdex"
|
||||
filterNaN
|
||||
fillSpans={false}
|
||||
widget={apDexMetricsWidget}
|
||||
onDragSelect={onDragSelect}
|
||||
onClickHandler={handleGraphClick('ApDex')}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import { GraphTitle } from 'container/MetricsApplication/constant';
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
} from 'container/MetricsApplication/constant';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||
@@ -59,6 +62,7 @@ function ServiceOverview({
|
||||
title: GraphTitle.LATENCY,
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
yAxisUnit: 'ns',
|
||||
id: SERVICE_CHART_ID.latency,
|
||||
}),
|
||||
[servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems],
|
||||
);
|
||||
@@ -88,7 +92,7 @@ function ServiceOverview({
|
||||
widget={latencyWidget}
|
||||
onClickHandler={handleGraphClick('Service')}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
filterNaN
|
||||
fillSpans={false}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
|
||||
@@ -27,7 +27,7 @@ function TopLevelOperation({
|
||||
) : (
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
filterNaN
|
||||
fillSpans={false}
|
||||
name={name}
|
||||
widget={widget}
|
||||
onClickHandler={handleGraphClick(opName)}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { TopOperationList } from '../TopOperationsTable';
|
||||
|
||||
interface TopOperation {
|
||||
numCalls: number;
|
||||
errorCount: number;
|
||||
}
|
||||
|
||||
export const getTopOperationList = ({
|
||||
errorCount,
|
||||
numCalls,
|
||||
}: TopOperation): TopOperationList =>
|
||||
({
|
||||
p50: 0,
|
||||
errorCount,
|
||||
name: 'test',
|
||||
numCalls,
|
||||
p95: 0,
|
||||
p99: 0,
|
||||
} as TopOperationList);
|
||||
@@ -79,3 +79,17 @@ export const topOperationMetricsDownloadOptions: DownloadOptions = {
|
||||
isDownloadEnabled: true,
|
||||
fileName: 'top-operation',
|
||||
} as const;
|
||||
|
||||
export const SERVICE_CHART_ID = {
|
||||
latency: 'SERVICE_OVERVIEW_LATENCY',
|
||||
error: 'SERVICE_OVERVIEW_ERROR',
|
||||
rps: 'SERVICE_OVERVIEW_RPS',
|
||||
apdex: 'SERVICE_OVERVIEW_APDEX',
|
||||
errorPercentage: 'SERVICE_OVERVIEW_ERROR_PERCENTAGE',
|
||||
dbCallsRPS: 'SERVICE_DATABASE_CALLS_RPS',
|
||||
dbCallsAvgDuration: 'SERVICE_DATABASE_CALLS_AVG_DURATION',
|
||||
externalCallDurationByAddress: 'SERVICE_EXTERNAL_CALLS_DURATION_BY_ADDRESS',
|
||||
externalCallErrorPercentage: 'SERVICE_EXTERNAL_CALLS_ERROR_PERCENTAGE',
|
||||
externalCallDuration: 'SERVICE_EXTERNAL_CALLS_DURATION',
|
||||
externalCallRPSByAddress: 'SERVICE_EXTERNAL_CALLS_RPS_BY_ADDRESS',
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface GetWidgetQueryBuilderProps {
|
||||
title?: ReactNode;
|
||||
panelTypes: Widgets['panelTypes'];
|
||||
yAxisUnit?: Widgets['yAxisUnit'];
|
||||
id?: Widgets['id'];
|
||||
}
|
||||
|
||||
export interface NavigateToTraceProps {
|
||||
|
||||
70
frontend/src/container/MetricsApplication/utils.test.ts
Normal file
70
frontend/src/container/MetricsApplication/utils.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { getTopOperationList } from './__mocks__/getTopOperation';
|
||||
import { TopOperationList } from './TopOperationsTable';
|
||||
import {
|
||||
convertedTracesToDownloadData,
|
||||
getErrorRate,
|
||||
getNearestHighestBucketValue,
|
||||
} from './utils';
|
||||
|
||||
describe('Error Rate', () => {
|
||||
test('should return correct error rate', () => {
|
||||
const list: TopOperationList = getTopOperationList({
|
||||
errorCount: 10,
|
||||
numCalls: 100,
|
||||
});
|
||||
|
||||
expect(getErrorRate(list)).toBe(10);
|
||||
});
|
||||
|
||||
test('should handle no errors gracefully', () => {
|
||||
const list = getTopOperationList({ errorCount: 0, numCalls: 100 });
|
||||
expect(getErrorRate(list)).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle zero calls', () => {
|
||||
const list = getTopOperationList({ errorCount: 0, numCalls: 0 });
|
||||
expect(getErrorRate(list)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNearestHighestBucketValue', () => {
|
||||
test('should return nearest higher bucket value', () => {
|
||||
expect(getNearestHighestBucketValue(50, [10, 20, 30, 40, 60, 70])).toBe('60');
|
||||
});
|
||||
|
||||
test('should return +Inf for value higher than any bucket', () => {
|
||||
expect(getNearestHighestBucketValue(80, [10, 20, 30, 40, 60, 70])).toBe(
|
||||
'+Inf',
|
||||
);
|
||||
});
|
||||
|
||||
test('should return the first bucket for value lower than all buckets', () => {
|
||||
expect(getNearestHighestBucketValue(5, [10, 20, 30, 40, 60, 70])).toBe('10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertedTracesToDownloadData', () => {
|
||||
test('should convert trace data correctly', () => {
|
||||
const data = [
|
||||
{
|
||||
name: 'op1',
|
||||
p50: 50000000,
|
||||
p95: 95000000,
|
||||
p99: 99000000,
|
||||
numCalls: 100,
|
||||
errorCount: 10,
|
||||
},
|
||||
];
|
||||
|
||||
expect(convertedTracesToDownloadData(data)).toEqual([
|
||||
{
|
||||
Name: 'op1',
|
||||
'P50 (in ms)': '50.00',
|
||||
'P95 (in ms)': '95.00',
|
||||
'P99 (in ms)': '99.00',
|
||||
'Number of calls': '100',
|
||||
'Error Rate (%)': '10.00',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,12 @@ import history from 'lib/history';
|
||||
import { TopOperationList } from './TopOperationsTable';
|
||||
import { NavigateToTraceProps } from './types';
|
||||
|
||||
export const getErrorRate = (list: TopOperationList): number =>
|
||||
(list.errorCount / list.numCalls) * 100;
|
||||
export const getErrorRate = (list: TopOperationList): number => {
|
||||
if (list.errorCount === 0 && list.numCalls === 0) {
|
||||
return 0;
|
||||
}
|
||||
return (list.errorCount / list.numCalls) * 100;
|
||||
};
|
||||
|
||||
export const navigateToTrace = ({
|
||||
servicename,
|
||||
|
||||
@@ -29,7 +29,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
|
||||
<DrawerContainer
|
||||
title={drawerTitle}
|
||||
placement="right"
|
||||
width="50%"
|
||||
width="60%"
|
||||
onClose={onClose}
|
||||
open={visible}
|
||||
>
|
||||
|
||||
@@ -50,7 +50,7 @@ function DashboardDescription(): JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<Row gutter={16}>
|
||||
<Col flex={1} span={12}>
|
||||
<Col flex={1} span={9}>
|
||||
<Typography.Title
|
||||
level={4}
|
||||
style={{ padding: 0, margin: 0 }}
|
||||
@@ -80,12 +80,12 @@ function DashboardDescription(): JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={12}>
|
||||
<Row justify="end">
|
||||
<DashboardVariableSelection />
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={4} style={{ textAlign: 'right' }}>
|
||||
<Col span={3} style={{ textAlign: 'right' }}>
|
||||
{selectedData && (
|
||||
<ShareModal
|
||||
isJSONModalVisible={openDashboardJSON}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.delete-variable-name {
|
||||
font-weight: 700;
|
||||
color: rgb(207, 19, 34);
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.query-container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
min-width: 0;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -1,21 +1,16 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './VariableItem.styles.scss';
|
||||
|
||||
import { orange } from '@ant-design/colors';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import query from 'api/dashboard/variables/query';
|
||||
import { Button, Divider, Input, Select, Switch, Tag, Typography } from 'antd';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import Editor from 'components/Editor';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||
import { map } from 'lodash-es';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import {
|
||||
IDashboardVariable,
|
||||
TSortVariableValuesType,
|
||||
@@ -23,10 +18,10 @@ import {
|
||||
VariableQueryTypeArr,
|
||||
VariableSortTypeArr,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { v4 } from 'uuid';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
import { variablePropsToPayloadVariables } from '../../../utils';
|
||||
import { TVariableViewMode } from '../types';
|
||||
import { TVariableMode } from '../types';
|
||||
import { LabelContainer, VariableItemRow } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
@@ -35,9 +30,9 @@ interface VariableItemProps {
|
||||
variableData: IDashboardVariable;
|
||||
existingVariables: Record<string, IDashboardVariable>;
|
||||
onCancel: () => void;
|
||||
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void;
|
||||
onSave: (mode: TVariableMode, variableData: IDashboardVariable) => void;
|
||||
validateName: (arg0: string) => boolean;
|
||||
variableViewMode: TVariableViewMode;
|
||||
mode: TVariableMode;
|
||||
}
|
||||
function VariableItem({
|
||||
variableData,
|
||||
@@ -45,7 +40,7 @@ function VariableItem({
|
||||
onCancel,
|
||||
onSave,
|
||||
validateName,
|
||||
variableViewMode,
|
||||
mode,
|
||||
}: VariableItemProps): JSX.Element {
|
||||
const [variableName, setVariableName] = useState<string>(
|
||||
variableData.name || '',
|
||||
@@ -79,8 +74,6 @@ function VariableItem({
|
||||
);
|
||||
const [previewValues, setPreviewValues] = useState<string[]>([]);
|
||||
|
||||
// Internal states
|
||||
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
|
||||
// Error messages
|
||||
const [errorName, setErrorName] = useState<boolean>(false);
|
||||
const [errorPreview, setErrorPreview] = useState<string | null>(null);
|
||||
@@ -104,7 +97,7 @@ function VariableItem({
|
||||
]);
|
||||
|
||||
const handleSave = (): void => {
|
||||
const newVariableData: IDashboardVariable = {
|
||||
const variable: IDashboardVariable = {
|
||||
name: variableName,
|
||||
description: variableDescription,
|
||||
type: queryType,
|
||||
@@ -118,245 +111,277 @@ function VariableItem({
|
||||
selectedValue: (variableData.selectedValue ||
|
||||
variableTextboxValue) as never,
|
||||
}),
|
||||
modificationUUID: v4(),
|
||||
modificationUUID: generateUUID(),
|
||||
id: variableData.id || generateUUID(),
|
||||
order: variableData.order,
|
||||
};
|
||||
onSave(
|
||||
variableName,
|
||||
newVariableData,
|
||||
(variableViewMode === 'EDIT' && variableName !== variableData.name
|
||||
? variableData.name
|
||||
: '') as string,
|
||||
);
|
||||
onCancel();
|
||||
|
||||
onSave(mode, variable);
|
||||
};
|
||||
|
||||
// Fetches the preview values for the SQL variable query
|
||||
const handleQueryResult = async (): Promise<void> => {
|
||||
setPreviewLoading(true);
|
||||
setErrorPreview(null);
|
||||
try {
|
||||
const variableQueryResponse = await query({
|
||||
query: variableQueryValue,
|
||||
variables: variablePropsToPayloadVariables(existingVariables),
|
||||
});
|
||||
setPreviewLoading(false);
|
||||
if (variableQueryResponse.error) {
|
||||
let message = variableQueryResponse.error;
|
||||
if (variableQueryResponse.error.includes('Syntax error:')) {
|
||||
message =
|
||||
'Please make sure query is valid and dependent variables are selected';
|
||||
}
|
||||
setErrorPreview(message);
|
||||
return;
|
||||
}
|
||||
if (variableQueryResponse.payload?.variableValues)
|
||||
setPreviewValues(
|
||||
sortValues(
|
||||
variableQueryResponse.payload?.variableValues || [],
|
||||
variableSortType,
|
||||
) as never,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
const handleQueryResult = (response: any): void => {
|
||||
if (response?.payload?.variableValues)
|
||||
setPreviewValues(
|
||||
sortValues(
|
||||
response.payload?.variableValues || [],
|
||||
variableSortType,
|
||||
) as never,
|
||||
);
|
||||
};
|
||||
|
||||
const { isFetching: previewLoading, refetch: runQuery } = useQuery(
|
||||
[REACT_QUERY_KEY.DASHBOARD_BY_ID, variableData.name, variableName],
|
||||
{
|
||||
enabled: false,
|
||||
queryFn: () =>
|
||||
dashboardVariablesQuery({
|
||||
query: variableQueryValue || '',
|
||||
variables: variablePropsToPayloadVariables(existingVariables),
|
||||
}),
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (response) => {
|
||||
setErrorPreview(null);
|
||||
handleQueryResult(response);
|
||||
},
|
||||
onError: (error: {
|
||||
details: {
|
||||
error: string;
|
||||
};
|
||||
}) => {
|
||||
const { details } = error;
|
||||
|
||||
if (details.error) {
|
||||
let message = details.error;
|
||||
if (details.error.includes('Syntax error:')) {
|
||||
message =
|
||||
'Please make sure query is valid and dependent variables are selected';
|
||||
}
|
||||
setErrorPreview(message);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleTestRunQuery = useCallback(() => {
|
||||
runQuery();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Col>
|
||||
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Name</Typography>
|
||||
</LabelContainer>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Unique name of the variable"
|
||||
style={{ width: 400 }}
|
||||
value={variableName}
|
||||
onChange={(e): void => {
|
||||
setVariableName(e.target.value);
|
||||
setErrorName(
|
||||
!validateName(e.target.value) && e.target.value !== variableData.name,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="variable-item-container">
|
||||
<div className="variable-item-content">
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Name</Typography>
|
||||
</LabelContainer>
|
||||
<div>
|
||||
<Typography.Text type="warning">
|
||||
{errorName ? 'Variable name already exists' : ''}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</VariableItemRow>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Description</Typography>
|
||||
</LabelContainer>
|
||||
|
||||
<Input.TextArea
|
||||
value={variableDescription}
|
||||
placeholder="Write description of the variable"
|
||||
style={{ width: 400 }}
|
||||
onChange={(e): void => setVariableDescription(e.target.value)}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Type</Typography>
|
||||
</LabelContainer>
|
||||
|
||||
<Select
|
||||
defaultActiveFirstOption
|
||||
style={{ width: 400 }}
|
||||
onChange={(e: TVariableQueryType): void => {
|
||||
setQueryType(e);
|
||||
}}
|
||||
value={queryType}
|
||||
>
|
||||
<Option value={VariableQueryTypeArr[0]}>Query</Option>
|
||||
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
|
||||
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
|
||||
</Select>
|
||||
</VariableItemRow>
|
||||
<Typography.Title
|
||||
level={5}
|
||||
style={{ marginTop: '1rem', marginBottom: '1rem' }}
|
||||
>
|
||||
Options
|
||||
</Typography.Title>
|
||||
{queryType === 'QUERY' && (
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Query</Typography>
|
||||
</LabelContainer>
|
||||
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<Editor
|
||||
language="sql"
|
||||
value={variableQueryValue}
|
||||
onChange={(e): void => setVariableQueryValue(e)}
|
||||
height="300px"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleQueryResult}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
}}
|
||||
loading={previewLoading}
|
||||
>
|
||||
Test Run Query
|
||||
</Button>
|
||||
</div>
|
||||
</VariableItemRow>
|
||||
)}
|
||||
{queryType === 'CUSTOM' && (
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Values separated by comma</Typography>
|
||||
</LabelContainer>
|
||||
<Input.TextArea
|
||||
value={variableCustomValue}
|
||||
placeholder="1, 10, mykey, mykey:myvalue"
|
||||
style={{ width: 400 }}
|
||||
onChange={(e): void => {
|
||||
setVariableCustomValue(e.target.value);
|
||||
setPreviewValues(
|
||||
sortValues(
|
||||
commaValuesParser(e.target.value),
|
||||
variableSortType,
|
||||
) as never,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
)}
|
||||
{queryType === 'TEXTBOX' && (
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Default Value</Typography>
|
||||
</LabelContainer>
|
||||
<Input
|
||||
value={variableTextboxValue}
|
||||
onChange={(e): void => {
|
||||
setVariableTextboxValue(e.target.value);
|
||||
}}
|
||||
placeholder="Default value if any"
|
||||
style={{ width: 400 }}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
)}
|
||||
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
|
||||
<>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Preview of Values</Typography>
|
||||
</LabelContainer>
|
||||
<div style={{ flex: 1 }}>
|
||||
{errorPreview ? (
|
||||
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
|
||||
) : (
|
||||
map(previewValues, (value, idx) => (
|
||||
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</VariableItemRow>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Sort</Typography>
|
||||
</LabelContainer>
|
||||
|
||||
<Select
|
||||
defaultActiveFirstOption
|
||||
<Input
|
||||
placeholder="Unique name of the variable"
|
||||
style={{ width: 400 }}
|
||||
defaultValue={VariableSortTypeArr[0]}
|
||||
value={variableSortType}
|
||||
onChange={(value: TSortVariableValuesType): void =>
|
||||
setVariableSortType(value)
|
||||
}
|
||||
>
|
||||
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
|
||||
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
|
||||
<Option value={VariableSortTypeArr[2]}>Descending</Option>
|
||||
</Select>
|
||||
</VariableItemRow>
|
||||
value={variableName}
|
||||
onChange={(e): void => {
|
||||
setVariableName(e.target.value);
|
||||
setErrorName(
|
||||
!validateName(e.target.value) && e.target.value !== variableData.name,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Typography.Text type="warning">
|
||||
{errorName ? 'Variable name already exists' : ''}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</VariableItemRow>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Description</Typography>
|
||||
</LabelContainer>
|
||||
|
||||
<Input.TextArea
|
||||
value={variableDescription}
|
||||
placeholder="Write description of the variable"
|
||||
style={{ width: 400 }}
|
||||
onChange={(e): void => setVariableDescription(e.target.value)}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Type</Typography>
|
||||
</LabelContainer>
|
||||
|
||||
<Select
|
||||
defaultActiveFirstOption
|
||||
style={{ width: 400 }}
|
||||
onChange={(e: TVariableQueryType): void => {
|
||||
setQueryType(e);
|
||||
}}
|
||||
value={queryType}
|
||||
>
|
||||
<Option value={VariableQueryTypeArr[0]}>Query</Option>
|
||||
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
|
||||
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
|
||||
</Select>
|
||||
</VariableItemRow>
|
||||
<Typography.Title
|
||||
level={5}
|
||||
style={{ marginTop: '1rem', marginBottom: '1rem' }}
|
||||
>
|
||||
Options
|
||||
</Typography.Title>
|
||||
{queryType === 'QUERY' && (
|
||||
<div className="query-container">
|
||||
<LabelContainer>
|
||||
<Typography>Query</Typography>
|
||||
</LabelContainer>
|
||||
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<Editor
|
||||
language="sql"
|
||||
value={variableQueryValue}
|
||||
onChange={(e): void => setVariableQueryValue(e)}
|
||||
height="240px"
|
||||
options={{
|
||||
fontSize: 13,
|
||||
wordWrap: 'on',
|
||||
lineNumbers: 'off',
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 0,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={handleTestRunQuery}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
}}
|
||||
loading={previewLoading}
|
||||
>
|
||||
Test Run Query
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{queryType === 'CUSTOM' && (
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Enable multiple values to be checked</Typography>
|
||||
<Typography>Values separated by comma</Typography>
|
||||
</LabelContainer>
|
||||
<Switch
|
||||
checked={variableMultiSelect}
|
||||
<Input.TextArea
|
||||
value={variableCustomValue}
|
||||
placeholder="1, 10, mykey, mykey:myvalue"
|
||||
style={{ width: 400 }}
|
||||
onChange={(e): void => {
|
||||
setVariableMultiSelect(e);
|
||||
if (!e) {
|
||||
setVariableShowALLOption(false);
|
||||
}
|
||||
setVariableCustomValue(e.target.value);
|
||||
setPreviewValues(
|
||||
sortValues(
|
||||
commaValuesParser(e.target.value),
|
||||
variableSortType,
|
||||
) as never,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
{variableMultiSelect && (
|
||||
)}
|
||||
{queryType === 'TEXTBOX' && (
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Default Value</Typography>
|
||||
</LabelContainer>
|
||||
<Input
|
||||
value={variableTextboxValue}
|
||||
onChange={(e): void => {
|
||||
setVariableTextboxValue(e.target.value);
|
||||
}}
|
||||
placeholder="Default value if any"
|
||||
style={{ width: 400 }}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
)}
|
||||
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
|
||||
<>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Include an option for ALL values</Typography>
|
||||
<Typography>Preview of Values</Typography>
|
||||
</LabelContainer>
|
||||
<div style={{ flex: 1 }}>
|
||||
{errorPreview ? (
|
||||
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
|
||||
) : (
|
||||
map(previewValues, (value, idx) => (
|
||||
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</VariableItemRow>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Sort</Typography>
|
||||
</LabelContainer>
|
||||
|
||||
<Select
|
||||
defaultActiveFirstOption
|
||||
style={{ width: 400 }}
|
||||
defaultValue={VariableSortTypeArr[0]}
|
||||
value={variableSortType}
|
||||
onChange={(value: TSortVariableValuesType): void =>
|
||||
setVariableSortType(value)
|
||||
}
|
||||
>
|
||||
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
|
||||
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
|
||||
<Option value={VariableSortTypeArr[2]}>Descending</Option>
|
||||
</Select>
|
||||
</VariableItemRow>
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Enable multiple values to be checked</Typography>
|
||||
</LabelContainer>
|
||||
<Switch
|
||||
checked={variableShowALLOption}
|
||||
onChange={(e): void => setVariableShowALLOption(e)}
|
||||
checked={variableMultiSelect}
|
||||
onChange={(e): void => {
|
||||
setVariableMultiSelect(e);
|
||||
if (!e) {
|
||||
setVariableShowALLOption(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Divider />
|
||||
<VariableItemRow>
|
||||
<Button type="primary" onClick={handleSave} disabled={errorName}>
|
||||
Save
|
||||
</Button>
|
||||
<Button type="dashed" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</VariableItemRow>
|
||||
</Col>
|
||||
{variableMultiSelect && (
|
||||
<VariableItemRow>
|
||||
<LabelContainer>
|
||||
<Typography>Include an option for ALL values</Typography>
|
||||
</LabelContainer>
|
||||
<Switch
|
||||
checked={variableShowALLOption}
|
||||
onChange={(e): void => setVariableShowALLOption(e)}
|
||||
/>
|
||||
</VariableItemRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="variable-item-footer">
|
||||
<Divider />
|
||||
<VariableItemRow>
|
||||
<Button type="primary" onClick={handleSave} disabled={errorName}>
|
||||
Save
|
||||
</Button>
|
||||
<Button type="default" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</VariableItemRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,78 @@
|
||||
import '../DashboardSettings.styles.scss';
|
||||
|
||||
import { blue, red } from '@ant-design/colors';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal, Row, Space, Tag } from 'antd';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { MenuOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
|
||||
import { RowProps } from 'antd/lib';
|
||||
import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { PencilIcon, TrashIcon } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { TVariableViewMode } from './types';
|
||||
import { TVariableMode } from './types';
|
||||
import VariableItem from './VariableItem/VariableItem';
|
||||
|
||||
function TableRow({ children, ...props }: RowProps): JSX.Element {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
setActivatorNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
id: props['data-row-key'],
|
||||
});
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
...props.style,
|
||||
transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }),
|
||||
transition,
|
||||
...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
|
||||
};
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<tr {...props} ref={setNodeRef} style={style} {...attributes}>
|
||||
{React.Children.map(children, (child) => {
|
||||
if ((child as React.ReactElement).key === 'sort') {
|
||||
return React.cloneElement(child as React.ReactElement, {
|
||||
children: (
|
||||
<MenuOutlined
|
||||
ref={setActivatorNodeRef}
|
||||
style={{ touchAction: 'none', cursor: 'move' }}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...listeners}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function VariablesSetting(): JSX.Element {
|
||||
const variableToDelete = useRef<string | null>(null);
|
||||
const variableToDelete = useRef<IDashboardVariable | null>(null);
|
||||
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
@@ -24,16 +83,15 @@ function VariablesSetting(): JSX.Element {
|
||||
|
||||
const { variables = {} } = selectedDashboard?.data || {};
|
||||
|
||||
const variablesTableData = Object.keys(variables).map((variableName) => ({
|
||||
key: variableName,
|
||||
name: variableName,
|
||||
...variables[variableName],
|
||||
}));
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
const [variblesOrderArr, setVariablesOrderArr] = useState<number[]>([]);
|
||||
const [existingVariableNamesMap, setExistingVariableNamesMap] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
|
||||
const [
|
||||
variableViewMode,
|
||||
setVariableViewMode,
|
||||
] = useState<null | TVariableViewMode>(null);
|
||||
const [variableViewMode, setVariableViewMode] = useState<null | TVariableMode>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [
|
||||
variableEditData,
|
||||
@@ -46,7 +104,7 @@ function VariablesSetting(): JSX.Element {
|
||||
};
|
||||
|
||||
const onVariableViewModeEnter = (
|
||||
viewType: TVariableViewMode,
|
||||
viewType: TVariableMode,
|
||||
varData: IDashboardVariable,
|
||||
): void => {
|
||||
setVariableEditData(varData);
|
||||
@@ -55,6 +113,41 @@ function VariablesSetting(): JSX.Element {
|
||||
|
||||
const updateMutation = useUpdateDashboard();
|
||||
|
||||
useEffect(() => {
|
||||
const tableRowData = [];
|
||||
const variableOrderArr = [];
|
||||
const variableNamesMap = {};
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const { order, id, name } = value;
|
||||
|
||||
tableRowData.push({
|
||||
key,
|
||||
name: key,
|
||||
...variables[key],
|
||||
id,
|
||||
});
|
||||
|
||||
if (name) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
variableNamesMap[name] = name;
|
||||
}
|
||||
|
||||
if (order) {
|
||||
variableOrderArr.push(order);
|
||||
}
|
||||
}
|
||||
|
||||
tableRowData.sort((a, b) => a.order - b.order);
|
||||
variableOrderArr.sort((a, b) => a - b);
|
||||
|
||||
setVariablesTableData(tableRowData);
|
||||
setVariablesOrderArr(variableOrderArr);
|
||||
setExistingVariableNamesMap(variableNamesMap);
|
||||
}, [variables]);
|
||||
|
||||
const updateVariables = (
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
): void => {
|
||||
@@ -88,34 +181,58 @@ function VariablesSetting(): JSX.Element {
|
||||
);
|
||||
};
|
||||
|
||||
const getVariableOrder = (): number => {
|
||||
if (variblesOrderArr && variblesOrderArr.length > 0) {
|
||||
return variblesOrderArr[variblesOrderArr.length - 1] + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const onVariableSaveHandler = (
|
||||
name: string,
|
||||
mode: TVariableMode,
|
||||
variableData: IDashboardVariable,
|
||||
oldName: string,
|
||||
): void => {
|
||||
if (!variableData.name) {
|
||||
return;
|
||||
const updatedVariableData = {
|
||||
...variableData,
|
||||
order: variableData?.order >= 0 ? variableData.order : getVariableOrder(),
|
||||
};
|
||||
|
||||
const newVariablesArr = variablesTableData.map(
|
||||
(variable: IDashboardVariable) => {
|
||||
if (variable.id === updatedVariableData.id) {
|
||||
return updatedVariableData;
|
||||
}
|
||||
|
||||
return variable;
|
||||
},
|
||||
);
|
||||
|
||||
if (mode === 'ADD') {
|
||||
newVariablesArr.push(updatedVariableData);
|
||||
}
|
||||
|
||||
const newVariables = { ...variables };
|
||||
newVariables[name] = variableData;
|
||||
const variables = convertVariablesToDbFormat(newVariablesArr);
|
||||
|
||||
if (oldName) {
|
||||
delete newVariables[oldName];
|
||||
}
|
||||
updateVariables(newVariables);
|
||||
setVariablesTableData(newVariablesArr);
|
||||
updateVariables(variables);
|
||||
onDoneVariableViewMode();
|
||||
};
|
||||
|
||||
const onVariableDeleteHandler = (variableName: string): void => {
|
||||
variableToDelete.current = variableName;
|
||||
const onVariableDeleteHandler = (variable: IDashboardVariable): void => {
|
||||
variableToDelete.current = variable;
|
||||
setDeleteVariableModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = (): void => {
|
||||
const newVariables = { ...variables };
|
||||
if (variableToDelete?.current) delete newVariables[variableToDelete?.current];
|
||||
updateVariables(newVariables);
|
||||
const newVariablesArr = variablesTableData.filter(
|
||||
(variable: IDashboardVariable) =>
|
||||
variable.id !== variableToDelete?.current?.id,
|
||||
);
|
||||
|
||||
const updatedVariables = convertVariablesToDbFormat(newVariablesArr);
|
||||
|
||||
updateVariables(updatedVariables);
|
||||
variableToDelete.current = null;
|
||||
setDeleteVariableModal(false);
|
||||
};
|
||||
@@ -124,48 +241,100 @@ function VariablesSetting(): JSX.Element {
|
||||
setDeleteVariableModal(false);
|
||||
};
|
||||
|
||||
const validateVariableName = (name: string): boolean => !variables[name];
|
||||
const validateVariableName = (name: string): boolean =>
|
||||
!existingVariableNamesMap[name];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'sort',
|
||||
width: '10%',
|
||||
},
|
||||
{
|
||||
title: 'Variable',
|
||||
dataIndex: 'name',
|
||||
width: 100,
|
||||
width: '40%',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Definition',
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
width: 100,
|
||||
width: '35%',
|
||||
key: 'description',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
width: 50,
|
||||
width: '15%',
|
||||
key: 'action',
|
||||
render: (_: IDashboardVariable): JSX.Element => (
|
||||
render: (variable: IDashboardVariable): JSX.Element => (
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ padding: 0, cursor: 'pointer', color: blue[5] }}
|
||||
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
|
||||
style={{ padding: 8, cursor: 'pointer', color: blue[5] }}
|
||||
onClick={(): void => onVariableViewModeEnter('EDIT', variable)}
|
||||
>
|
||||
Edit
|
||||
<PencilIcon size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ padding: 0, color: red[6], cursor: 'pointer' }}
|
||||
style={{ padding: 8, color: red[6], cursor: 'pointer' }}
|
||||
onClick={(): void => {
|
||||
if (_.name) onVariableDeleteHandler(_.name);
|
||||
if (variable) {
|
||||
onVariableDeleteHandler(variable);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
<TrashIcon size={14} />
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
// https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
|
||||
distance: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const onDragEnd = ({ active, over }: DragEndEvent): void => {
|
||||
if (active.id !== over?.id) {
|
||||
const activeIndex = variablesTableData.findIndex(
|
||||
(i: { key: UniqueIdentifier }) => i.key === active.id,
|
||||
);
|
||||
const overIndex = variablesTableData.findIndex(
|
||||
(i: { key: UniqueIdentifier | undefined }) => i.key === over?.id,
|
||||
);
|
||||
|
||||
const updatedVariables: IDashboardVariable[] = arrayMove(
|
||||
variablesTableData,
|
||||
activeIndex,
|
||||
overIndex,
|
||||
);
|
||||
|
||||
const reArrangedVariables = {};
|
||||
|
||||
for (let index = 0; index < updatedVariables.length; index += 1) {
|
||||
const variableName = updatedVariables[index].name;
|
||||
|
||||
if (variableName) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
reArrangedVariables[variableName] = {
|
||||
...updatedVariables[index],
|
||||
order: index,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
updateVariables(reArrangedVariables);
|
||||
|
||||
setVariablesTableData(updatedVariables);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{variableViewMode ? (
|
||||
@@ -175,11 +344,17 @@ function VariablesSetting(): JSX.Element {
|
||||
onSave={onVariableSaveHandler}
|
||||
onCancel={onDoneVariableViewMode}
|
||||
validateName={validateVariableName}
|
||||
variableViewMode={variableViewMode}
|
||||
mode={variableViewMode}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Row style={{ flexDirection: 'row-reverse', padding: '0.5rem 0' }}>
|
||||
<Row
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '0.5rem 0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
data-testid="add-new-variable"
|
||||
type="primary"
|
||||
@@ -187,10 +362,32 @@ function VariablesSetting(): JSX.Element {
|
||||
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
|
||||
}
|
||||
>
|
||||
<PlusOutlined /> New Variables
|
||||
<PlusOutlined /> Add Variable
|
||||
</Button>
|
||||
</Row>
|
||||
<ResizeTable columns={columns} dataSource={variablesTableData} />
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
// rowKey array
|
||||
items={variablesTableData.map((variable: { key: any }) => variable.key)}
|
||||
>
|
||||
<Table
|
||||
components={{
|
||||
body: {
|
||||
row: TableRow,
|
||||
},
|
||||
}}
|
||||
rowKey="key"
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
dataSource={variablesTableData}
|
||||
/>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
@@ -200,8 +397,13 @@ function VariablesSetting(): JSX.Element {
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
>
|
||||
Are you sure you want to delete variable{' '}
|
||||
<Tag>{variableToDelete.current}</Tag>?
|
||||
<Typography.Text>
|
||||
Are you sure you want to delete variable{' '}
|
||||
<span className="delete-variable-name">
|
||||
{variableToDelete?.current?.name}
|
||||
</span>
|
||||
?
|
||||
</Typography.Text>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
export type TVariableViewMode = 'EDIT' | 'ADD';
|
||||
export type TVariableMode = 'VIEW' | 'EDIT' | 'ADD';
|
||||
|
||||
export const VariableModes = {
|
||||
VIEW: 'VIEW',
|
||||
EDIT: 'EDIT',
|
||||
ADD: 'ADD',
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ function DashboardSettingsContent(): JSX.Element {
|
||||
{ label: 'Variables', key: 'variables', children: <VariablesSetting /> },
|
||||
];
|
||||
|
||||
return <Tabs items={items} />;
|
||||
return <Tabs items={items} animated />;
|
||||
}
|
||||
|
||||
export default DashboardSettingsContent;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.variable-name {
|
||||
font-size: 0.8rem;
|
||||
min-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: gray;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Row } from 'antd';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { convertVariablesToDbFormat } from './util';
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
function DashboardVariableSelection(): JSX.Element | null {
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
const { variables } = data || {};
|
||||
|
||||
const [update, setUpdate] = useState<boolean>(false);
|
||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
useEffect(() => {
|
||||
if (variables) {
|
||||
const tableRowData = [];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const { id } = value;
|
||||
|
||||
tableRowData.push({
|
||||
key,
|
||||
name: key,
|
||||
...variables[key],
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
tableRowData.sort((a, b) => a.order - b.order);
|
||||
|
||||
setVariablesTableData(tableRowData);
|
||||
}
|
||||
}, [variables]);
|
||||
|
||||
const onVarChanged = (name: string): void => {
|
||||
setLastUpdatedVar(name);
|
||||
setUpdate(!update);
|
||||
};
|
||||
|
||||
const updateMutation = useUpdateDashboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const updateVariables = (
|
||||
name: string,
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
): void => {
|
||||
if (!selectedDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMutation.mutateAsync(
|
||||
{
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
variables: updatedVariablesData,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: `Error updating ${name} variable`,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onValueUpdate = (
|
||||
name: string,
|
||||
id: string,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
): void => {
|
||||
if (id) {
|
||||
const newVariablesArr = variablesTableData.map(
|
||||
(variable: IDashboardVariable) => {
|
||||
const variableCopy = { ...variable };
|
||||
|
||||
if (variableCopy.id === id) {
|
||||
variableCopy.selectedValue = value;
|
||||
variableCopy.allSelected = allSelected;
|
||||
}
|
||||
|
||||
return variableCopy;
|
||||
},
|
||||
);
|
||||
|
||||
const variables = convertVariablesToDbFormat(newVariablesArr);
|
||||
|
||||
if (role !== 'VIEWER' && selectedDashboard) {
|
||||
updateVariables(name, variables);
|
||||
}
|
||||
onVarChanged(name);
|
||||
|
||||
setUpdate(!update);
|
||||
}
|
||||
};
|
||||
|
||||
if (!variables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orderBasedSortedVariables = variablesTableData.sort(
|
||||
(a: { order: number }, b: { order: number }) => a.order - b.order,
|
||||
);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
{orderBasedSortedVariables &&
|
||||
Array.isArray(orderBasedSortedVariables) &&
|
||||
orderBasedSortedVariables.length > 0 &&
|
||||
orderBasedSortedVariables.map((variable) => (
|
||||
<VariableItem
|
||||
key={`${variable.name}${variable.id}}${variable.order}`}
|
||||
existingVariables={variables}
|
||||
lastUpdatedVar={lastUpdatedVar}
|
||||
variableData={{
|
||||
name: variable.name,
|
||||
...variable,
|
||||
change: update,
|
||||
}}
|
||||
onValueUpdate={onValueUpdate}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DashboardVariableSelection);
|
||||
@@ -1,12 +1,20 @@
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import React, { useEffect } from 'react';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
const mockVariableData: IDashboardVariable = {
|
||||
id: 'test_variable',
|
||||
description: 'Test Variable',
|
||||
type: 'TEXTBOX',
|
||||
textboxValue: 'defaultValue',
|
||||
@@ -25,7 +33,6 @@ const mockCustomVariableData: IDashboardVariable = {
|
||||
};
|
||||
|
||||
const mockOnValueUpdate = jest.fn();
|
||||
const mockOnAllSelectedUpdate = jest.fn();
|
||||
|
||||
describe('VariableItem', () => {
|
||||
let useEffectSpy: jest.SpyInstance;
|
||||
@@ -41,13 +48,14 @@ describe('VariableItem', () => {
|
||||
|
||||
test('renders component with default props', () => {
|
||||
render(
|
||||
<VariableItem
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>,
|
||||
<MockQueryClientProvider>
|
||||
<VariableItem
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('$testVariable')).toBeInTheDocument();
|
||||
@@ -55,45 +63,56 @@ describe('VariableItem', () => {
|
||||
|
||||
test('renders Input when the variable type is TEXTBOX', () => {
|
||||
render(
|
||||
<VariableItem
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>,
|
||||
<MockQueryClientProvider>
|
||||
<VariableItem
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onChange event handler when Input value changes', () => {
|
||||
test('calls onChange event handler when Input value changes', async () => {
|
||||
render(
|
||||
<VariableItem
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>,
|
||||
<MockQueryClientProvider>
|
||||
<VariableItem
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
const inputElement = screen.getByPlaceholderText('Enter value');
|
||||
fireEvent.change(inputElement, { target: { value: 'newValue' } });
|
||||
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledWith('testVariable', 'newValue');
|
||||
expect(mockOnAllSelectedUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnAllSelectedUpdate).toHaveBeenCalledWith('testVariable', false);
|
||||
act(() => {
|
||||
const inputElement = screen.getByPlaceholderText('Enter value');
|
||||
fireEvent.change(inputElement, { target: { value: 'newValue' } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||
'testVariable',
|
||||
'test_variable',
|
||||
'newValue',
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('renders a Select element when variable type is CUSTOM', () => {
|
||||
render(
|
||||
<VariableItem
|
||||
variableData={mockCustomVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>,
|
||||
<MockQueryClientProvider>
|
||||
<VariableItem
|
||||
variableData={mockCustomVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('$customVariable')).toBeInTheDocument();
|
||||
@@ -107,13 +126,14 @@ describe('VariableItem', () => {
|
||||
};
|
||||
|
||||
render(
|
||||
<VariableItem
|
||||
variableData={customVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>,
|
||||
<MockQueryClientProvider>
|
||||
<VariableItem
|
||||
variableData={customVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTitle('ALL')).toBeInTheDocument();
|
||||
@@ -121,48 +141,16 @@ describe('VariableItem', () => {
|
||||
|
||||
test('calls useEffect when the component mounts', () => {
|
||||
render(
|
||||
<VariableItem
|
||||
variableData={mockCustomVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>,
|
||||
<MockQueryClientProvider>
|
||||
<VariableItem
|
||||
variableData={mockCustomVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(useEffect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls useEffect only once when the component mounts', () => {
|
||||
// Render the component
|
||||
const { rerender } = render(
|
||||
<VariableItem
|
||||
variableData={mockCustomVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>,
|
||||
);
|
||||
|
||||
// Create an updated version of the mock data
|
||||
const updatedMockCustomVariableData = {
|
||||
...mockCustomVariableData,
|
||||
selectedValue: 'option1',
|
||||
};
|
||||
|
||||
// Re-render the component with the updated data
|
||||
rerender(
|
||||
<VariableItem
|
||||
variableData={updatedMockCustomVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
onAllSelectedUpdate={mockOnAllSelectedUpdate}
|
||||
lastUpdatedVar=""
|
||||
/>,
|
||||
);
|
||||
|
||||
// Check if the useEffect is called with the correct arguments
|
||||
expect(useEffectSpy).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import './DashboardVariableSelection.styles.scss';
|
||||
|
||||
import { orange } from '@ant-design/colors';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Input, Popover, Select, Typography } from 'antd';
|
||||
import query from 'api/dashboard/variables/query';
|
||||
import { Input, Popover, Select, Tooltip, Typography } from 'antd';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||
import map from 'lodash-es/map';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||
|
||||
import { variablePropsToPayloadVariables } from '../utils';
|
||||
import { SelectItemStyle, VariableContainer, VariableName } from './styles';
|
||||
import { SelectItemStyle, VariableContainer, VariableValue } from './styles';
|
||||
import { areArraysEqual } from './util';
|
||||
|
||||
const ALL_SELECT_VALUE = '__ALL__';
|
||||
|
||||
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
|
||||
|
||||
interface VariableItemProps {
|
||||
variableData: IDashboardVariable;
|
||||
existingVariables: Record<string, IDashboardVariable>;
|
||||
onValueUpdate: (
|
||||
name: string,
|
||||
id: string,
|
||||
arg1: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
) => void;
|
||||
onAllSelectedUpdate: (name: string, arg1: boolean) => void;
|
||||
lastUpdatedVar: string;
|
||||
}
|
||||
|
||||
@@ -38,48 +48,75 @@ function VariableItem({
|
||||
variableData,
|
||||
existingVariables,
|
||||
onValueUpdate,
|
||||
onAllSelectedUpdate,
|
||||
lastUpdatedVar,
|
||||
}: VariableItemProps): JSX.Element {
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||
[],
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [variableValue, setVaribleValue] = useState(
|
||||
variableData?.selectedValue?.toString() || '',
|
||||
);
|
||||
|
||||
const debouncedVariableValue = useDebounce(variableValue, 500);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
const getOptions = useCallback(async (): Promise<void> => {
|
||||
if (variableData.type === 'QUERY') {
|
||||
useEffect(() => {
|
||||
const { selectedValue } = variableData;
|
||||
|
||||
if (selectedValue) {
|
||||
setVaribleValue(selectedValue?.toString());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [variableData]);
|
||||
|
||||
const getDependentVariables = (queryValue: string): string[] => {
|
||||
const matches = queryValue.match(variableRegexPattern);
|
||||
|
||||
// Extract variable names from the matches array without {{ . }}
|
||||
return matches
|
||||
? matches.map((match) => match.replace(variableRegexPattern, '$1'))
|
||||
: [];
|
||||
};
|
||||
|
||||
const getQueryKey = (variableData: IDashboardVariable): string[] => {
|
||||
let dependentVariablesStr = '';
|
||||
|
||||
const dependentVariables = getDependentVariables(
|
||||
variableData.queryValue || '',
|
||||
);
|
||||
|
||||
const variableName = variableData.name || '';
|
||||
|
||||
dependentVariables?.forEach((element) => {
|
||||
dependentVariablesStr += `${element}${existingVariables[element]?.selectedValue}`;
|
||||
});
|
||||
|
||||
const variableKey = dependentVariablesStr.replace(/\s/g, '');
|
||||
|
||||
return [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableName, variableKey];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const getOptions = (variablesRes: VariableResponseProps | null): void => {
|
||||
if (variablesRes && variableData.type === 'QUERY') {
|
||||
try {
|
||||
setErrorMessage(null);
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await query({
|
||||
query: variableData.queryValue || '',
|
||||
variables: variablePropsToPayloadVariables(existingVariables),
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
if (response.error) {
|
||||
let message = response.error;
|
||||
if (response.error.includes('Syntax error:')) {
|
||||
message =
|
||||
'Please make sure query is valid and dependent variables are selected';
|
||||
}
|
||||
setErrorMessage(message);
|
||||
return;
|
||||
}
|
||||
if (response.payload?.variableValues) {
|
||||
if (
|
||||
variablesRes?.variableValues &&
|
||||
Array.isArray(variablesRes?.variableValues)
|
||||
) {
|
||||
const newOptionsData = sortValues(
|
||||
response.payload?.variableValues,
|
||||
variablesRes?.variableValues,
|
||||
variableData.sort,
|
||||
);
|
||||
// Since there is a chance of a variable being dependent on other
|
||||
// variables, we need to check if the optionsData has changed
|
||||
// If it has changed, we need to update the dependent variable
|
||||
// So we compare the new optionsData with the old optionsData
|
||||
|
||||
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
|
||||
|
||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||
/* eslint-disable no-useless-escape */
|
||||
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
|
||||
@@ -103,11 +140,12 @@ function VariableItem({
|
||||
} else {
|
||||
[value] = newOptionsData;
|
||||
}
|
||||
if (variableData.name) {
|
||||
onValueUpdate(variableData.name, value);
|
||||
onAllSelectedUpdate(variableData.name, allSelected);
|
||||
|
||||
if (variableData && variableData?.name && variableData?.id) {
|
||||
onValueUpdate(variableData.name, variableData.id, value, allSelected);
|
||||
}
|
||||
}
|
||||
|
||||
setOptionsData(newOptionsData);
|
||||
}
|
||||
}
|
||||
@@ -115,26 +153,43 @@ function VariableItem({
|
||||
console.error(e);
|
||||
}
|
||||
} else if (variableData.type === 'CUSTOM') {
|
||||
setOptionsData(
|
||||
sortValues(
|
||||
commaValuesParser(variableData.customValue || ''),
|
||||
variableData.sort,
|
||||
) as never,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
variableData,
|
||||
existingVariables,
|
||||
onValueUpdate,
|
||||
onAllSelectedUpdate,
|
||||
optionsData,
|
||||
lastUpdatedVar,
|
||||
]);
|
||||
const optionsData = sortValues(
|
||||
commaValuesParser(variableData.customValue || ''),
|
||||
variableData.sort,
|
||||
) as never;
|
||||
|
||||
useEffect(() => {
|
||||
getOptions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [variableData, existingVariables]);
|
||||
setOptionsData(optionsData);
|
||||
}
|
||||
};
|
||||
|
||||
const { isLoading } = useQuery(getQueryKey(variableData), {
|
||||
enabled: variableData && variableData.type === 'QUERY',
|
||||
queryFn: () =>
|
||||
dashboardVariablesQuery({
|
||||
query: variableData.queryValue || '',
|
||||
variables: variablePropsToPayloadVariables(existingVariables),
|
||||
}),
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (response) => {
|
||||
getOptions(response.payload);
|
||||
},
|
||||
onError: (error: {
|
||||
details: {
|
||||
error: string;
|
||||
};
|
||||
}) => {
|
||||
const { details } = error;
|
||||
|
||||
if (details.error) {
|
||||
let message = details.error;
|
||||
if (details.error.includes('Syntax error:')) {
|
||||
message =
|
||||
'Please make sure query is valid and dependent variables are selected';
|
||||
}
|
||||
setErrorMessage(message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = (value: string | string[]): void => {
|
||||
if (variableData.name)
|
||||
@@ -143,11 +198,9 @@ function VariableItem({
|
||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
||||
(Array.isArray(value) && value.length === 0)
|
||||
) {
|
||||
onValueUpdate(variableData.name, optionsData);
|
||||
onAllSelectedUpdate(variableData.name, true);
|
||||
onValueUpdate(variableData.name, variableData.id, optionsData, true);
|
||||
} else {
|
||||
onValueUpdate(variableData.name, value);
|
||||
onAllSelectedUpdate(variableData.name, false);
|
||||
onValueUpdate(variableData.name, variableData.id, value, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,62 +218,94 @@ function VariableItem({
|
||||
? 'multiple'
|
||||
: undefined;
|
||||
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedVariableValue !== variableData?.selectedValue?.toString()) {
|
||||
handleChange(debouncedVariableValue);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedVariableValue]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch options for CUSTOM Type
|
||||
if (variableData.type === 'CUSTOM') {
|
||||
getOptions(null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [variableData.type, variableData.customValue]);
|
||||
|
||||
return (
|
||||
<VariableContainer>
|
||||
<VariableName>${variableData.name}</VariableName>
|
||||
{variableData.type === 'TEXTBOX' ? (
|
||||
<Input
|
||||
placeholder="Enter value"
|
||||
bordered={false}
|
||||
value={variableData.selectedValue?.toString()}
|
||||
onChange={(e): void => {
|
||||
handleChange(e.target.value || '');
|
||||
}}
|
||||
style={{
|
||||
width:
|
||||
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
!errorMessage && (
|
||||
<Select
|
||||
value={selectValue}
|
||||
onChange={handleChange}
|
||||
bordered={false}
|
||||
placeholder="Select value"
|
||||
mode={mode}
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={SelectItemStyle}
|
||||
loading={isLoading}
|
||||
showArrow
|
||||
showSearch
|
||||
data-testid="variable-select"
|
||||
>
|
||||
{enableSelectAll && (
|
||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||
ALL
|
||||
</Select.Option>
|
||||
)}
|
||||
{map(optionsData, (option) => (
|
||||
<Select.Option
|
||||
data-testid={`option-${option}`}
|
||||
key={option.toString()}
|
||||
value={option}
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={isDashboardLocked ? 'Dashboard is locked' : ''}
|
||||
>
|
||||
<VariableContainer>
|
||||
<Typography.Text className="variable-name" ellipsis>
|
||||
${variableData.name}
|
||||
</Typography.Text>
|
||||
<VariableValue>
|
||||
{variableData.type === 'TEXTBOX' ? (
|
||||
<Input
|
||||
placeholder="Enter value"
|
||||
disabled={isDashboardLocked}
|
||||
bordered={false}
|
||||
value={variableValue}
|
||||
onChange={(e): void => {
|
||||
setVaribleValue(e.target.value || '');
|
||||
}}
|
||||
style={{
|
||||
width:
|
||||
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
!errorMessage &&
|
||||
optionsData && (
|
||||
<Select
|
||||
value={selectValue}
|
||||
onChange={handleChange}
|
||||
bordered={false}
|
||||
placeholder="Select value"
|
||||
mode={mode}
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={SelectItemStyle}
|
||||
loading={isLoading}
|
||||
showArrow
|
||||
showSearch
|
||||
data-testid="variable-select"
|
||||
disabled={isDashboardLocked}
|
||||
>
|
||||
{option.toString()}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
)}
|
||||
{errorMessage && (
|
||||
<span style={{ margin: '0 0.5rem' }}>
|
||||
<Popover placement="top" content={<Typography>{errorMessage}</Typography>}>
|
||||
<WarningOutlined style={{ color: orange[5] }} />
|
||||
</Popover>
|
||||
</span>
|
||||
)}
|
||||
</VariableContainer>
|
||||
{enableSelectAll && (
|
||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||
ALL
|
||||
</Select.Option>
|
||||
)}
|
||||
{map(optionsData, (option) => (
|
||||
<Select.Option
|
||||
data-testid={`option-${option}`}
|
||||
key={option.toString()}
|
||||
value={option}
|
||||
>
|
||||
{option.toString()}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
)}
|
||||
{variableData.type !== 'TEXTBOX' && errorMessage && (
|
||||
<span style={{ margin: '0 0.5rem' }}>
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<Typography>{errorMessage}</Typography>}
|
||||
>
|
||||
<WarningOutlined style={{ color: orange[5] }} />
|
||||
</Popover>
|
||||
</span>
|
||||
)}
|
||||
</VariableValue>
|
||||
</VariableContainer>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,117 +1,3 @@
|
||||
import { Row } from 'antd';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { map, sortBy } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardVariableSelection from './DashboardVariableSelection';
|
||||
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
function DashboardVariableSelection(): JSX.Element | null {
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
const { variables } = data || {};
|
||||
|
||||
const [update, setUpdate] = useState<boolean>(false);
|
||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const onVarChanged = (name: string): void => {
|
||||
setLastUpdatedVar(name);
|
||||
setUpdate(!update);
|
||||
};
|
||||
|
||||
const updateMutation = useUpdateDashboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const updateVariables = (
|
||||
updatedVariablesData: Dashboard['data']['variables'],
|
||||
): void => {
|
||||
if (!selectedDashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMutation.mutateAsync(
|
||||
{
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
variables: updatedVariablesData,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
notifications.success({
|
||||
message: 'Variable updated successfully',
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: 'Error while updating variable',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onValueUpdate = (
|
||||
name: string,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
): void => {
|
||||
const updatedVariablesData = { ...variables };
|
||||
updatedVariablesData[name].selectedValue = value;
|
||||
|
||||
if (role !== 'VIEWER' && selectedDashboard) {
|
||||
updateVariables(updatedVariablesData);
|
||||
}
|
||||
|
||||
onVarChanged(name);
|
||||
};
|
||||
const onAllSelectedUpdate = (
|
||||
name: string,
|
||||
value: IDashboardVariable['allSelected'],
|
||||
): void => {
|
||||
const updatedVariablesData = { ...variables };
|
||||
updatedVariablesData[name].allSelected = value;
|
||||
|
||||
if (role !== 'VIEWER') {
|
||||
updateVariables(updatedVariablesData);
|
||||
}
|
||||
onVarChanged(name);
|
||||
};
|
||||
|
||||
if (!variables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
{map(sortBy(Object.keys(variables)), (variableName) => (
|
||||
<VariableItem
|
||||
key={`${variableName}${variables[variableName].modificationUUID}`}
|
||||
existingVariables={variables}
|
||||
variableData={{
|
||||
name: variableName,
|
||||
...variables[variableName],
|
||||
change: update,
|
||||
}}
|
||||
onValueUpdate={onValueUpdate}
|
||||
onAllSelectedUpdate={onAllSelectedUpdate}
|
||||
lastUpdatedVar={lastUpdatedVar}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DashboardVariableSelection);
|
||||
export default DashboardVariableSelection;
|
||||
|
||||
@@ -3,19 +3,40 @@ import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const VariableContainer = styled.div`
|
||||
max-width: 100%;
|
||||
border: 1px solid ${grey[1]}66;
|
||||
border-radius: 2px;
|
||||
padding: 0;
|
||||
padding-left: 0.5rem;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3rem;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
`;
|
||||
|
||||
export const VariableName = styled(Typography)`
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
color: ${grey[0]};
|
||||
|
||||
min-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const VariableValue = styled(Typography)`
|
||||
font-size: 0.8rem;
|
||||
color: ${grey[0]};
|
||||
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
export const SelectItemStyle = {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function areArraysEqual(
|
||||
a: (string | number | boolean)[],
|
||||
b: (string | number | boolean)[],
|
||||
@@ -14,3 +16,16 @@ export function areArraysEqual(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const convertVariablesToDbFormat = (
|
||||
variblesArr: IDashboardVariable[],
|
||||
): Dashboard['data']['variables'] =>
|
||||
variblesArr.reduce((result, obj: IDashboardVariable) => {
|
||||
const { id } = obj;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result[id] = obj;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
@@ -6,8 +6,10 @@ export function variablePropsToPayloadVariables(
|
||||
): PayloadVariables {
|
||||
const payloadVariables: PayloadVariables = {};
|
||||
|
||||
Object.entries(variables).forEach(([key, value]) => {
|
||||
payloadVariables[key] = value?.selectedValue;
|
||||
Object.entries(variables).forEach(([, value]) => {
|
||||
if (value?.name) {
|
||||
payloadVariables[value.name] = value?.selectedValue;
|
||||
}
|
||||
});
|
||||
|
||||
return payloadVariables;
|
||||
|
||||
@@ -6,13 +6,16 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
function WidgetGraph({
|
||||
getWidgetQueryRange,
|
||||
@@ -23,6 +26,21 @@ function WidgetGraph({
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(getWidgetQueryRange);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [getWidgetQueryRange, maxTime, minTime, globalSelectedInterval]);
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
@@ -63,6 +81,8 @@ function WidgetGraph({
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
}),
|
||||
[
|
||||
widgetId,
|
||||
@@ -73,6 +93,8 @@ function WidgetGraph({
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ export const Container = styled(Card)<Props>`
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: ${({ $panelType }): string =>
|
||||
$panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'};
|
||||
padding: 8px;
|
||||
height: 57vh;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
CategoryNames,
|
||||
DataFormats,
|
||||
DataRateFormats,
|
||||
HelperCategory,
|
||||
HelperFormat,
|
||||
MiscellaneousFormats,
|
||||
ThroughputFormats,
|
||||
TimeFormats,
|
||||
@@ -76,6 +78,7 @@ export const alertsCategory = [
|
||||
name: CategoryNames.Miscellaneous,
|
||||
formats: [
|
||||
{ name: 'Percent (0.0-1.0)', id: MiscellaneousFormats.PercentUnit },
|
||||
{ name: 'Percent (0 - 100)', id: MiscellaneousFormats.Percent },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -119,3 +122,10 @@ export const getCategoryByOptionId = (id: string): Category | undefined =>
|
||||
|
||||
export const isCategoryName = (name: string): name is CategoryNames =>
|
||||
alertsCategory.some((category) => category.name === name);
|
||||
|
||||
const allFormats: HelperFormat[] = alertsCategory.flatMap(
|
||||
(category: HelperCategory) => category.formats,
|
||||
);
|
||||
|
||||
export const getFormatNameByOptionId = (id: string): string | undefined =>
|
||||
allFormats.find((format) => format.id === id)?.name;
|
||||
|
||||
@@ -107,48 +107,8 @@ function RightContainer({
|
||||
}
|
||||
/>
|
||||
|
||||
{/* <TextContainer>
|
||||
<Typography>Stacked Graphs :</Typography>
|
||||
<Switch
|
||||
checked={stacked}
|
||||
onChange={(): void => {
|
||||
setStacked((value) => !value);
|
||||
}}
|
||||
/>
|
||||
</TextContainer> */}
|
||||
|
||||
{/* <Title light={'true'}>Fill Opacity: </Title> */}
|
||||
|
||||
{/* <Slider
|
||||
value={parseInt(opacity, 10)}
|
||||
marks={{
|
||||
0: '0',
|
||||
33: '33',
|
||||
66: '66',
|
||||
100: '100',
|
||||
}}
|
||||
onChange={(number): void => onChangeHandler(setOpacity, number.toString())}
|
||||
step={1}
|
||||
/> */}
|
||||
|
||||
{/* <Title light={'true'}>Null/Zero values: </Title>
|
||||
|
||||
<NullButtonContainer>
|
||||
{nullValueButtons.map((button) => (
|
||||
<Button
|
||||
type={button.check === selectedNullZeroValue ? 'primary' : 'default'}
|
||||
key={button.name}
|
||||
onClick={(): void =>
|
||||
onChangeHandler(setSelectedNullZeroValue, button.check)
|
||||
}
|
||||
>
|
||||
{button.name}
|
||||
</Button>
|
||||
))}
|
||||
</NullButtonContainer> */}
|
||||
|
||||
<Space style={{ marginTop: 10 }} direction="vertical">
|
||||
<Typography>Fill span gaps</Typography>
|
||||
<Typography>Fill gaps</Typography>
|
||||
|
||||
<Switch
|
||||
checked={isFillSpans}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user