mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-18 02:32:13 +00:00
Compare commits
28 Commits
debug_time
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ec91fcc19 | ||
|
|
53e46a3a2c | ||
|
|
756c3274ff | ||
|
|
05cd026983 | ||
|
|
1770ded9df | ||
|
|
24b72084ac | ||
|
|
2db83b453d | ||
|
|
2f012715b4 | ||
|
|
aa05a7bf14 | ||
|
|
99327960b0 | ||
|
|
12b02a1002 | ||
|
|
4ce220ba92 | ||
|
|
0211ddf0cb | ||
|
|
e5eb62e45b | ||
|
|
7371dcacf0 | ||
|
|
3cdf3e06f3 | ||
|
|
f8c38df2bf | ||
|
|
cab4a56694 | ||
|
|
78041fe457 | ||
|
|
09b6382820 | ||
|
|
9689b847f0 | ||
|
|
15e5938e95 | ||
|
|
c5ef455283 | ||
|
|
2316b5be83 | ||
|
|
937ebc1582 | ||
|
|
dcc8173c79 | ||
|
|
4b4ef5ce58 | ||
|
|
5b8d5fbfd3 |
19
.github/CODEOWNERS
vendored
19
.github/CODEOWNERS
vendored
@@ -1,8 +1,6 @@
|
|||||||
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
|
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
|
||||||
|
|
||||||
# Owners are automatically requested for review for PRs that changes code
|
# Owners are automatically requested for review for PRs that changes code that they own.
|
||||||
|
|
||||||
# that they own.
|
|
||||||
|
|
||||||
/frontend/ @SigNoz/frontend-maintainers
|
/frontend/ @SigNoz/frontend-maintainers
|
||||||
|
|
||||||
@@ -11,8 +9,10 @@
|
|||||||
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
|
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
|
||||||
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
|
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
|
||||||
|
|
||||||
/deploy/ @SigNoz/devops
|
# CI
|
||||||
.github @SigNoz/devops
|
/deploy/ @therealpandey
|
||||||
|
.github @therealpandey
|
||||||
|
go.mod @therealpandey
|
||||||
|
|
||||||
# Scaffold Owners
|
# Scaffold Owners
|
||||||
|
|
||||||
@@ -105,6 +105,10 @@
|
|||||||
/pkg/modules/authdomain/ @vikrantgupta25
|
/pkg/modules/authdomain/ @vikrantgupta25
|
||||||
/pkg/modules/role/ @vikrantgupta25
|
/pkg/modules/role/ @vikrantgupta25
|
||||||
|
|
||||||
|
# IdentN Owners
|
||||||
|
/pkg/identn/ @vikrantgupta25
|
||||||
|
/pkg/http/middleware/identn.go @vikrantgupta25
|
||||||
|
|
||||||
# Integration tests
|
# Integration tests
|
||||||
|
|
||||||
/tests/integration/ @vikrantgupta25
|
/tests/integration/ @vikrantgupta25
|
||||||
@@ -127,12 +131,15 @@
|
|||||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||||
|
|
||||||
|
# Dashboard Widget Page
|
||||||
|
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
|
||||||
|
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||||
|
|
||||||
## Dashboard Page
|
## Dashboard Page
|
||||||
|
|
||||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
|
||||||
|
|
||||||
## Public Dashboard Page
|
## Public Dashboard Page
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/goci.yaml
vendored
10
.github/workflows/goci.yaml
vendored
@@ -102,13 +102,3 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
go run cmd/enterprise/*.go generate openapi
|
go run cmd/enterprise/*.go generate openapi
|
||||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
|
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
|
||||||
- name: node-install
|
|
||||||
uses: actions/setup-node@v5
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
- name: install-frontend
|
|
||||||
run: cd frontend && yarn install
|
|
||||||
- name: generate-api-clients
|
|
||||||
run: |
|
|
||||||
cd frontend && yarn generate:api
|
|
||||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run yarn generate:api in frontend/ locally and commit."; exit 1)
|
|
||||||
|
|||||||
51
.github/workflows/jsci.yaml
vendored
51
.github/workflows/jsci.yaml
vendored
@@ -52,16 +52,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
PRIMUS_REF: main
|
PRIMUS_REF: main
|
||||||
JS_SRC: frontend
|
JS_SRC: frontend
|
||||||
md-languages:
|
languages:
|
||||||
if: |
|
if: |
|
||||||
github.event_name == 'merge_group' ||
|
github.event_name == 'merge_group' ||
|
||||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: self-checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: validate md languages
|
- name: run
|
||||||
run: bash frontend/scripts/validate-md-languages.sh
|
run: bash frontend/scripts/validate-md-languages.sh
|
||||||
authz:
|
authz:
|
||||||
if: |
|
if: |
|
||||||
@@ -70,44 +70,55 @@ jobs:
|
|||||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: self-checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
- name: node-install
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
|
- name: deps-install
|
||||||
- name: Install frontend dependencies
|
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
yarn install
|
||||||
|
- name: uv-install
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v5
|
||||||
|
- name: uv-deps
|
||||||
- name: Install Python dependencies
|
|
||||||
working-directory: ./tests/integration
|
working-directory: ./tests/integration
|
||||||
run: |
|
run: |
|
||||||
uv sync
|
uv sync
|
||||||
|
- name: setup-test
|
||||||
- name: Start test environment
|
|
||||||
run: |
|
run: |
|
||||||
make py-test-setup
|
make py-test-setup
|
||||||
|
- name: generate
|
||||||
- name: Generate permissions.type.ts
|
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: |
|
run: |
|
||||||
yarn generate:permissions-type
|
yarn generate:permissions-type
|
||||||
|
- name: teardown-test
|
||||||
- name: Teardown test environment
|
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
make py-test-teardown
|
make py-test-teardown
|
||||||
|
- name: validate
|
||||||
- name: Check for changes
|
|
||||||
run: |
|
run: |
|
||||||
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
|
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
|
||||||
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
|
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
openapi:
|
||||||
|
if: |
|
||||||
|
github.event_name == 'merge_group' ||
|
||||||
|
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||||
|
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: self-checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: node-install
|
||||||
|
uses: actions/setup-node@v5
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
- name: install-frontend
|
||||||
|
run: cd frontend && yarn install
|
||||||
|
- name: generate-api-clients
|
||||||
|
run: |
|
||||||
|
cd frontend && yarn generate:api
|
||||||
|
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run yarn generate:api in frontend/ locally and commit."; exit 1)
|
||||||
|
|||||||
60
.github/workflows/mergequeueci.yaml
vendored
Normal file
60
.github/workflows/mergequeueci.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: mergequeueci
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- dequeued
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.merged == false
|
||||||
|
steps:
|
||||||
|
- name: alert
|
||||||
|
uses: slackapi/slack-github-action@v2.1.1
|
||||||
|
with:
|
||||||
|
webhook: ${{ secrets.SLACK_MERGE_QUEUE_WEBHOOK }}
|
||||||
|
webhook-type: incoming-webhook
|
||||||
|
payload: |
|
||||||
|
{
|
||||||
|
"text": ":x: PR removed from merge queue",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "header",
|
||||||
|
"text": {
|
||||||
|
"type": "plain_text",
|
||||||
|
"text": ":x: PR Removed from Merge Queue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "*<${{ github.event.pull_request.html_url }}|PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}>*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "divider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": "*Author*\n@${{ github.event.pull_request.user.login }}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
- name: comment
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
run: |
|
||||||
|
gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments \
|
||||||
|
-f body="> :x: **PR removed from merge queue**
|
||||||
|
>
|
||||||
|
> @$PR_AUTHOR your PR was removed from the merge queue. Fix the issue and re-queue when ready."
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/version"
|
"github.com/SigNoz/signoz/pkg/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.uber.org/zap" //nolint:depguard
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
var RootCmd = &cobra.Command{
|
||||||
@@ -19,12 +18,6 @@ var RootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Execute(logger *slog.Logger) {
|
func Execute(logger *slog.Logger) {
|
||||||
zapLogger := newZapLogger()
|
|
||||||
zap.ReplaceGlobals(zapLogger)
|
|
||||||
defer func() {
|
|
||||||
_ = zapLogger.Sync()
|
|
||||||
}()
|
|
||||||
|
|
||||||
err := RootCmd.Execute()
|
err := RootCmd.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(RootCmd.Context(), "error running command", "error", err)
|
logger.ErrorContext(RootCmd.Context(), "error running command", "error", err)
|
||||||
|
|||||||
110
cmd/zap.go
110
cmd/zap.go
@@ -1,110 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"go.uber.org/zap" //nolint:depguard
|
|
||||||
"go.uber.org/zap/zapcore" //nolint:depguard
|
|
||||||
)
|
|
||||||
|
|
||||||
// Deprecated: Use `NewLogger` from `pkg/instrumentation` instead.
|
|
||||||
func newZapLogger() *zap.Logger {
|
|
||||||
config := zap.NewProductionConfig()
|
|
||||||
config.EncoderConfig.TimeKey = "timestamp"
|
|
||||||
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
|
||||||
|
|
||||||
// Extract sampling config before building the logger.
|
|
||||||
// We need to disable sampling in the config and apply it manually later
|
|
||||||
// to ensure correct core ordering. See filteringCore documentation for details.
|
|
||||||
samplerConfig := config.Sampling
|
|
||||||
config.Sampling = nil
|
|
||||||
|
|
||||||
logger, _ := config.Build()
|
|
||||||
|
|
||||||
// Wrap with custom core wrapping to filter certain log entries.
|
|
||||||
// The order of wrapping is important:
|
|
||||||
// 1. First wrap with filteringCore
|
|
||||||
// 2. Then wrap with sampler
|
|
||||||
//
|
|
||||||
// This creates the call chain: sampler -> filteringCore -> ioCore
|
|
||||||
//
|
|
||||||
// During logging:
|
|
||||||
// - sampler.Check decides whether to sample the log entry
|
|
||||||
// - If sampled, filteringCore.Check is called
|
|
||||||
// - filteringCore adds itself to CheckedEntry.cores
|
|
||||||
// - All cores in CheckedEntry.cores have their Write method called
|
|
||||||
// - filteringCore.Write can now filter the entry before passing to ioCore
|
|
||||||
//
|
|
||||||
// If we didn't disable the sampler above, filteringCore would have wrapped
|
|
||||||
// sampler. By calling sampler.Check we would have allowed it to call
|
|
||||||
// ioCore.Check that adds itself to CheckedEntry.cores. Then ioCore.Write
|
|
||||||
// would have bypassed our checks, making filtering impossible.
|
|
||||||
return logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
|
||||||
core = &filteringCore{core}
|
|
||||||
if samplerConfig != nil {
|
|
||||||
core = zapcore.NewSamplerWithOptions(
|
|
||||||
core,
|
|
||||||
time.Second,
|
|
||||||
samplerConfig.Initial,
|
|
||||||
samplerConfig.Thereafter,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return core
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// filteringCore wraps a zapcore.Core to filter out log entries based on a
|
|
||||||
// custom logic.
|
|
||||||
//
|
|
||||||
// Note: This core must be positioned before the sampler in the core chain
|
|
||||||
// to ensure Write is called. See newZapLogger for ordering details.
|
|
||||||
type filteringCore struct {
|
|
||||||
zapcore.Core
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter determines whether a log entry should be written based on its fields.
|
|
||||||
// Returns false if the entry should be suppressed, true otherwise.
|
|
||||||
//
|
|
||||||
// Current filters:
|
|
||||||
// - context.Canceled: These are expected errors from cancelled operations,
|
|
||||||
// and create noise in logs.
|
|
||||||
func (c *filteringCore) filter(fields []zapcore.Field) bool {
|
|
||||||
for _, field := range fields {
|
|
||||||
if field.Type == zapcore.ErrorType {
|
|
||||||
if loggedErr, ok := field.Interface.(error); ok {
|
|
||||||
// Suppress logs containing context.Canceled errors
|
|
||||||
if errors.Is(loggedErr, context.Canceled) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// With implements zapcore.Core.With
|
|
||||||
// It returns a new copy with the added context.
|
|
||||||
func (c *filteringCore) With(fields []zapcore.Field) zapcore.Core {
|
|
||||||
return &filteringCore{c.Core.With(fields)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check implements zapcore.Core.Check.
|
|
||||||
// It adds this core to the CheckedEntry if the log level is enabled,
|
|
||||||
// ensuring that Write will be called for this entry.
|
|
||||||
func (c *filteringCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
|
||||||
if c.Enabled(ent.Level) {
|
|
||||||
return ce.AddCore(ent, c)
|
|
||||||
}
|
|
||||||
return ce
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write implements zapcore.Core.Write.
|
|
||||||
// It filters log entries based on their fields before delegating to the wrapped core.
|
|
||||||
func (c *filteringCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
|
|
||||||
if !c.filter(fields) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.Core.Write(ent, fields)
|
|
||||||
}
|
|
||||||
@@ -321,3 +321,19 @@ user:
|
|||||||
org:
|
org:
|
||||||
name: default
|
name: default
|
||||||
id: 00000000-0000-0000-0000-000000000000
|
id: 00000000-0000-0000-0000-000000000000
|
||||||
|
|
||||||
|
##################### IdentN #####################
|
||||||
|
identn:
|
||||||
|
tokenizer:
|
||||||
|
# toggle the identN resolver
|
||||||
|
enabled: true
|
||||||
|
# headers to use for tokenizer identN resolver
|
||||||
|
headers:
|
||||||
|
- Authorization
|
||||||
|
- Sec-WebSocket-Protocol
|
||||||
|
apikey:
|
||||||
|
# toggle the identN resolver
|
||||||
|
enabled: true
|
||||||
|
# headers to use for apikey identN resolver
|
||||||
|
headers:
|
||||||
|
- SIGNOZ-API-KEY
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
x-common: &common
|
|
||||||
networks:
|
|
||||||
- signoz-net
|
|
||||||
extra_hosts:
|
|
||||||
- host.docker.internal:host-gateway
|
|
||||||
logging:
|
|
||||||
options:
|
|
||||||
max-size: 50m
|
|
||||||
max-file: "3"
|
|
||||||
deploy:
|
|
||||||
restart_policy:
|
|
||||||
condition: on-failure
|
|
||||||
services:
|
|
||||||
hotrod:
|
|
||||||
<<: *common
|
|
||||||
image: jaegertracing/example-hotrod:1.61.0
|
|
||||||
command: [ "all" ]
|
|
||||||
environment:
|
|
||||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 #
|
|
||||||
load-hotrod:
|
|
||||||
<<: *common
|
|
||||||
image: "signoz/locust:1.2.3"
|
|
||||||
environment:
|
|
||||||
ATTACKED_HOST: http://hotrod:8080
|
|
||||||
LOCUST_MODE: standalone
|
|
||||||
NO_PROXY: standalone
|
|
||||||
TASK_DELAY_FROM: 5
|
|
||||||
TASK_DELAY_TO: 30
|
|
||||||
QUIET_MODE: "${QUIET_MODE:-false}"
|
|
||||||
LOCUST_OPTS: "--headless -u 10 -r 1"
|
|
||||||
volumes:
|
|
||||||
- ../../../common/locust-scripts:/locust
|
|
||||||
|
|
||||||
networks:
|
|
||||||
signoz-net:
|
|
||||||
name: signoz-net
|
|
||||||
external: true
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
x-common: &common
|
|
||||||
networks:
|
|
||||||
- signoz-net
|
|
||||||
extra_hosts:
|
|
||||||
- host.docker.internal:host-gateway
|
|
||||||
logging:
|
|
||||||
options:
|
|
||||||
max-size: 50m
|
|
||||||
max-file: "3"
|
|
||||||
deploy:
|
|
||||||
mode: global
|
|
||||||
restart_policy:
|
|
||||||
condition: on-failure
|
|
||||||
services:
|
|
||||||
otel-agent:
|
|
||||||
<<: *common
|
|
||||||
image: otel/opentelemetry-collector-contrib:0.111.0
|
|
||||||
command:
|
|
||||||
- --config=/etc/otel-collector-config.yaml
|
|
||||||
volumes:
|
|
||||||
- ./otel-agent-config.yaml:/etc/otel-collector-config.yaml
|
|
||||||
- /:/hostfs:ro
|
|
||||||
environment:
|
|
||||||
- SIGNOZ_COLLECTOR_ENDPOINT=http://host.docker.internal:4317 # In case of external SigNoz or cloud, update the endpoint and access token
|
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
|
|
||||||
# - SIGNOZ_ACCESS_TOKEN="<your-access-token>"
|
|
||||||
# Before exposing the ports, make sure the ports are not used by other services
|
|
||||||
# ports:
|
|
||||||
# - "4317:4317"
|
|
||||||
# - "4318:4318"
|
|
||||||
otel-metrics:
|
|
||||||
<<: *common
|
|
||||||
image: otel/opentelemetry-collector-contrib:0.111.0
|
|
||||||
user: 0:0 # If you have security concerns, you can replace this with your `UID:GID` that has necessary permissions to docker.sock
|
|
||||||
command:
|
|
||||||
- --config=/etc/otel-collector-config.yaml
|
|
||||||
volumes:
|
|
||||||
- ./otel-metrics-config.yaml:/etc/otel-collector-config.yaml
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
environment:
|
|
||||||
- SIGNOZ_COLLECTOR_ENDPOINT=http://host.docker.internal:4317 # In case of external SigNoz or cloud, update the endpoint and access token
|
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
|
|
||||||
# - SIGNOZ_ACCESS_TOKEN="<your-access-token>"
|
|
||||||
# Before exposing the ports, make sure the ports are not used by other services
|
|
||||||
# ports:
|
|
||||||
# - "4317:4317"
|
|
||||||
# - "4318:4318"
|
|
||||||
deploy:
|
|
||||||
mode: replicated
|
|
||||||
replicas: 1
|
|
||||||
placement:
|
|
||||||
constraints:
|
|
||||||
- node.role == manager
|
|
||||||
logspout:
|
|
||||||
<<: *common
|
|
||||||
image: "gliderlabs/logspout:v3.2.14"
|
|
||||||
command: syslog+tcp://otel-agent:2255
|
|
||||||
user: root
|
|
||||||
volumes:
|
|
||||||
- /etc/hostname:/etc/host_hostname:ro
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
depends_on:
|
|
||||||
- otel-agent
|
|
||||||
|
|
||||||
networks:
|
|
||||||
signoz-net:
|
|
||||||
name: signoz-net
|
|
||||||
external: true
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
receivers:
|
|
||||||
hostmetrics:
|
|
||||||
collection_interval: 30s
|
|
||||||
root_path: /hostfs
|
|
||||||
scrapers:
|
|
||||||
cpu: {}
|
|
||||||
load: {}
|
|
||||||
memory: {}
|
|
||||||
disk: {}
|
|
||||||
filesystem: {}
|
|
||||||
network: {}
|
|
||||||
otlp:
|
|
||||||
protocols:
|
|
||||||
grpc:
|
|
||||||
endpoint: 0.0.0.0:4317
|
|
||||||
http:
|
|
||||||
endpoint: 0.0.0.0:4318
|
|
||||||
prometheus:
|
|
||||||
config:
|
|
||||||
global:
|
|
||||||
scrape_interval: 60s
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: otel-agent
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- localhost:8888
|
|
||||||
labels:
|
|
||||||
job_name: otel-agent
|
|
||||||
tcplog/docker:
|
|
||||||
listen_address: "0.0.0.0:2255"
|
|
||||||
operators:
|
|
||||||
- type: regex_parser
|
|
||||||
regex: '^<([0-9]+)>[0-9]+ (?P<timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?) (?P<container_id>\S+) (?P<container_name>\S+) [0-9]+ - -( (?P<body>.*))?'
|
|
||||||
timestamp:
|
|
||||||
parse_from: attributes.timestamp
|
|
||||||
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
|
||||||
- type: move
|
|
||||||
from: attributes["body"]
|
|
||||||
to: body
|
|
||||||
- type: remove
|
|
||||||
field: attributes.timestamp
|
|
||||||
# please remove names from below if you want to collect logs from them
|
|
||||||
- type: filter
|
|
||||||
id: signoz_logs_filter
|
|
||||||
expr: 'attributes.container_name matches "^(signoz_(logspout|signoz|otel-collector|clickhouse|zookeeper))|(infra_(logspout|otel-agent|otel-metrics)).*"'
|
|
||||||
processors:
|
|
||||||
batch:
|
|
||||||
send_batch_size: 10000
|
|
||||||
send_batch_max_size: 11000
|
|
||||||
timeout: 10s
|
|
||||||
resourcedetection:
|
|
||||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
|
||||||
detectors:
|
|
||||||
# - ec2
|
|
||||||
# - gcp
|
|
||||||
# - azure
|
|
||||||
- env
|
|
||||||
- system
|
|
||||||
timeout: 2s
|
|
||||||
extensions:
|
|
||||||
health_check:
|
|
||||||
endpoint: 0.0.0.0:13133
|
|
||||||
pprof:
|
|
||||||
endpoint: 0.0.0.0:1777
|
|
||||||
exporters:
|
|
||||||
otlp:
|
|
||||||
endpoint: ${env:SIGNOZ_COLLECTOR_ENDPOINT}
|
|
||||||
tls:
|
|
||||||
insecure: true
|
|
||||||
headers:
|
|
||||||
signoz-access-token: ${env:SIGNOZ_ACCESS_TOKEN}
|
|
||||||
# debug: {}
|
|
||||||
service:
|
|
||||||
telemetry:
|
|
||||||
logs:
|
|
||||||
encoding: json
|
|
||||||
metrics:
|
|
||||||
address: 0.0.0.0:8888
|
|
||||||
extensions:
|
|
||||||
- health_check
|
|
||||||
- pprof
|
|
||||||
pipelines:
|
|
||||||
traces:
|
|
||||||
receivers: [otlp]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
metrics:
|
|
||||||
receivers: [otlp]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
metrics/hostmetrics:
|
|
||||||
receivers: [hostmetrics]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
metrics/prometheus:
|
|
||||||
receivers: [prometheus]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
logs:
|
|
||||||
receivers: [otlp, tcplog/docker]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
receivers:
|
|
||||||
prometheus:
|
|
||||||
config:
|
|
||||||
global:
|
|
||||||
scrape_interval: 60s
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: otel-metrics
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- localhost:8888
|
|
||||||
labels:
|
|
||||||
job_name: otel-metrics
|
|
||||||
# For Docker daemon metrics to be scraped, it must be configured to expose
|
|
||||||
# Prometheus metrics, as documented here: https://docs.docker.com/config/daemon/prometheus/
|
|
||||||
# - job_name: docker-daemon
|
|
||||||
# dockerswarm_sd_configs:
|
|
||||||
# - host: unix:///var/run/docker.sock
|
|
||||||
# role: nodes
|
|
||||||
# relabel_configs:
|
|
||||||
# - source_labels: [__meta_dockerswarm_node_address]
|
|
||||||
# target_label: __address__
|
|
||||||
# replacement: $1:9323
|
|
||||||
- job_name: "dockerswarm"
|
|
||||||
dockerswarm_sd_configs:
|
|
||||||
- host: unix:///var/run/docker.sock
|
|
||||||
role: tasks
|
|
||||||
relabel_configs:
|
|
||||||
- action: keep
|
|
||||||
regex: running
|
|
||||||
source_labels:
|
|
||||||
- __meta_dockerswarm_task_desired_state
|
|
||||||
- action: keep
|
|
||||||
regex: true
|
|
||||||
source_labels:
|
|
||||||
- __meta_dockerswarm_service_label_signoz_io_scrape
|
|
||||||
- regex: ([^:]+)(?::\d+)?
|
|
||||||
replacement: $1
|
|
||||||
source_labels:
|
|
||||||
- __address__
|
|
||||||
target_label: swarm_container_ip
|
|
||||||
- separator: .
|
|
||||||
source_labels:
|
|
||||||
- __meta_dockerswarm_service_name
|
|
||||||
- __meta_dockerswarm_task_slot
|
|
||||||
- __meta_dockerswarm_task_id
|
|
||||||
target_label: swarm_container_name
|
|
||||||
- target_label: __address__
|
|
||||||
source_labels:
|
|
||||||
- swarm_container_ip
|
|
||||||
- __meta_dockerswarm_service_label_signoz_io_port
|
|
||||||
separator: ":"
|
|
||||||
- source_labels:
|
|
||||||
- __meta_dockerswarm_service_label_signoz_io_path
|
|
||||||
target_label: __metrics_path__
|
|
||||||
- source_labels:
|
|
||||||
- __meta_dockerswarm_service_label_com_docker_stack_namespace
|
|
||||||
target_label: namespace
|
|
||||||
- source_labels:
|
|
||||||
- __meta_dockerswarm_service_name
|
|
||||||
target_label: service_name
|
|
||||||
- source_labels:
|
|
||||||
- __meta_dockerswarm_task_id
|
|
||||||
target_label: service_instance_id
|
|
||||||
- source_labels:
|
|
||||||
- __meta_dockerswarm_node_hostname
|
|
||||||
target_label: host_name
|
|
||||||
processors:
|
|
||||||
batch:
|
|
||||||
send_batch_size: 10000
|
|
||||||
send_batch_max_size: 11000
|
|
||||||
timeout: 10s
|
|
||||||
resourcedetection:
|
|
||||||
detectors:
|
|
||||||
- env
|
|
||||||
- system
|
|
||||||
timeout: 2s
|
|
||||||
extensions:
|
|
||||||
health_check:
|
|
||||||
endpoint: 0.0.0.0:13133
|
|
||||||
pprof:
|
|
||||||
endpoint: 0.0.0.0:1777
|
|
||||||
exporters:
|
|
||||||
otlp:
|
|
||||||
endpoint: ${env:SIGNOZ_COLLECTOR_ENDPOINT}
|
|
||||||
tls:
|
|
||||||
insecure: true
|
|
||||||
headers:
|
|
||||||
signoz-access-token: ${env:SIGNOZ_ACCESS_TOKEN}
|
|
||||||
# debug: {}
|
|
||||||
service:
|
|
||||||
telemetry:
|
|
||||||
logs:
|
|
||||||
encoding: json
|
|
||||||
metrics:
|
|
||||||
address: 0.0.0.0:8888
|
|
||||||
extensions:
|
|
||||||
- health_check
|
|
||||||
- pprof
|
|
||||||
pipelines:
|
|
||||||
metrics:
|
|
||||||
receivers: [prometheus]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
x-common: &common
|
|
||||||
networks:
|
|
||||||
- signoz-net
|
|
||||||
extra_hosts:
|
|
||||||
- host.docker.internal:host-gateway
|
|
||||||
logging:
|
|
||||||
options:
|
|
||||||
max-size: 50m
|
|
||||||
max-file: "3"
|
|
||||||
restart: unless-stopped
|
|
||||||
services:
|
|
||||||
hotrod:
|
|
||||||
<<: *common
|
|
||||||
image: jaegertracing/example-hotrod:1.61.0
|
|
||||||
container_name: hotrod
|
|
||||||
command: [ "all" ]
|
|
||||||
environment:
|
|
||||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 # In case of external SigNoz or cloud, update the endpoint and access token
|
|
||||||
# - OTEL_OTLP_HEADERS=signoz-access-token=<your-access-token>
|
|
||||||
load-hotrod:
|
|
||||||
<<: *common
|
|
||||||
image: "signoz/locust:1.2.3"
|
|
||||||
container_name: load-hotrod
|
|
||||||
environment:
|
|
||||||
ATTACKED_HOST: http://hotrod:8080
|
|
||||||
LOCUST_MODE: standalone
|
|
||||||
NO_PROXY: standalone
|
|
||||||
TASK_DELAY_FROM: 5
|
|
||||||
TASK_DELAY_TO: 30
|
|
||||||
QUIET_MODE: "${QUIET_MODE:-false}"
|
|
||||||
LOCUST_OPTS: "--headless -u 10 -r 1"
|
|
||||||
volumes:
|
|
||||||
- ../../../common/locust-scripts:/locust
|
|
||||||
|
|
||||||
networks:
|
|
||||||
signoz-net:
|
|
||||||
name: signoz-net
|
|
||||||
external: true
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
x-common: &common
|
|
||||||
networks:
|
|
||||||
- signoz-net
|
|
||||||
extra_hosts:
|
|
||||||
- host.docker.internal:host-gateway
|
|
||||||
logging:
|
|
||||||
options:
|
|
||||||
max-size: 50m
|
|
||||||
max-file: "3"
|
|
||||||
restart: unless-stopped
|
|
||||||
services:
|
|
||||||
otel-agent:
|
|
||||||
<<: *common
|
|
||||||
image: otel/opentelemetry-collector-contrib:0.111.0
|
|
||||||
command:
|
|
||||||
- --config=/etc/otel-collector-config.yaml
|
|
||||||
volumes:
|
|
||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
|
||||||
- /:/hostfs:ro
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
environment:
|
|
||||||
- SIGNOZ_COLLECTOR_ENDPOINT=http://host.docker.internal:4317 # In case of external SigNoz or cloud, update the endpoint and access token
|
|
||||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux # Replace signoz-host with the actual hostname
|
|
||||||
# - SIGNOZ_ACCESS_TOKEN="<your-access-token>"
|
|
||||||
# Before exposing the ports, make sure the ports are not used by other services
|
|
||||||
# ports:
|
|
||||||
# - "4317:4317"
|
|
||||||
# - "4318:4318"
|
|
||||||
logspout:
|
|
||||||
<<: *common
|
|
||||||
image: "gliderlabs/logspout:v3.2.14"
|
|
||||||
volumes:
|
|
||||||
- /etc/hostname:/etc/host_hostname:ro
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
command: syslog+tcp://otel-agent:2255
|
|
||||||
depends_on:
|
|
||||||
- otel-agent
|
|
||||||
|
|
||||||
networks:
|
|
||||||
signoz-net:
|
|
||||||
name: signoz-net
|
|
||||||
external: true
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
receivers:
|
|
||||||
hostmetrics:
|
|
||||||
collection_interval: 30s
|
|
||||||
root_path: /hostfs
|
|
||||||
scrapers:
|
|
||||||
cpu: {}
|
|
||||||
load: {}
|
|
||||||
memory: {}
|
|
||||||
disk: {}
|
|
||||||
filesystem: {}
|
|
||||||
network: {}
|
|
||||||
otlp:
|
|
||||||
protocols:
|
|
||||||
grpc:
|
|
||||||
endpoint: 0.0.0.0:4317
|
|
||||||
http:
|
|
||||||
endpoint: 0.0.0.0:4318
|
|
||||||
prometheus:
|
|
||||||
config:
|
|
||||||
global:
|
|
||||||
scrape_interval: 60s
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: otel-collector
|
|
||||||
static_configs:
|
|
||||||
- targets:
|
|
||||||
- localhost:8888
|
|
||||||
labels:
|
|
||||||
job_name: otel-collector
|
|
||||||
# For Docker daemon metrics to be scraped, it must be configured to expose
|
|
||||||
# Prometheus metrics, as documented here: https://docs.docker.com/config/daemon/prometheus/
|
|
||||||
# - job_name: docker-daemon
|
|
||||||
# static_configs:
|
|
||||||
# - targets:
|
|
||||||
# - host.docker.internal:9323
|
|
||||||
# labels:
|
|
||||||
# job_name: docker-daemon
|
|
||||||
- job_name: docker-container
|
|
||||||
docker_sd_configs:
|
|
||||||
- host: unix:///var/run/docker.sock
|
|
||||||
relabel_configs:
|
|
||||||
- action: keep
|
|
||||||
regex: true
|
|
||||||
source_labels:
|
|
||||||
- __meta_docker_container_label_signoz_io_scrape
|
|
||||||
- regex: true
|
|
||||||
source_labels:
|
|
||||||
- __meta_docker_container_label_signoz_io_path
|
|
||||||
target_label: __metrics_path__
|
|
||||||
- regex: (.+)
|
|
||||||
source_labels:
|
|
||||||
- __meta_docker_container_label_signoz_io_path
|
|
||||||
target_label: __metrics_path__
|
|
||||||
- separator: ":"
|
|
||||||
source_labels:
|
|
||||||
- __meta_docker_network_ip
|
|
||||||
- __meta_docker_container_label_signoz_io_port
|
|
||||||
target_label: __address__
|
|
||||||
- regex: '/(.*)'
|
|
||||||
replacement: '$1'
|
|
||||||
source_labels:
|
|
||||||
- __meta_docker_container_name
|
|
||||||
target_label: container_name
|
|
||||||
- regex: __meta_docker_container_label_signoz_io_(.+)
|
|
||||||
action: labelmap
|
|
||||||
replacement: $1
|
|
||||||
tcplog/docker:
|
|
||||||
listen_address: "0.0.0.0:2255"
|
|
||||||
operators:
|
|
||||||
- type: regex_parser
|
|
||||||
regex: '^<([0-9]+)>[0-9]+ (?P<timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?) (?P<container_id>\S+) (?P<container_name>\S+) [0-9]+ - -( (?P<body>.*))?'
|
|
||||||
timestamp:
|
|
||||||
parse_from: attributes.timestamp
|
|
||||||
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
|
||||||
- type: move
|
|
||||||
from: attributes["body"]
|
|
||||||
to: body
|
|
||||||
- type: remove
|
|
||||||
field: attributes.timestamp
|
|
||||||
# please remove names from below if you want to collect logs from them
|
|
||||||
- type: filter
|
|
||||||
id: signoz_logs_filter
|
|
||||||
expr: 'attributes.container_name matches "^signoz|(signoz-(|otel-collector|clickhouse|zookeeper))|(infra-(logspout|otel-agent)-.*)"'
|
|
||||||
processors:
|
|
||||||
batch:
|
|
||||||
send_batch_size: 10000
|
|
||||||
send_batch_max_size: 11000
|
|
||||||
timeout: 10s
|
|
||||||
resourcedetection:
|
|
||||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
|
||||||
detectors:
|
|
||||||
# - ec2
|
|
||||||
# - gcp
|
|
||||||
# - azure
|
|
||||||
- env
|
|
||||||
- system
|
|
||||||
timeout: 2s
|
|
||||||
extensions:
|
|
||||||
health_check:
|
|
||||||
endpoint: 0.0.0.0:13133
|
|
||||||
pprof:
|
|
||||||
endpoint: 0.0.0.0:1777
|
|
||||||
exporters:
|
|
||||||
otlp:
|
|
||||||
endpoint: ${env:SIGNOZ_COLLECTOR_ENDPOINT}
|
|
||||||
tls:
|
|
||||||
insecure: true
|
|
||||||
headers:
|
|
||||||
signoz-access-token: ${env:SIGNOZ_ACCESS_TOKEN}
|
|
||||||
# debug: {}
|
|
||||||
service:
|
|
||||||
telemetry:
|
|
||||||
logs:
|
|
||||||
encoding: json
|
|
||||||
metrics:
|
|
||||||
address: 0.0.0.0:8888
|
|
||||||
extensions:
|
|
||||||
- health_check
|
|
||||||
- pprof
|
|
||||||
pipelines:
|
|
||||||
traces:
|
|
||||||
receivers: [otlp]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
metrics:
|
|
||||||
receivers: [otlp]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
metrics/hostmetrics:
|
|
||||||
receivers: [hostmetrics]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
metrics/prometheus:
|
|
||||||
receivers: [prometheus]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
logs:
|
|
||||||
receivers: [otlp, tcplog/docker]
|
|
||||||
processors: [resourcedetection, batch]
|
|
||||||
exporters: [otlp]
|
|
||||||
@@ -220,6 +220,13 @@ components:
|
|||||||
- additions
|
- additions
|
||||||
- deletions
|
- deletions
|
||||||
type: object
|
type: object
|
||||||
|
AuthtypesPatchableRole:
|
||||||
|
properties:
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- description
|
||||||
|
type: object
|
||||||
AuthtypesPostableAuthDomain:
|
AuthtypesPostableAuthDomain:
|
||||||
properties:
|
properties:
|
||||||
config:
|
config:
|
||||||
@@ -236,6 +243,15 @@ components:
|
|||||||
password:
|
password:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
AuthtypesPostableRole:
|
||||||
|
properties:
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
AuthtypesPostableRotateToken:
|
AuthtypesPostableRotateToken:
|
||||||
properties:
|
properties:
|
||||||
refreshToken:
|
refreshToken:
|
||||||
@@ -251,6 +267,31 @@ components:
|
|||||||
- name
|
- name
|
||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
|
AuthtypesRole:
|
||||||
|
properties:
|
||||||
|
createdAt:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
orgId:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
updatedAt:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- description
|
||||||
|
- type
|
||||||
|
- orgId
|
||||||
|
type: object
|
||||||
AuthtypesRoleMapping:
|
AuthtypesRoleMapping:
|
||||||
properties:
|
properties:
|
||||||
defaultRole:
|
defaultRole:
|
||||||
@@ -1722,47 +1763,6 @@ components:
|
|||||||
- status
|
- status
|
||||||
- error
|
- error
|
||||||
type: object
|
type: object
|
||||||
RoletypesPatchableRole:
|
|
||||||
properties:
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- description
|
|
||||||
type: object
|
|
||||||
RoletypesPostableRole:
|
|
||||||
properties:
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
type: object
|
|
||||||
RoletypesRole:
|
|
||||||
properties:
|
|
||||||
createdAt:
|
|
||||||
format: date-time
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
orgId:
|
|
||||||
type: string
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
updatedAt:
|
|
||||||
format: date-time
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- name
|
|
||||||
- description
|
|
||||||
- type
|
|
||||||
- orgId
|
|
||||||
type: object
|
|
||||||
ServiceaccounttypesFactorAPIKey:
|
ServiceaccounttypesFactorAPIKey:
|
||||||
properties:
|
properties:
|
||||||
createdAt:
|
createdAt:
|
||||||
@@ -4234,7 +4234,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/RoletypesRole'
|
$ref: '#/components/schemas/AuthtypesRole'
|
||||||
type: array
|
type: array
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
@@ -4277,7 +4277,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RoletypesPostableRole'
|
$ref: '#/components/schemas/AuthtypesPostableRole'
|
||||||
responses:
|
responses:
|
||||||
"201":
|
"201":
|
||||||
content:
|
content:
|
||||||
@@ -4422,7 +4422,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/components/schemas/RoletypesRole'
|
$ref: '#/components/schemas/AuthtypesRole'
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
@@ -4470,7 +4470,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RoletypesPatchableRole'
|
$ref: '#/components/schemas/AuthtypesPatchableRole'
|
||||||
responses:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
content:
|
content:
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/roletypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||||
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
|
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
|
||||||
@@ -23,7 +22,7 @@ type provider struct {
|
|||||||
pkgAuthzService authz.AuthZ
|
pkgAuthzService authz.AuthZ
|
||||||
openfgaServer *openfgaserver.Server
|
openfgaServer *openfgaserver.Server
|
||||||
licensing licensing.Licensing
|
licensing licensing.Licensing
|
||||||
store roletypes.Store
|
store authtypes.RoleStore
|
||||||
registry []authz.RegisterTypeable
|
registry []authz.RegisterTypeable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,23 +81,23 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl
|
|||||||
return provider.openfgaServer.Write(ctx, additions, deletions)
|
return provider.openfgaServer.Write(ctx, additions, deletions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
|
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
|
||||||
return provider.pkgAuthzService.Get(ctx, orgID, id)
|
return provider.pkgAuthzService.Get(ctx, orgID, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
|
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
|
||||||
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
|
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
|
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
|
||||||
return provider.pkgAuthzService.List(ctx, orgID)
|
return provider.pkgAuthzService.List(ctx, orgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
|
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
|
||||||
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
|
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
|
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
|
||||||
return provider.pkgAuthzService.ListByOrgIDAndIDs(ctx, orgID, ids)
|
return provider.pkgAuthzService.ListByOrgIDAndIDs(ctx, orgID, ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
|
|||||||
return provider.pkgAuthzService.Revoke(ctx, orgID, names, subject)
|
return provider.pkgAuthzService.Revoke(ctx, orgID, names, subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*roletypes.Role) error {
|
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*authtypes.Role) error {
|
||||||
return provider.pkgAuthzService.CreateManagedRoles(ctx, orgID, managedRoles)
|
return provider.pkgAuthzService.CreateManagedRoles(ctx, orgID, managedRoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,16 +135,16 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
|
|||||||
return provider.Write(ctx, tuples, nil)
|
return provider.Write(ctx, tuples, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
|
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
|
||||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
|
return provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) {
|
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
|
||||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||||
@@ -159,10 +158,10 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existingRole != nil {
|
if existingRole != nil {
|
||||||
return roletypes.NewRoleFromStorableRole(existingRole), nil
|
return authtypes.NewRoleFromStorableRole(existingRole), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
|
err = provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -217,13 +216,13 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
|
|||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
|
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
|
||||||
_, err := provider.licensing.GetActive(ctx, orgID)
|
_, err := provider.licensing.GetActive(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return provider.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
|
return provider.store.Update(ctx, orgID, authtypes.NewStorableRoleFromRole(role))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
|
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
|
||||||
@@ -232,12 +231,12 @@ func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, n
|
|||||||
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
|
additionTuples, err := authtypes.GetAdditionTuples(name, orgID, relation, additions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
|
deletionTuples, err := authtypes.GetDeletionTuples(name, orgID, relation, deletions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -261,7 +260,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
role := roletypes.NewRoleFromStorableRole(storableRole)
|
role := authtypes.NewRoleFromStorableRole(storableRole)
|
||||||
err = role.ErrIfManaged()
|
err = role.ErrIfManaged()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -271,7 +270,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
|
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
|
||||||
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
|
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||||
@@ -283,7 +282,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
|
|||||||
adminSubject,
|
adminSubject,
|
||||||
authtypes.RelationAssignee,
|
authtypes.RelationAssignee,
|
||||||
[]authtypes.Selector{
|
[]authtypes.Selector{
|
||||||
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
|
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
|
||||||
},
|
},
|
||||||
orgID,
|
orgID,
|
||||||
)
|
)
|
||||||
@@ -298,7 +297,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
|
|||||||
anonymousSubject,
|
anonymousSubject,
|
||||||
authtypes.RelationAssignee,
|
authtypes.RelationAssignee,
|
||||||
[]authtypes.Selector{
|
[]authtypes.Selector{
|
||||||
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAnonymousRoleName),
|
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
|
||||||
},
|
},
|
||||||
orgID,
|
orgID,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||||
"github.com/SigNoz/signoz/pkg/types/roletypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -224,7 +223,7 @@ func (module *module) MustGetTypeables() []authtypes.Typeable {
|
|||||||
|
|
||||||
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
|
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
|
||||||
return map[string][]*authtypes.Transaction{
|
return map[string][]*authtypes.Transaction{
|
||||||
roletypes.SigNozAnonymousRoleName: {
|
authtypes.SigNozAnonymousRoleName: {
|
||||||
{
|
{
|
||||||
ID: valuer.GenerateUUID(),
|
ID: valuer.GenerateUUID(),
|
||||||
Relation: authtypes.RelationRead,
|
Relation: authtypes.RelationRead,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package anomaly
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,7 +14,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -67,7 +67,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
|
|||||||
instrumentationtypes.CodeNamespace: "anomaly",
|
instrumentationtypes.CodeNamespace: "anomaly",
|
||||||
instrumentationtypes.CodeFunctionName: "getResults",
|
instrumentationtypes.CodeFunctionName: "getResults",
|
||||||
})
|
})
|
||||||
zap.L().Info("fetching results for current period", zap.Any("currentPeriodQuery", params.CurrentPeriodQuery))
|
slog.InfoContext(ctx, "fetching results for current period", "current_period_query", params.CurrentPeriodQuery)
|
||||||
currentPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentPeriodQuery)
|
currentPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentPeriodQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -78,7 +78,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("fetching results for past period", zap.Any("pastPeriodQuery", params.PastPeriodQuery))
|
slog.InfoContext(ctx, "fetching results for past period", "past_period_query", params.PastPeriodQuery)
|
||||||
pastPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.PastPeriodQuery)
|
pastPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.PastPeriodQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -89,7 +89,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("fetching results for current season", zap.Any("currentSeasonQuery", params.CurrentSeasonQuery))
|
slog.InfoContext(ctx, "fetching results for current season", "current_season_query", params.CurrentSeasonQuery)
|
||||||
currentSeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentSeasonQuery)
|
currentSeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentSeasonQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -100,7 +100,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("fetching results for past season", zap.Any("pastSeasonQuery", params.PastSeasonQuery))
|
slog.InfoContext(ctx, "fetching results for past season", "past_season_query", params.PastSeasonQuery)
|
||||||
pastSeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.PastSeasonQuery)
|
pastSeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.PastSeasonQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -111,7 +111,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("fetching results for past 2 season", zap.Any("past2SeasonQuery", params.Past2SeasonQuery))
|
slog.InfoContext(ctx, "fetching results for past 2 season", "past_2_season_query", params.Past2SeasonQuery)
|
||||||
past2SeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.Past2SeasonQuery)
|
past2SeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.Past2SeasonQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -122,7 +122,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("fetching results for past 3 season", zap.Any("past3SeasonQuery", params.Past3SeasonQuery))
|
slog.InfoContext(ctx, "fetching results for past 3 season", "past_3_season_query", params.Past3SeasonQuery)
|
||||||
past3SeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.Past3SeasonQuery)
|
past3SeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.Past3SeasonQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -235,17 +235,17 @@ func (p *BaseSeasonalProvider) getPredictedSeries(
|
|||||||
if predictedValue < 0 {
|
if predictedValue < 0 {
|
||||||
// this should not happen (except when the data has extreme outliers)
|
// this should not happen (except when the data has extreme outliers)
|
||||||
// we will use the moving avg of the previous period series in this case
|
// we will use the moving avg of the previous period series in this case
|
||||||
zap.L().Warn("predictedValue is less than 0", zap.Float64("predictedValue", predictedValue), zap.Any("labels", series.Labels))
|
slog.Warn("predicted value is less than 0", "predicted_value", predictedValue, "labels", series.Labels)
|
||||||
predictedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
|
predictedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Debug("predictedSeries",
|
slog.Debug("predicted series",
|
||||||
zap.Float64("movingAvg", movingAvg),
|
"moving_avg", movingAvg,
|
||||||
zap.Float64("avg", avg),
|
"avg", avg,
|
||||||
zap.Float64("mean", mean),
|
"mean", mean,
|
||||||
zap.Any("labels", series.Labels),
|
"labels", series.Labels,
|
||||||
zap.Float64("predictedValue", predictedValue),
|
"predicted_value", predictedValue,
|
||||||
zap.Float64("curr", curr.Value),
|
"curr", curr.Value,
|
||||||
)
|
)
|
||||||
predictedSeries.Points = append(predictedSeries.Points, v3.Point{
|
predictedSeries.Points = append(predictedSeries.Points, v3.Point{
|
||||||
Timestamp: curr.Timestamp,
|
Timestamp: curr.Timestamp,
|
||||||
@@ -418,7 +418,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UU
|
|||||||
|
|
||||||
for _, series := range result.Series {
|
for _, series := range result.Series {
|
||||||
stdDev := p.getStdDev(series)
|
stdDev := p.getStdDev(series)
|
||||||
zap.L().Info("stdDev", zap.Float64("stdDev", stdDev), zap.Any("labels", series.Labels))
|
slog.InfoContext(ctx, "computed standard deviation", "std_dev", stdDev, "labels", series.Labels)
|
||||||
|
|
||||||
pastPeriodSeries := p.getMatchingSeries(pastPeriodResult, series)
|
pastPeriodSeries := p.getMatchingSeries(pastPeriodResult, series)
|
||||||
currentSeasonSeries := p.getMatchingSeries(currentSeasonResult, series)
|
currentSeasonSeries := p.getMatchingSeries(currentSeasonResult, series)
|
||||||
@@ -431,7 +431,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UU
|
|||||||
pastSeasonSeriesAvg := p.getAvg(pastSeasonSeries)
|
pastSeasonSeriesAvg := p.getAvg(pastSeasonSeries)
|
||||||
past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries)
|
past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries)
|
||||||
past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries)
|
past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries)
|
||||||
zap.L().Info("getAvg", zap.Float64("prevSeriesAvg", prevSeriesAvg), zap.Float64("currentSeasonSeriesAvg", currentSeasonSeriesAvg), zap.Float64("pastSeasonSeriesAvg", pastSeasonSeriesAvg), zap.Float64("past2SeasonSeriesAvg", past2SeasonSeriesAvg), zap.Float64("past3SeasonSeriesAvg", past3SeasonSeriesAvg), zap.Any("labels", series.Labels))
|
slog.InfoContext(ctx, "computed averages", "prev_series_avg", prevSeriesAvg, "current_season_series_avg", currentSeasonSeriesAvg, "past_season_series_avg", pastSeasonSeriesAvg, "past_2_season_series_avg", past2SeasonSeriesAvg, "past_3_season_series_avg", past3SeasonSeriesAvg, "labels", series.Labels)
|
||||||
|
|
||||||
predictedSeries := p.getPredictedSeries(
|
predictedSeries := p.getPredictedSeries(
|
||||||
series,
|
series,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.uber.org/zap"
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CloudIntegrationConnectionParamsResponse struct {
|
type CloudIntegrationConnectionParamsResponse struct {
|
||||||
@@ -71,7 +71,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
|||||||
// Return the API Key (PAT) even if the rest of the params can not be deduced.
|
// Return the API Key (PAT) even if the rest of the params can not be deduced.
|
||||||
// Params not returned from here will be requested from the user via form inputs.
|
// Params not returned from here will be requested from the user via form inputs.
|
||||||
// This enables gracefully degraded but working experience even for non-cloud deployments.
|
// This enables gracefully degraded but working experience even for non-cloud deployments.
|
||||||
zap.L().Info("ingestion params and signoz api url can not be deduced since no license was found")
|
slog.InfoContext(r.Context(), "ingestion params and signoz api url can not be deduced since no license was found")
|
||||||
ah.Respond(w, result)
|
ah.Respond(w, result)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
|||||||
result.IngestionKey = ingestionKey
|
result.IngestionKey = ingestionKey
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
zap.L().Info("ingestion key can't be deduced since no gateway url has been configured")
|
slog.InfoContext(r.Context(), "ingestion key can't be deduced since no gateway url has been configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
ah.Respond(w, result)
|
ah.Respond(w, result)
|
||||||
@@ -138,9 +138,8 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info(
|
slog.InfoContext(ctx, "no PAT found for cloud integration, creating a new one",
|
||||||
"no PAT found for cloud integration, creating a new one",
|
"cloud_provider", cloudProvider,
|
||||||
zap.String("cloudProvider", cloudProvider),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
newPAT, err := types.NewStorableAPIKey(
|
newPAT, err := types.NewStorableAPIKey(
|
||||||
@@ -287,9 +286,8 @@ func getOrCreateCloudProviderIngestionKey(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info(
|
slog.InfoContext(ctx, "no existing ingestion key found for cloud integration, creating a new one",
|
||||||
"no existing ingestion key found for cloud integration, creating a new one",
|
"cloud_provider", cloudProvider,
|
||||||
zap.String("cloudProvider", cloudProvider),
|
|
||||||
)
|
)
|
||||||
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
|
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
|
||||||
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
|
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"go.uber.org/zap"
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -35,23 +35,23 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if constants.FetchFeatures == "true" {
|
if constants.FetchFeatures == "true" {
|
||||||
zap.L().Debug("fetching license")
|
slog.DebugContext(ctx, "fetching license")
|
||||||
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
|
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to fetch license", zap.Error(err))
|
slog.ErrorContext(ctx, "failed to fetch license", "error", err)
|
||||||
} else if license == nil {
|
} else if license == nil {
|
||||||
zap.L().Debug("no active license found")
|
slog.DebugContext(ctx, "no active license found")
|
||||||
} else {
|
} else {
|
||||||
licenseKey := license.Key
|
licenseKey := license.Key
|
||||||
|
|
||||||
zap.L().Debug("fetching zeus features")
|
slog.DebugContext(ctx, "fetching zeus features")
|
||||||
zeusFeatures, err := fetchZeusFeatures(constants.ZeusFeaturesURL, licenseKey)
|
zeusFeatures, err := fetchZeusFeatures(constants.ZeusFeaturesURL, licenseKey)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
zap.L().Debug("fetched zeus features", zap.Any("features", zeusFeatures))
|
slog.DebugContext(ctx, "fetched zeus features", "features", zeusFeatures)
|
||||||
// merge featureSet and zeusFeatures in featureSet with higher priority to zeusFeatures
|
// merge featureSet and zeusFeatures in featureSet with higher priority to zeusFeatures
|
||||||
featureSet = MergeFeatureSets(zeusFeatures, featureSet)
|
featureSet = MergeFeatureSets(zeusFeatures, featureSet)
|
||||||
} else {
|
} else {
|
||||||
zap.L().Error("failed to fetch zeus features", zap.Error(err))
|
slog.ErrorContext(ctx, "failed to fetch zeus features", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"go.uber.org/zap"
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -35,7 +35,7 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
|||||||
queryRangeParams, apiErrorObj := baseapp.ParseQueryRangeParams(r)
|
queryRangeParams, apiErrorObj := baseapp.ParseQueryRangeParams(r)
|
||||||
|
|
||||||
if apiErrorObj != nil {
|
if apiErrorObj != nil {
|
||||||
zap.L().Error("error parsing metric query range params", zap.Error(apiErrorObj.Err))
|
slog.ErrorContext(r.Context(), "error parsing metric query range params", "error", apiErrorObj.Err)
|
||||||
RespondError(w, apiErrorObj, nil)
|
RespondError(w, apiErrorObj, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
|||||||
// add temporality for each metric
|
// add temporality for each metric
|
||||||
temporalityErr := aH.PopulateTemporality(r.Context(), orgID, queryRangeParams)
|
temporalityErr := aH.PopulateTemporality(r.Context(), orgID, queryRangeParams)
|
||||||
if temporalityErr != nil {
|
if temporalityErr != nil {
|
||||||
zap.L().Error("Error while adding temporality for metrics", zap.Error(temporalityErr))
|
slog.ErrorContext(r.Context(), "error while adding temporality for metrics", "error", temporalityErr)
|
||||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil)
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import (
|
|||||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||||
"go.uber.org/zap"
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server runs HTTP, Mux and a grpc server
|
// Server runs HTTP, Mux and a grpc server
|
||||||
@@ -83,6 +83,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reader := clickhouseReader.NewReader(
|
reader := clickhouseReader.NewReader(
|
||||||
|
signoz.Instrumentation.Logger(),
|
||||||
signoz.SQLStore,
|
signoz.SQLStore,
|
||||||
signoz.TelemetryStore,
|
signoz.TelemetryStore,
|
||||||
signoz.Prometheus,
|
signoz.Prometheus,
|
||||||
@@ -216,8 +217,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
|||||||
}),
|
}),
|
||||||
otelmux.WithPublicEndpoint(),
|
otelmux.WithPublicEndpoint(),
|
||||||
))
|
))
|
||||||
r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Tokenizer, s.signoz.Instrumentation.Logger()).Wrap)
|
r.Use(middleware.NewIdentN(s.signoz.IdentNResolver, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
|
||||||
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
|
||||||
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
|
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
|
||||||
s.config.APIServer.Timeout.ExcludedRoutes,
|
s.config.APIServer.Timeout.ExcludedRoutes,
|
||||||
s.config.APIServer.Timeout.Default,
|
s.config.APIServer.Timeout.Default,
|
||||||
@@ -278,7 +278,7 @@ func (s *Server) initListeners() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
|
slog.Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -298,31 +298,31 @@ func (s *Server) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.httpHostPort))
|
slog.Info("Starting HTTP server", "port", httpPort, "addr", s.httpHostPort)
|
||||||
|
|
||||||
switch err := s.httpServer.Serve(s.httpConn); err {
|
switch err := s.httpServer.Serve(s.httpConn); err {
|
||||||
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
|
||||||
// normal exit, nothing to do
|
// normal exit, nothing to do
|
||||||
default:
|
default:
|
||||||
zap.L().Error("Could not start HTTP server", zap.Error(err))
|
slog.Error("Could not start HTTP server", "error", err)
|
||||||
}
|
}
|
||||||
s.unavailableChannel <- healthcheck.Unavailable
|
s.unavailableChannel <- healthcheck.Unavailable
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
zap.L().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort))
|
slog.Info("Starting pprof server", "addr", baseconst.DebugHttpPort)
|
||||||
|
|
||||||
err = http.ListenAndServe(baseconst.DebugHttpPort, nil)
|
err = http.ListenAndServe(baseconst.DebugHttpPort, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("Could not start pprof server", zap.Error(err))
|
slog.Error("Could not start pprof server", "error", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
|
slog.Info("Starting OpAmp Websocket server", "addr", baseconst.OpAmpWsEndpoint)
|
||||||
err := s.opampServer.Start(baseconst.OpAmpWsEndpoint)
|
err := s.opampServer.Start(baseconst.OpAmpWsEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("opamp ws server failed to start", zap.Error(err))
|
slog.Error("opamp ws server failed to start", "error", err)
|
||||||
s.unavailableChannel <- healthcheck.Unavailable
|
s.unavailableChannel <- healthcheck.Unavailable
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -358,10 +358,9 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
|
|||||||
MetadataStore: metadataStore,
|
MetadataStore: metadataStore,
|
||||||
Prometheus: prometheus,
|
Prometheus: prometheus,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Logger: zap.L(),
|
|
||||||
Reader: ch,
|
Reader: ch,
|
||||||
Querier: querier,
|
Querier: querier,
|
||||||
SLogger: providerSettings.Logger,
|
Logger: providerSettings.Logger,
|
||||||
Cache: cache,
|
Cache: cache,
|
||||||
EvalDelay: baseconst.GetEvalDelay(),
|
EvalDelay: baseconst.GetEvalDelay(),
|
||||||
PrepareTaskFunc: rules.PrepareTaskFunc,
|
PrepareTaskFunc: rules.PrepareTaskFunc,
|
||||||
@@ -380,7 +379,7 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
|
|||||||
return nil, fmt.Errorf("rule manager error: %v", err)
|
return nil, fmt.Errorf("rule manager error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("rules manager is ready")
|
slog.Info("rules manager is ready")
|
||||||
|
|
||||||
return manager, nil
|
return manager, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package rules
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
|
|||||||
|
|
||||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||||
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||||
|
|
||||||
rule, err := NewAnomalyRule(
|
rule, err := NewAnomalyRule(
|
||||||
"test-anomaly-rule",
|
"test-anomaly-rule",
|
||||||
@@ -247,7 +248,7 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
|
|||||||
|
|
||||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
|
||||||
options := clickhouseReader.NewOptions("primaryNamespace")
|
options := clickhouseReader.NewOptions("primaryNamespace")
|
||||||
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
|
||||||
|
|
||||||
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
|
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
|
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
|
||||||
@@ -34,7 +34,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
|||||||
opts.Rule,
|
opts.Rule,
|
||||||
opts.Reader,
|
opts.Reader,
|
||||||
opts.Querier,
|
opts.Querier,
|
||||||
opts.SLogger,
|
opts.Logger,
|
||||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||||
baserules.WithSQLStore(opts.SQLStore),
|
baserules.WithSQLStore(opts.SQLStore),
|
||||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||||
@@ -57,7 +57,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
|||||||
ruleId,
|
ruleId,
|
||||||
opts.OrgID,
|
opts.OrgID,
|
||||||
opts.Rule,
|
opts.Rule,
|
||||||
opts.SLogger,
|
opts.Logger,
|
||||||
opts.Reader,
|
opts.Reader,
|
||||||
opts.ManagerOpts.Prometheus,
|
opts.ManagerOpts.Prometheus,
|
||||||
baserules.WithSQLStore(opts.SQLStore),
|
baserules.WithSQLStore(opts.SQLStore),
|
||||||
@@ -82,7 +82,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
|||||||
opts.Rule,
|
opts.Rule,
|
||||||
opts.Reader,
|
opts.Reader,
|
||||||
opts.Querier,
|
opts.Querier,
|
||||||
opts.SLogger,
|
opts.Logger,
|
||||||
opts.Cache,
|
opts.Cache,
|
||||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||||
baserules.WithSQLStore(opts.SQLStore),
|
baserules.WithSQLStore(opts.SQLStore),
|
||||||
@@ -142,7 +142,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
|||||||
parsedRule,
|
parsedRule,
|
||||||
opts.Reader,
|
opts.Reader,
|
||||||
opts.Querier,
|
opts.Querier,
|
||||||
opts.SLogger,
|
opts.Logger,
|
||||||
baserules.WithSendAlways(),
|
baserules.WithSendAlways(),
|
||||||
baserules.WithSendUnmatched(),
|
baserules.WithSendUnmatched(),
|
||||||
baserules.WithSQLStore(opts.SQLStore),
|
baserules.WithSQLStore(opts.SQLStore),
|
||||||
@@ -151,7 +151,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to prepare a new threshold rule for test", zap.String("name", alertname), zap.Error(err))
|
slog.Error("failed to prepare a new threshold rule for test", "name", alertname, "error", err)
|
||||||
return 0, basemodel.BadRequest(err)
|
return 0, basemodel.BadRequest(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
|||||||
alertname,
|
alertname,
|
||||||
opts.OrgID,
|
opts.OrgID,
|
||||||
parsedRule,
|
parsedRule,
|
||||||
opts.SLogger,
|
opts.Logger,
|
||||||
opts.Reader,
|
opts.Reader,
|
||||||
opts.ManagerOpts.Prometheus,
|
opts.ManagerOpts.Prometheus,
|
||||||
baserules.WithSendAlways(),
|
baserules.WithSendAlways(),
|
||||||
@@ -173,7 +173,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", alertname), zap.Error(err))
|
slog.Error("failed to prepare a new promql rule for test", "name", alertname, "error", err)
|
||||||
return 0, basemodel.BadRequest(err)
|
return 0, basemodel.BadRequest(err)
|
||||||
}
|
}
|
||||||
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
|
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||||
@@ -184,7 +184,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
|||||||
parsedRule,
|
parsedRule,
|
||||||
opts.Reader,
|
opts.Reader,
|
||||||
opts.Querier,
|
opts.Querier,
|
||||||
opts.SLogger,
|
opts.Logger,
|
||||||
opts.Cache,
|
opts.Cache,
|
||||||
baserules.WithSendAlways(),
|
baserules.WithSendAlways(),
|
||||||
baserules.WithSendUnmatched(),
|
baserules.WithSendUnmatched(),
|
||||||
@@ -193,7 +193,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
|||||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", alertname), zap.Error(err))
|
slog.Error("failed to prepare a new anomaly rule for test", "name", alertname, "error", err)
|
||||||
return 0, basemodel.BadRequest(err)
|
return 0, basemodel.BadRequest(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -205,7 +205,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
|||||||
|
|
||||||
alertsFound, err := rule.Eval(ctx, ts)
|
alertsFound, err := rule.Eval(ctx, ts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("evaluating rule failed", zap.String("rule", rule.Name()), zap.Error(err))
|
slog.Error("evaluating rule failed", "rule", rule.Name(), "error", err)
|
||||||
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
|
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
|
||||||
}
|
}
|
||||||
rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc)
|
rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc)
|
||||||
|
|||||||
@@ -80,6 +80,21 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
|
|||||||
alertDataRows := cmock.NewRows(cols, tc.Values)
|
alertDataRows := cmock.NewRows(cols, tc.Values)
|
||||||
|
|
||||||
mock := telemetryStore.Mock()
|
mock := telemetryStore.Mock()
|
||||||
|
// Mock metadata queries for FetchTemporalityAndTypeMulti
|
||||||
|
// First query: fetchMetricsTemporalityAndType (from signoz_metrics time series table)
|
||||||
|
metadataCols := []cmock.ColumnType{
|
||||||
|
{Name: "metric_name", Type: "String"},
|
||||||
|
{Name: "temporality", Type: "String"},
|
||||||
|
{Name: "type", Type: "String"},
|
||||||
|
{Name: "is_monotonic", Type: "Bool"},
|
||||||
|
}
|
||||||
|
metadataRows := cmock.NewRows(metadataCols, [][]any{
|
||||||
|
{"probe_success", metrictypes.Unspecified, metrictypes.GaugeType, false},
|
||||||
|
})
|
||||||
|
mock.ExpectQuery("*distributed_time_series_v4*").WithArgs(nil, nil, nil).WillReturnRows(metadataRows)
|
||||||
|
// Second query: fetchMeterSourceMetricsTemporalityAndType (from signoz_meter table)
|
||||||
|
emptyMetadataRows := cmock.NewRows(metadataCols, [][]any{})
|
||||||
|
mock.ExpectQuery("*meter*").WithArgs(nil).WillReturnRows(emptyMetadataRows)
|
||||||
|
|
||||||
// Generate query arguments for the metric query
|
// Generate query arguments for the metric query
|
||||||
evalTime := time.Now().UTC()
|
evalTime := time.Now().UTC()
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
"github.com/ClickHouse/clickhouse-go/v2"
|
||||||
"github.com/go-co-op/gocron"
|
"github.com/go-co-op/gocron"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
@@ -76,19 +76,19 @@ func (lm *Manager) Start(ctx context.Context) error {
|
|||||||
func (lm *Manager) UploadUsage(ctx context.Context) {
|
func (lm *Manager) UploadUsage(ctx context.Context) {
|
||||||
organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
|
organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to get organizations", zap.Error(err))
|
slog.ErrorContext(ctx, "failed to get organizations", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, organization := range organizations {
|
for _, organization := range organizations {
|
||||||
// check if license is present or not
|
// check if license is present or not
|
||||||
license, err := lm.licenseService.GetActive(ctx, organization.ID)
|
license, err := lm.licenseService.GetActive(ctx, organization.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to get active license", zap.Error(err))
|
slog.ErrorContext(ctx, "failed to get active license", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if license == nil {
|
if license == nil {
|
||||||
// we will not start the usage reporting if license is not present.
|
// we will not start the usage reporting if license is not present.
|
||||||
zap.L().Info("no license present, skipping usage reporting")
|
slog.InfoContext(ctx, "no license present, skipping usage reporting")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
|
|||||||
dbusages := []model.UsageDB{}
|
dbusages := []model.UsageDB{}
|
||||||
err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
|
err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour)))
|
||||||
if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
|
if err != nil && !strings.Contains(err.Error(), "doesn't exist") {
|
||||||
zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
|
slog.ErrorContext(ctx, "failed to get usage from clickhouse", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, u := range dbusages {
|
for _, u := range dbusages {
|
||||||
@@ -125,24 +125,24 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(usages) <= 0 {
|
if len(usages) <= 0 {
|
||||||
zap.L().Info("no snapshots to upload, skipping.")
|
slog.InfoContext(ctx, "no snapshots to upload, skipping")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("uploading usage data")
|
slog.InfoContext(ctx, "uploading usage data")
|
||||||
|
|
||||||
usagesPayload := []model.Usage{}
|
usagesPayload := []model.Usage{}
|
||||||
for _, usage := range usages {
|
for _, usage := range usages {
|
||||||
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
|
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
|
slog.ErrorContext(ctx, "error while decrypting usage data", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
usageData := model.Usage{}
|
usageData := model.Usage{}
|
||||||
err = json.Unmarshal(usageDataBytes, &usageData)
|
err = json.Unmarshal(usageDataBytes, &usageData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
|
slog.ErrorContext(ctx, "error while unmarshalling usage data", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,13 +163,13 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
|
|||||||
|
|
||||||
body, errv2 := json.Marshal(payload)
|
body, errv2 := json.Marshal(payload)
|
||||||
if errv2 != nil {
|
if errv2 != nil {
|
||||||
zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2))
|
slog.ErrorContext(ctx, "error while marshalling usage payload", "error", errv2)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
|
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
|
||||||
if errv2 != nil {
|
if errv2 != nil {
|
||||||
zap.L().Error("failed to upload usage: %v", zap.Error(errv2))
|
slog.ErrorContext(ctx, "failed to upload usage", "error", errv2)
|
||||||
// not returning error here since it is captured in the failed count
|
// not returning error here since it is captured in the failed count
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -179,7 +179,7 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
|
|||||||
func (lm *Manager) Stop(ctx context.Context) {
|
func (lm *Manager) Stop(ctx context.Context) {
|
||||||
lm.scheduler.Stop()
|
lm.scheduler.Stop()
|
||||||
|
|
||||||
zap.L().Info("sending usage data before shutting down")
|
slog.InfoContext(ctx, "sending usage data before shutting down")
|
||||||
// send usage before shutting down
|
// send usage before shutting down
|
||||||
lm.UploadUsage(ctx)
|
lm.UploadUsage(ctx)
|
||||||
atomic.StoreUint32(&locker, stateUnlocked)
|
atomic.StoreUint32(&locker, stateUnlocked)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
@@ -32,9 +33,9 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
|||||||
fmter: fmter,
|
fmter: fmter,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
|
operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
|
||||||
DropConstraint: true,
|
SCreateAndDropConstraint: true,
|
||||||
ColumnIfNotExistsExists: true,
|
SAlterTableAddAndDropColumnIfNotExistsAndExists: true,
|
||||||
AlterColumnSetNotNull: true,
|
SAlterTableAlterColumnSetAndDrop: true,
|
||||||
}),
|
}),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -72,8 +73,9 @@ WHERE
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return nil, nil, sql.ErrNoRows
|
return nil, nil, provider.sqlstore.WrapNotFoundErrf(sql.ErrNoRows, errors.CodeNotFound, "table (%s) not found", tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlschemaColumns := make([]*sqlschema.Column, 0)
|
sqlschemaColumns := make([]*sqlschema.Column, 0)
|
||||||
@@ -220,7 +222,9 @@ SELECT
|
|||||||
ci.relname AS index_name,
|
ci.relname AS index_name,
|
||||||
i.indisunique AS unique,
|
i.indisunique AS unique,
|
||||||
i.indisprimary AS primary,
|
i.indisprimary AS primary,
|
||||||
a.attname AS column_name
|
a.attname AS column_name,
|
||||||
|
array_position(i.indkey, a.attnum) AS column_position,
|
||||||
|
pg_get_expr(i.indpred, i.indrelid) AS predicate
|
||||||
FROM
|
FROM
|
||||||
pg_index i
|
pg_index i
|
||||||
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
|
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
|
||||||
@@ -231,9 +235,10 @@ WHERE
|
|||||||
a.attnum = ANY(i.indkey)
|
a.attnum = ANY(i.indkey)
|
||||||
AND con.oid IS NULL
|
AND con.oid IS NULL
|
||||||
AND ct.relkind = 'r'
|
AND ct.relkind = 'r'
|
||||||
AND ct.relname = ?`, string(name))
|
AND ct.relname = ?
|
||||||
|
ORDER BY index_name, column_position`, string(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, provider.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "no indices for table (%s) found", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -242,7 +247,12 @@ WHERE
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
uniqueIndicesMap := make(map[string]*sqlschema.UniqueIndex)
|
type indexEntry struct {
|
||||||
|
columns []sqlschema.ColumnName
|
||||||
|
predicate *string
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueIndicesMap := make(map[string]*indexEntry)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var (
|
var (
|
||||||
tableName string
|
tableName string
|
||||||
@@ -250,27 +260,53 @@ WHERE
|
|||||||
unique bool
|
unique bool
|
||||||
primary bool
|
primary bool
|
||||||
columnName string
|
columnName string
|
||||||
|
// starts from 0 and is unused in this function, this is to ensure that the column names are in the correct order
|
||||||
|
columnPosition int
|
||||||
|
predicate *string
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName); err != nil {
|
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName, &columnPosition, &predicate); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if unique {
|
if unique {
|
||||||
if _, ok := uniqueIndicesMap[indexName]; !ok {
|
if _, ok := uniqueIndicesMap[indexName]; !ok {
|
||||||
uniqueIndicesMap[indexName] = &sqlschema.UniqueIndex{
|
uniqueIndicesMap[indexName] = &indexEntry{
|
||||||
TableName: name,
|
columns: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
|
||||||
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
|
predicate: predicate,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
uniqueIndicesMap[indexName].ColumnNames = append(uniqueIndicesMap[indexName].ColumnNames, sqlschema.ColumnName(columnName))
|
uniqueIndicesMap[indexName].columns = append(uniqueIndicesMap[indexName].columns, sqlschema.ColumnName(columnName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
indices := make([]sqlschema.Index, 0)
|
indices := make([]sqlschema.Index, 0)
|
||||||
for _, index := range uniqueIndicesMap {
|
for indexName, entry := range uniqueIndicesMap {
|
||||||
indices = append(indices, index)
|
if entry.predicate != nil {
|
||||||
|
index := &sqlschema.PartialUniqueIndex{
|
||||||
|
TableName: name,
|
||||||
|
ColumnNames: entry.columns,
|
||||||
|
Where: *entry.predicate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if index.Name() == indexName {
|
||||||
|
indices = append(indices, index)
|
||||||
|
} else {
|
||||||
|
indices = append(indices, index.Named(indexName))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index := &sqlschema.UniqueIndex{
|
||||||
|
TableName: name,
|
||||||
|
ColumnNames: entry.columns,
|
||||||
|
}
|
||||||
|
|
||||||
|
if index.Name() == indexName {
|
||||||
|
indices = append(indices, index)
|
||||||
|
} else {
|
||||||
|
indices = append(indices, index.Named(indexName))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return indices, nil
|
return indices, nil
|
||||||
|
|||||||
29
frontend/__mocks__/resizableMock.tsx
Normal file
29
frontend/__mocks__/resizableMock.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
type CommonProps = PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
minSize?: number;
|
||||||
|
maxSize?: number;
|
||||||
|
defaultSize?: number;
|
||||||
|
direction?: 'horizontal' | 'vertical';
|
||||||
|
autoSaveId?: string;
|
||||||
|
withHandle?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function ResizablePanelGroup({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: CommonProps): JSX.Element {
|
||||||
|
return <div className={className}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResizablePanel({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: CommonProps): JSX.Element {
|
||||||
|
return <div className={className}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResizableHandle({ className }: CommonProps): JSX.Element {
|
||||||
|
return <div className={className} />;
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ const config: Config.InitialOptions = {
|
|||||||
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
||||||
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
||||||
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
|
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
|
||||||
|
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
|
||||||
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||||
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||||
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"prettify": "prettier --write .",
|
"prettify": "prettier --write .",
|
||||||
"fmt": "prettier --check .",
|
"fmt": "prettier --check .",
|
||||||
"lint": "eslint ./src",
|
"lint": "eslint ./src",
|
||||||
|
"lint:generated": "eslint ./src/api/generated --fix",
|
||||||
"lint:fix": "eslint ./src --fix",
|
"lint:fix": "eslint ./src --fix",
|
||||||
"jest": "jest",
|
"jest": "jest",
|
||||||
"jest:coverage": "jest --coverage",
|
"jest:coverage": "jest --coverage",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"@signozhq/sonner": "0.1.0",
|
"@signozhq/sonner": "0.1.0",
|
||||||
"@signozhq/switch": "0.0.2",
|
"@signozhq/switch": "0.0.2",
|
||||||
"@signozhq/table": "0.3.7",
|
"@signozhq/table": "0.3.7",
|
||||||
"@signozhq/toggle-group": "^0.0.1",
|
"@signozhq/toggle-group": "0.0.1",
|
||||||
"@signozhq/tooltip": "0.0.2",
|
"@signozhq/tooltip": "0.0.2",
|
||||||
"@tanstack/react-table": "8.20.6",
|
"@tanstack/react-table": "8.20.6",
|
||||||
"@tanstack/react-virtual": "3.11.2",
|
"@tanstack/react-virtual": "3.11.2",
|
||||||
|
|||||||
2021
frontend/public/Images/allInOneLightMode.svg
Normal file
2021
frontend/public/Images/allInOneLightMode.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 214 KiB |
@@ -25,7 +25,7 @@ echo "\n✅ Prettier formatting successful"
|
|||||||
|
|
||||||
# Fix linting issues
|
# Fix linting issues
|
||||||
echo "\n\n---\nRunning eslint...\n"
|
echo "\n\n---\nRunning eslint...\n"
|
||||||
if ! yarn lint --fix --quiet src/api/generated; then
|
if ! yarn lint:generated; then
|
||||||
echo "ESLint check failed! Please fix linting errors before proceeding."
|
echo "ESLint check failed! Please fix linting errors before proceeding."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
|||||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||||
import type {
|
import type {
|
||||||
AuthtypesPatchableObjectsDTO,
|
AuthtypesPatchableObjectsDTO,
|
||||||
|
AuthtypesPatchableRoleDTO,
|
||||||
|
AuthtypesPostableRoleDTO,
|
||||||
CreateRole201,
|
CreateRole201,
|
||||||
DeleteRolePathParameters,
|
DeleteRolePathParameters,
|
||||||
GetObjects200,
|
GetObjects200,
|
||||||
@@ -31,8 +33,6 @@ import type {
|
|||||||
PatchObjectsPathParameters,
|
PatchObjectsPathParameters,
|
||||||
PatchRolePathParameters,
|
PatchRolePathParameters,
|
||||||
RenderErrorResponseDTO,
|
RenderErrorResponseDTO,
|
||||||
RoletypesPatchableRoleDTO,
|
|
||||||
RoletypesPostableRoleDTO,
|
|
||||||
} from '../sigNoz.schemas';
|
} from '../sigNoz.schemas';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,14 +118,14 @@ export const invalidateListRoles = async (
|
|||||||
* @summary Create role
|
* @summary Create role
|
||||||
*/
|
*/
|
||||||
export const createRole = (
|
export const createRole = (
|
||||||
roletypesPostableRoleDTO: BodyType<RoletypesPostableRoleDTO>,
|
authtypesPostableRoleDTO: BodyType<AuthtypesPostableRoleDTO>,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
) => {
|
) => {
|
||||||
return GeneratedAPIInstance<CreateRole201>({
|
return GeneratedAPIInstance<CreateRole201>({
|
||||||
url: `/api/v1/roles`,
|
url: `/api/v1/roles`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
data: roletypesPostableRoleDTO,
|
data: authtypesPostableRoleDTO,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -137,13 +137,13 @@ export const getCreateRoleMutationOptions = <
|
|||||||
mutation?: UseMutationOptions<
|
mutation?: UseMutationOptions<
|
||||||
Awaited<ReturnType<typeof createRole>>,
|
Awaited<ReturnType<typeof createRole>>,
|
||||||
TError,
|
TError,
|
||||||
{ data: BodyType<RoletypesPostableRoleDTO> },
|
{ data: BodyType<AuthtypesPostableRoleDTO> },
|
||||||
TContext
|
TContext
|
||||||
>;
|
>;
|
||||||
}): UseMutationOptions<
|
}): UseMutationOptions<
|
||||||
Awaited<ReturnType<typeof createRole>>,
|
Awaited<ReturnType<typeof createRole>>,
|
||||||
TError,
|
TError,
|
||||||
{ data: BodyType<RoletypesPostableRoleDTO> },
|
{ data: BodyType<AuthtypesPostableRoleDTO> },
|
||||||
TContext
|
TContext
|
||||||
> => {
|
> => {
|
||||||
const mutationKey = ['createRole'];
|
const mutationKey = ['createRole'];
|
||||||
@@ -157,7 +157,7 @@ export const getCreateRoleMutationOptions = <
|
|||||||
|
|
||||||
const mutationFn: MutationFunction<
|
const mutationFn: MutationFunction<
|
||||||
Awaited<ReturnType<typeof createRole>>,
|
Awaited<ReturnType<typeof createRole>>,
|
||||||
{ data: BodyType<RoletypesPostableRoleDTO> }
|
{ data: BodyType<AuthtypesPostableRoleDTO> }
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const { data } = props ?? {};
|
const { data } = props ?? {};
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ export const getCreateRoleMutationOptions = <
|
|||||||
export type CreateRoleMutationResult = NonNullable<
|
export type CreateRoleMutationResult = NonNullable<
|
||||||
Awaited<ReturnType<typeof createRole>>
|
Awaited<ReturnType<typeof createRole>>
|
||||||
>;
|
>;
|
||||||
export type CreateRoleMutationBody = BodyType<RoletypesPostableRoleDTO>;
|
export type CreateRoleMutationBody = BodyType<AuthtypesPostableRoleDTO>;
|
||||||
export type CreateRoleMutationError = ErrorType<RenderErrorResponseDTO>;
|
export type CreateRoleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,13 +183,13 @@ export const useCreateRole = <
|
|||||||
mutation?: UseMutationOptions<
|
mutation?: UseMutationOptions<
|
||||||
Awaited<ReturnType<typeof createRole>>,
|
Awaited<ReturnType<typeof createRole>>,
|
||||||
TError,
|
TError,
|
||||||
{ data: BodyType<RoletypesPostableRoleDTO> },
|
{ data: BodyType<AuthtypesPostableRoleDTO> },
|
||||||
TContext
|
TContext
|
||||||
>;
|
>;
|
||||||
}): UseMutationResult<
|
}): UseMutationResult<
|
||||||
Awaited<ReturnType<typeof createRole>>,
|
Awaited<ReturnType<typeof createRole>>,
|
||||||
TError,
|
TError,
|
||||||
{ data: BodyType<RoletypesPostableRoleDTO> },
|
{ data: BodyType<AuthtypesPostableRoleDTO> },
|
||||||
TContext
|
TContext
|
||||||
> => {
|
> => {
|
||||||
const mutationOptions = getCreateRoleMutationOptions(options);
|
const mutationOptions = getCreateRoleMutationOptions(options);
|
||||||
@@ -370,13 +370,13 @@ export const invalidateGetRole = async (
|
|||||||
*/
|
*/
|
||||||
export const patchRole = (
|
export const patchRole = (
|
||||||
{ id }: PatchRolePathParameters,
|
{ id }: PatchRolePathParameters,
|
||||||
roletypesPatchableRoleDTO: BodyType<RoletypesPatchableRoleDTO>,
|
authtypesPatchableRoleDTO: BodyType<AuthtypesPatchableRoleDTO>,
|
||||||
) => {
|
) => {
|
||||||
return GeneratedAPIInstance<string>({
|
return GeneratedAPIInstance<string>({
|
||||||
url: `/api/v1/roles/${id}`,
|
url: `/api/v1/roles/${id}`,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
data: roletypesPatchableRoleDTO,
|
data: authtypesPatchableRoleDTO,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -389,7 +389,7 @@ export const getPatchRoleMutationOptions = <
|
|||||||
TError,
|
TError,
|
||||||
{
|
{
|
||||||
pathParams: PatchRolePathParameters;
|
pathParams: PatchRolePathParameters;
|
||||||
data: BodyType<RoletypesPatchableRoleDTO>;
|
data: BodyType<AuthtypesPatchableRoleDTO>;
|
||||||
},
|
},
|
||||||
TContext
|
TContext
|
||||||
>;
|
>;
|
||||||
@@ -398,7 +398,7 @@ export const getPatchRoleMutationOptions = <
|
|||||||
TError,
|
TError,
|
||||||
{
|
{
|
||||||
pathParams: PatchRolePathParameters;
|
pathParams: PatchRolePathParameters;
|
||||||
data: BodyType<RoletypesPatchableRoleDTO>;
|
data: BodyType<AuthtypesPatchableRoleDTO>;
|
||||||
},
|
},
|
||||||
TContext
|
TContext
|
||||||
> => {
|
> => {
|
||||||
@@ -415,7 +415,7 @@ export const getPatchRoleMutationOptions = <
|
|||||||
Awaited<ReturnType<typeof patchRole>>,
|
Awaited<ReturnType<typeof patchRole>>,
|
||||||
{
|
{
|
||||||
pathParams: PatchRolePathParameters;
|
pathParams: PatchRolePathParameters;
|
||||||
data: BodyType<RoletypesPatchableRoleDTO>;
|
data: BodyType<AuthtypesPatchableRoleDTO>;
|
||||||
}
|
}
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const { pathParams, data } = props ?? {};
|
const { pathParams, data } = props ?? {};
|
||||||
@@ -429,7 +429,7 @@ export const getPatchRoleMutationOptions = <
|
|||||||
export type PatchRoleMutationResult = NonNullable<
|
export type PatchRoleMutationResult = NonNullable<
|
||||||
Awaited<ReturnType<typeof patchRole>>
|
Awaited<ReturnType<typeof patchRole>>
|
||||||
>;
|
>;
|
||||||
export type PatchRoleMutationBody = BodyType<RoletypesPatchableRoleDTO>;
|
export type PatchRoleMutationBody = BodyType<AuthtypesPatchableRoleDTO>;
|
||||||
export type PatchRoleMutationError = ErrorType<RenderErrorResponseDTO>;
|
export type PatchRoleMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -444,7 +444,7 @@ export const usePatchRole = <
|
|||||||
TError,
|
TError,
|
||||||
{
|
{
|
||||||
pathParams: PatchRolePathParameters;
|
pathParams: PatchRolePathParameters;
|
||||||
data: BodyType<RoletypesPatchableRoleDTO>;
|
data: BodyType<AuthtypesPatchableRoleDTO>;
|
||||||
},
|
},
|
||||||
TContext
|
TContext
|
||||||
>;
|
>;
|
||||||
@@ -453,7 +453,7 @@ export const usePatchRole = <
|
|||||||
TError,
|
TError,
|
||||||
{
|
{
|
||||||
pathParams: PatchRolePathParameters;
|
pathParams: PatchRolePathParameters;
|
||||||
data: BodyType<RoletypesPatchableRoleDTO>;
|
data: BodyType<AuthtypesPatchableRoleDTO>;
|
||||||
},
|
},
|
||||||
TContext
|
TContext
|
||||||
> => {
|
> => {
|
||||||
|
|||||||
@@ -278,6 +278,13 @@ export interface AuthtypesPatchableObjectsDTO {
|
|||||||
deletions: AuthtypesGettableObjectsDTO[] | null;
|
deletions: AuthtypesGettableObjectsDTO[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthtypesPatchableRoleDTO {
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuthtypesPostableAuthDomainDTO {
|
export interface AuthtypesPostableAuthDomainDTO {
|
||||||
config?: AuthtypesAuthDomainConfigDTO;
|
config?: AuthtypesAuthDomainConfigDTO;
|
||||||
/**
|
/**
|
||||||
@@ -301,6 +308,17 @@ export interface AuthtypesPostableEmailPasswordSessionDTO {
|
|||||||
password?: string;
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthtypesPostableRoleDTO {
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuthtypesPostableRotateTokenDTO {
|
export interface AuthtypesPostableRotateTokenDTO {
|
||||||
/**
|
/**
|
||||||
* @type string
|
* @type string
|
||||||
@@ -319,6 +337,39 @@ export interface AuthtypesResourceDTO {
|
|||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthtypesRoleDTO {
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
* @format date-time
|
||||||
|
*/
|
||||||
|
createdAt?: Date;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
orgId: string;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
* @format date-time
|
||||||
|
*/
|
||||||
|
updatedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @nullable
|
* @nullable
|
||||||
*/
|
*/
|
||||||
@@ -2039,57 +2090,6 @@ export interface RenderErrorResponseDTO {
|
|||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoletypesPatchableRoleDTO {
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RoletypesPostableRoleDTO {
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
description?: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RoletypesRoleDTO {
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
* @format date-time
|
|
||||||
*/
|
|
||||||
createdAt?: Date;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
orgId: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
*/
|
|
||||||
type: string;
|
|
||||||
/**
|
|
||||||
* @type string
|
|
||||||
* @format date-time
|
|
||||||
*/
|
|
||||||
updatedAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServiceaccounttypesFactorAPIKeyDTO {
|
export interface ServiceaccounttypesFactorAPIKeyDTO {
|
||||||
/**
|
/**
|
||||||
* @type string
|
* @type string
|
||||||
@@ -3163,7 +3163,7 @@ export type ListRoles200 = {
|
|||||||
/**
|
/**
|
||||||
* @type array
|
* @type array
|
||||||
*/
|
*/
|
||||||
data: RoletypesRoleDTO[];
|
data: AuthtypesRoleDTO[];
|
||||||
/**
|
/**
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
@@ -3185,7 +3185,7 @@ export type GetRolePathParameters = {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
export type GetRole200 = {
|
export type GetRole200 = {
|
||||||
data: RoletypesRoleDTO;
|
data: AuthtypesRoleDTO;
|
||||||
/**
|
/**
|
||||||
* @type string
|
* @type string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -302,7 +302,6 @@ function CustomTimePicker({
|
|||||||
): void => {
|
): void => {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
|
|
||||||
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
||||||
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// ** Helpers
|
// ** Helpers
|
||||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
|
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
||||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||||
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
|
||||||
@@ -548,3 +549,49 @@ export const DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY: Record<
|
|||||||
[DataTypes.ArrayBool]: 'boolAttributeValues',
|
[DataTypes.ArrayBool]: 'boolAttributeValues',
|
||||||
[DataTypes.EMPTY]: 'stringAttributeValues',
|
[DataTypes.EMPTY]: 'stringAttributeValues',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const listViewInitialLogQuery: Query = {
|
||||||
|
...initialQueriesMap.logs,
|
||||||
|
builder: {
|
||||||
|
...initialQueriesMap.logs.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueriesMap.logs.builder.queryData[0],
|
||||||
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
|
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PANEL_TYPES_INITIAL_QUERY: Record<PANEL_TYPES, Query> = {
|
||||||
|
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.LIST]: listViewInitialLogQuery,
|
||||||
|
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
||||||
|
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
|
||||||
|
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listViewInitialTraceQuery: Query = {
|
||||||
|
// it should be the above commented query
|
||||||
|
...initialQueriesMap.traces,
|
||||||
|
builder: {
|
||||||
|
...initialQueriesMap.traces.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueriesMap.traces.builder.queryData[0],
|
||||||
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
|
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
selectColumns: defaultTraceSelectedColumns,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
|
||||||
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
|
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
|
||||||
|
|
||||||
export const PANEL_TYPES_INITIAL_QUERY = {
|
|
||||||
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
|
|
||||||
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
|
||||||
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
|
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listViewInitialLogQuery: Query = {
|
|
||||||
...initialQueriesMap.logs,
|
|
||||||
builder: {
|
|
||||||
...initialQueriesMap.logs.builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...initialQueriesMap.logs.builder.queryData[0],
|
|
||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listViewInitialTraceQuery: Query = {
|
|
||||||
// it should be the above commented query
|
|
||||||
...initialQueriesMap.traces,
|
|
||||||
builder: {
|
|
||||||
...initialQueriesMap.traces.builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...initialQueriesMap.traces.builder.queryData[0],
|
|
||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 10,
|
|
||||||
selectColumns: defaultTraceSelectedColumns,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { Card, Modal } from 'antd';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
|
||||||
import { QueryParams } from 'constants/query';
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
|
||||||
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
|
|
||||||
import menuItems from './menuItems';
|
|
||||||
import { Text } from './styles';
|
|
||||||
|
|
||||||
import './ComponentSlider.styles.scss';
|
|
||||||
|
|
||||||
function DashboardGraphSlider(): JSX.Element {
|
|
||||||
const { handleToggleDashboardSlider, isDashboardSliderOpen } = useDashboard();
|
|
||||||
|
|
||||||
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
|
||||||
const id = uuid();
|
|
||||||
handleToggleDashboardSlider(false);
|
|
||||||
logEvent('Dashboard Detail: New panel type selected', {
|
|
||||||
// dashboardId: '',
|
|
||||||
// dashboardName: '',
|
|
||||||
// numberOfPanels: 0, // todo - at this point we don't know these attributes
|
|
||||||
panelType: name,
|
|
||||||
widgetId: id,
|
|
||||||
});
|
|
||||||
const queryParamsLog = {
|
|
||||||
graphType: name,
|
|
||||||
widgetId: id,
|
|
||||||
[QueryParams.compositeQuery]: JSON.stringify({
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
builder: {
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name].builder,
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
|
|
||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
offset: 0,
|
|
||||||
pageSize: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryParams = {
|
|
||||||
graphType: name,
|
|
||||||
widgetId: id,
|
|
||||||
[QueryParams.compositeQuery]: JSON.stringify(
|
|
||||||
PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
),
|
|
||||||
};
|
|
||||||
if (name === PANEL_TYPES.LIST) {
|
|
||||||
history.push(
|
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
history.push(
|
|
||||||
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCardClick = (panelType: PANEL_TYPES): void => {
|
|
||||||
onClickHandler(panelType)();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={isDashboardSliderOpen}
|
|
||||||
onCancel={(): void => {
|
|
||||||
handleToggleDashboardSlider(false);
|
|
||||||
}}
|
|
||||||
rootClassName="graph-selection"
|
|
||||||
footer={null}
|
|
||||||
title="New Panel"
|
|
||||||
>
|
|
||||||
<div className="panel-selection">
|
|
||||||
{menuItems.map(({ name, icon, display }) => (
|
|
||||||
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
|
|
||||||
{icon}
|
|
||||||
<Text>{display}</Text>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DashboardGraphSlider;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { Card as CardComponent, Typography } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: right;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Card = styled(CardComponent)`
|
|
||||||
min-height: 80px;
|
|
||||||
min-width: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
|
|
||||||
.ant-card-body {
|
|
||||||
padding: 12px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.ant-typography {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
border: 1px solid var(--bg-robin-400);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Text = styled(Typography)`
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
`;
|
|
||||||
@@ -182,9 +182,7 @@ describe('Dashboard landing page actions header tests', () => {
|
|||||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
|
||||||
const mockContextValue: IDashboardContext = {
|
const mockContextValue: IDashboardContext = {
|
||||||
isDashboardSliderOpen: false,
|
|
||||||
isDashboardLocked: false,
|
isDashboardLocked: false,
|
||||||
handleToggleDashboardSlider: jest.fn(),
|
|
||||||
handleDashboardLockToggle: jest.fn(),
|
handleDashboardLockToggle: jest.fn(),
|
||||||
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
|
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
|
||||||
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
|
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { sortLayout } from 'providers/Dashboard/util';
|
import { sortLayout } from 'providers/Dashboard/util';
|
||||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||||
import { Props } from 'types/api/dashboard/update';
|
import { Props } from 'types/api/dashboard/update';
|
||||||
@@ -48,10 +49,10 @@ import { ComponentTypes } from 'utils/permission';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
|
||||||
import DashboardGraphSlider from '../ComponentsSlider';
|
|
||||||
import DashboardSettings from '../DashboardSettings';
|
import DashboardSettings from '../DashboardSettings';
|
||||||
import { Base64Icons } from '../DashboardSettings/General/utils';
|
import { Base64Icons } from '../DashboardSettings/General/utils';
|
||||||
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
import DashboardVariableSelection from '../DashboardVariablesSelection';
|
||||||
|
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
|
||||||
import SettingsDrawer from './SettingsDrawer';
|
import SettingsDrawer from './SettingsDrawer';
|
||||||
import { VariablesSettingsTab } from './types';
|
import { VariablesSettingsTab } from './types';
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,9 @@ interface DashboardDescriptionProps {
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||||
const { handle } = props;
|
const { handle } = props;
|
||||||
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
selectedDashboard,
|
selectedDashboard,
|
||||||
panelMap,
|
panelMap,
|
||||||
@@ -77,7 +81,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
setLayouts,
|
setLayouts,
|
||||||
isDashboardLocked,
|
isDashboardLocked,
|
||||||
setSelectedDashboard,
|
setSelectedDashboard,
|
||||||
handleToggleDashboardSlider,
|
|
||||||
handleDashboardLockToggle,
|
handleDashboardLockToggle,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
@@ -145,14 +148,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
dashboardId: selectedDashboard?.id,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleToggleDashboardSlider]);
|
}, [setIsPanelTypeSelectionModalOpen]);
|
||||||
|
|
||||||
const handleLockDashboardToggle = (): void => {
|
const handleLockDashboardToggle = (): void => {
|
||||||
setIsDashbordSettingsOpen(false);
|
setIsDashbordSettingsOpen(false);
|
||||||
@@ -521,7 +524,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
<DashboardVariableSelection />
|
<DashboardVariableSelection />
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
<DashboardGraphSlider />
|
<PanelTypeSelectionModal />
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={isRenameDashboardOpen}
|
open={isRenameDashboardOpen}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from 'hooks/dashboard/useDashboardVariables';
|
} from 'hooks/dashboard/useDashboardVariables';
|
||||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
|
||||||
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||||
import {
|
import {
|
||||||
enqueueDescendantsOfVariable,
|
enqueueDescendantsOfVariable,
|
||||||
@@ -30,7 +29,7 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
const { updateUrlVariable } = useVariablesFromUrl();
|
||||||
|
|
||||||
const { dashboardVariables } = useDashboardVariables();
|
const { dashboardVariables } = useDashboardVariables();
|
||||||
const dashboardId = useDashboardVariablesSelector(
|
const dashboardId = useDashboardVariablesSelector(
|
||||||
@@ -50,15 +49,6 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize variables with default values if not in URL
|
|
||||||
initializeDefaultVariables(
|
|
||||||
dashboardVariables,
|
|
||||||
getUrlVariables,
|
|
||||||
updateUrlVariable,
|
|
||||||
);
|
|
||||||
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
|
|
||||||
|
|
||||||
// Memoize the order key to avoid unnecessary triggers
|
// Memoize the order key to avoid unnecessary triggers
|
||||||
const variableOrderKey = useMemo(() => {
|
const variableOrderKey = useMemo(() => {
|
||||||
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
|
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.graph-selection {
|
.panel-type-selection-modal {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
width: 515px;
|
width: 515px;
|
||||||
max-height: 646px;
|
max-height: 646px;
|
||||||
@@ -76,6 +76,11 @@
|
|||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-type-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.graph-selection {
|
.panel-type-selection-modal {
|
||||||
.ant-modal-content {
|
.ant-modal-content {
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { Card, Modal, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import { PANEL_TYPES, PANEL_TYPES_INITIAL_QUERY } from 'constants/queryBuilder';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { PanelTypesWithData } from './menuItems';
|
||||||
|
|
||||||
|
import './PanelTypeSelectionModal.styles.scss';
|
||||||
|
|
||||||
|
function PanelTypeSelectionModal(): JSX.Element {
|
||||||
|
const {
|
||||||
|
isPanelTypeSelectionModalOpen,
|
||||||
|
setIsPanelTypeSelectionModalOpen,
|
||||||
|
} = usePanelTypeSelectionModalStore();
|
||||||
|
|
||||||
|
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
||||||
|
const id = uuid();
|
||||||
|
setIsPanelTypeSelectionModalOpen(false);
|
||||||
|
logEvent('Dashboard Detail: New panel type selected', {
|
||||||
|
panelType: name,
|
||||||
|
widgetId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryParams = {
|
||||||
|
graphType: name,
|
||||||
|
widgetId: id,
|
||||||
|
[QueryParams.compositeQuery]: JSON.stringify(
|
||||||
|
PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
history.push(
|
||||||
|
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = (panelType: PANEL_TYPES): void => {
|
||||||
|
onClickHandler(panelType)();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={isPanelTypeSelectionModalOpen}
|
||||||
|
onCancel={(): void => {
|
||||||
|
setIsPanelTypeSelectionModalOpen(false);
|
||||||
|
}}
|
||||||
|
rootClassName="panel-type-selection-modal"
|
||||||
|
footer={null}
|
||||||
|
title="New Panel"
|
||||||
|
>
|
||||||
|
<div className="panel-selection">
|
||||||
|
{PanelTypesWithData.map(({ name, icon, display }) => (
|
||||||
|
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
|
||||||
|
{icon}
|
||||||
|
<Typography className="panel-type-text">{display}</Typography>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PanelTypeSelectionModal);
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const Items: ItemsProps[] = [
|
export const PanelTypesWithData: ItemsProps[] = [
|
||||||
{
|
{
|
||||||
name: PANEL_TYPES.TIME_SERIES,
|
name: PANEL_TYPES.TIME_SERIES,
|
||||||
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
|
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
|
||||||
@@ -52,5 +52,3 @@ export interface ItemsProps {
|
|||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
display: string;
|
display: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Items;
|
|
||||||
@@ -224,7 +224,7 @@ describe('TimeSeriesPanel utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses DrawStyle.Line and VisibilityMode.Never when series has multiple valid points', () => {
|
it('uses DrawStyle.Line and showPoints false when series has multiple valid points', () => {
|
||||||
const apiResponse = createApiResponse([
|
const apiResponse = createApiResponse([
|
||||||
{
|
{
|
||||||
metric: {},
|
metric: {},
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import getLabelName from 'lib/getLabelName';
|
|||||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import {
|
import {
|
||||||
DrawStyle,
|
DrawStyle,
|
||||||
|
FillMode,
|
||||||
LineInterpolation,
|
LineInterpolation,
|
||||||
LineStyle,
|
LineStyle,
|
||||||
VisibilityMode,
|
|
||||||
} from 'lib/uPlotV2/config/types';
|
} from 'lib/uPlotV2/config/types';
|
||||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||||
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
|
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
|
||||||
@@ -124,12 +124,12 @@ export const prepareUPlotConfig = ({
|
|||||||
label: label,
|
label: label,
|
||||||
colorMapping: widget.customLegendColors ?? {},
|
colorMapping: widget.customLegendColors ?? {},
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
lineStyle: LineStyle.Solid,
|
lineStyle: widget.lineStyle || LineStyle.Solid,
|
||||||
lineInterpolation: LineInterpolation.Spline,
|
lineInterpolation: widget.lineInterpolation || LineInterpolation.Spline,
|
||||||
showPoints: hasSingleValidPoint
|
showPoints:
|
||||||
? VisibilityMode.Always
|
widget.showPoints || hasSingleValidPoint ? true : !!widget.showPoints,
|
||||||
: VisibilityMode.Never,
|
|
||||||
pointSize: 5,
|
pointSize: 5,
|
||||||
|
fillMode: widget.fillMode || FillMode.None,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
|
|||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
|
|
||||||
import './DashboardEmptyState.styles.scss';
|
import './DashboardEmptyState.styles.scss';
|
||||||
|
|
||||||
export default function DashboardEmptyState(): JSX.Element {
|
export default function DashboardEmptyState(): JSX.Element {
|
||||||
const {
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
selectedDashboard,
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
isDashboardLocked,
|
);
|
||||||
handleToggleDashboardSlider,
|
|
||||||
} = useDashboard();
|
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||||
@@ -41,14 +42,14 @@ export default function DashboardEmptyState(): JSX.Element {
|
|||||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(() => {
|
const onEmptyWidgetHandler = useCallback(() => {
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||||
dashboardId: selectedDashboard?.id,
|
dashboardId: selectedDashboard?.id,
|
||||||
dashboardName: selectedDashboard?.data.title,
|
dashboardName: selectedDashboard?.data.title,
|
||||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleToggleDashboardSlider]);
|
}, [setIsPanelTypeSelectionModalOpen]);
|
||||||
|
|
||||||
const onConfigureClick = useCallback((): void => {
|
const onConfigureClick = useCallback((): void => {
|
||||||
setIsSettingsDrawerOpen(true);
|
setIsSettingsDrawerOpen(true);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { Select, Typography } from 'antd';
|
import { Select, Typography } from 'antd';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GraphTypes from 'container/DashboardContainer/ComponentsSlider/menuItems';
|
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
|
||||||
import { handleQueryChange } from 'container/NewWidget/utils';
|
import { handleQueryChange } from 'container/NewWidget/utils';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@@ -59,7 +59,7 @@ function PanelTypeSelector({
|
|||||||
data-testid="panel-change-select"
|
data-testid="panel-change-select"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{GraphTypes.map((item) => (
|
{PanelTypesWithData.map((item) => (
|
||||||
<Option key={item.name} value={item.name}>
|
<Option key={item.name} value={item.name}>
|
||||||
<div className="view-panel-select-option">
|
<div className="view-panel-select-option">
|
||||||
<div className="icon">{item.icon}</div>
|
<div className="icon">{item.icon}</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
|||||||
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||||
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
|
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
@@ -34,11 +35,11 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
|||||||
} = props;
|
} = props;
|
||||||
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
|
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const {
|
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
|
||||||
handleToggleDashboardSlider,
|
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||||
selectedDashboard,
|
);
|
||||||
isDashboardLocked,
|
|
||||||
} = useDashboard();
|
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||||
|
|
||||||
const permissions: ComponentTypes[] = ['add_panel'];
|
const permissions: ComponentTypes[] = ['add_panel'];
|
||||||
const { user } = useAppContext();
|
const { user } = useAppContext();
|
||||||
@@ -87,7 +88,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelectedRowWidgetId(selectedDashboard.id, id);
|
setSelectedRowWidgetId(selectedDashboard.id, id);
|
||||||
handleToggleDashboardSlider(true);
|
setIsPanelTypeSelectionModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
New Panel
|
New Panel
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ROUTES from 'constants/routes';
|
|||||||
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
|
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
|
||||||
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
import { AnimatePresence } from 'motion/react';
|
import { AnimatePresence } from 'motion/react';
|
||||||
@@ -43,6 +44,7 @@ const homeInterval = 30 * 60 * 1000;
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export default function Home(): JSX.Element {
|
export default function Home(): JSX.Element {
|
||||||
const { user } = useAppContext();
|
const { user } = useAppContext();
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
const [startTime, setStartTime] = useState<number | null>(null);
|
||||||
const [endTime, setEndTime] = useState<number | null>(null);
|
const [endTime, setEndTime] = useState<number | null>(null);
|
||||||
@@ -680,7 +682,11 @@ export default function Home(): JSX.Element {
|
|||||||
|
|
||||||
<div className="checklist-img-container">
|
<div className="checklist-img-container">
|
||||||
<img
|
<img
|
||||||
src="/Images/allInOne.svg"
|
src={
|
||||||
|
isDarkMode
|
||||||
|
? '/Images/allInOne.svg'
|
||||||
|
: '/Images/allInOneLightMode.svg'
|
||||||
|
}
|
||||||
alt="checklist-img"
|
alt="checklist-img"
|
||||||
className="checklist-img"
|
className="checklist-img"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -65,6 +65,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-widget-container {
|
||||||
|
.resizable-panel-left-container {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizable-panel-right-container {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
min-width: 350px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.3rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(136, 136, 136);
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-resizable-panel-group {
|
||||||
|
.widget-resizable-handle {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.edit-header {
|
.edit-header {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
@@ -81,4 +110,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.widget-resizable-panel-group {
|
||||||
|
.bg-border {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.column-unit-selector {
|
.column-unit-selector {
|
||||||
margin-top: 16px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -30,6 +32,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -72,22 +72,24 @@ export function ColumnUnitSelector(
|
|||||||
return (
|
return (
|
||||||
<section className="column-unit-selector">
|
<section className="column-unit-selector">
|
||||||
<Typography.Text className="heading">Column Units</Typography.Text>
|
<Typography.Text className="heading">Column Units</Typography.Text>
|
||||||
{aggregationQueries.map(({ value, label }) => {
|
<div className="column-unit-selector-content">
|
||||||
const baseQueryName = value.split('.')[0];
|
{aggregationQueries.map(({ value, label }) => {
|
||||||
return (
|
const baseQueryName = value.split('.')[0];
|
||||||
<YAxisUnitSelectorV2
|
return (
|
||||||
value={columnUnits[value] || ''}
|
<YAxisUnitSelectorV2
|
||||||
onSelect={(unitValue: string): void =>
|
value={columnUnits[value] || ''}
|
||||||
handleColumnUnitSelect(value, unitValue)
|
onSelect={(unitValue: string): void =>
|
||||||
}
|
handleColumnUnitSelect(value, unitValue)
|
||||||
fieldLabel={label}
|
}
|
||||||
key={value}
|
fieldLabel={label}
|
||||||
selectedQueryName={baseQueryName}
|
key={value}
|
||||||
// Update the column unit value automatically only in create mode
|
selectedQueryName={baseQueryName}
|
||||||
shouldUpdateYAxisUnit={isNewDashboard}
|
// Update the column unit value automatically only in create mode
|
||||||
/>
|
shouldUpdateYAxisUnit={isNewDashboard}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ describe('ContextLinks Component', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that the component renders
|
|
||||||
expect(screen.getByText('Context Links')).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Check that the add button is present
|
// Check that the add button is present
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: /context link/i }),
|
screen.getByRole('button', { name: /context link/i }),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { Button, Modal, Typography } from 'antd';
|
import { Button, Modal } from 'antd';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
|
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
@@ -134,11 +134,16 @@ function ContextLinks({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="context-links-container">
|
<div className="context-links-container">
|
||||||
<Typography.Text className="context-links-text">
|
|
||||||
Context Links
|
|
||||||
</Typography.Text>
|
|
||||||
|
|
||||||
<div className="context-links-list">
|
<div className="context-links-list">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
className="add-context-link-button"
|
||||||
|
icon={<Plus size={12} />}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onClick={handleAddContextLink}
|
||||||
|
>
|
||||||
|
Add Context Link
|
||||||
|
</Button>
|
||||||
<OverlayScrollbar>
|
<OverlayScrollbar>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
@@ -160,16 +165,6 @@ function ContextLinks({
|
|||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
|
|
||||||
{/* button to add context link */}
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
className="add-context-link-button"
|
|
||||||
icon={<Plus size={12} />}
|
|
||||||
onClick={handleAddContextLink}
|
|
||||||
>
|
|
||||||
Context Link
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-links-text {
|
.context-links-text {
|
||||||
@@ -110,10 +109,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-context-link-button {
|
.add-context-link-button {
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
|
||||||
margin: auto;
|
|
||||||
width: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.fill-mode-selector {
|
||||||
|
.fill-mode-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-mode-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.fill-mode-selector {
|
||||||
|
.fill-mode-label {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { FillMode } from 'lib/uPlotV2/config/types';
|
||||||
|
|
||||||
|
import './FillModeSelector.styles.scss';
|
||||||
|
|
||||||
|
interface FillModeSelectorProps {
|
||||||
|
value: FillMode;
|
||||||
|
onChange: (value: FillMode) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FillModeSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: FillModeSelectorProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="fill-mode-selector control-container">
|
||||||
|
<Typography.Text className="section-heading">Fill mode</Typography.Text>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={value}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
onValueChange={(newValue): void => {
|
||||||
|
if (newValue) {
|
||||||
|
onChange(newValue as FillMode);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value={FillMode.None} aria-label="None" title="None">
|
||||||
|
<svg
|
||||||
|
className="fill-mode-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="8" y="16" width="32" height="16" stroke="#888" fill="none" />
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">None</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value={FillMode.Solid} aria-label="Solid" title="Solid">
|
||||||
|
<svg
|
||||||
|
className="fill-mode-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="8" y="16" width="32" height="16" fill="#888" />
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">Solid</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={FillMode.Gradient}
|
||||||
|
aria-label="Gradient"
|
||||||
|
title="Gradient"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="fill-mode-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="fill-gradient" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stopColor="#888" stopOpacity="0.2" />
|
||||||
|
<stop offset="100%" stopColor="#888" stopOpacity="0.8" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
x="8"
|
||||||
|
y="16"
|
||||||
|
width="32"
|
||||||
|
height="16"
|
||||||
|
fill="url(#fill-gradient)"
|
||||||
|
stroke="#888"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">
|
||||||
|
Gradient
|
||||||
|
</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.line-interpolation-selector {
|
||||||
|
.line-interpolation-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-interpolation-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.line-interpolation-selector {
|
||||||
|
.line-interpolation-label {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { LineInterpolation } from 'lib/uPlotV2/config/types';
|
||||||
|
|
||||||
|
import './LineInterpolationSelector.styles.scss';
|
||||||
|
|
||||||
|
interface LineInterpolationSelectorProps {
|
||||||
|
value: LineInterpolation;
|
||||||
|
onChange: (value: LineInterpolation) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LineInterpolationSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: LineInterpolationSelectorProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="line-interpolation-selector control-container">
|
||||||
|
<Typography.Text className="section-heading">
|
||||||
|
Line interpolation
|
||||||
|
</Typography.Text>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={value}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
onValueChange={(newValue): void => {
|
||||||
|
if (newValue) {
|
||||||
|
onChange(newValue as LineInterpolation);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={LineInterpolation.Linear}
|
||||||
|
aria-label="Linear"
|
||||||
|
title="Linear"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="line-interpolation-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="32" r="3" fill="#888" />
|
||||||
|
<circle cx="24" cy="16" r="3" fill="#888" />
|
||||||
|
<circle cx="40" cy="32" r="3" fill="#888" />
|
||||||
|
<path d="M8 32 L24 16 L40 32" stroke="#888" />
|
||||||
|
</svg>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value={LineInterpolation.Spline} aria-label="Spline">
|
||||||
|
<svg
|
||||||
|
className="line-interpolation-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="32" r="3" fill="#888" />
|
||||||
|
<circle cx="24" cy="16" r="3" fill="#888" />
|
||||||
|
<circle cx="40" cy="32" r="3" fill="#888" />
|
||||||
|
<path d="M8 32 C16 8, 32 8, 40 32" />
|
||||||
|
</svg>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={LineInterpolation.StepAfter}
|
||||||
|
aria-label="Step After"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="line-interpolation-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="32" r="3" fill="#888" />
|
||||||
|
<circle cx="24" cy="16" r="3" fill="#888" />
|
||||||
|
<circle cx="40" cy="32" r="3" fill="#888" />
|
||||||
|
<path d="M8 32 V16 H24 V32 H40" />
|
||||||
|
</svg>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={LineInterpolation.StepBefore}
|
||||||
|
aria-label="Step Before"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="line-interpolation-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="32" r="3" fill="#888" />
|
||||||
|
<circle cx="24" cy="16" r="3" fill="#888" />
|
||||||
|
<circle cx="40" cy="32" r="3" fill="#888" />
|
||||||
|
<path d="M8 32 H24 V16 H40 V32" />
|
||||||
|
</svg>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.line-style-selector {
|
||||||
|
.line-style-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-style-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.line-style-selector {
|
||||||
|
.line-style-label {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { LineStyle } from 'lib/uPlotV2/config/types';
|
||||||
|
|
||||||
|
import './LineStyleSelector.styles.scss';
|
||||||
|
|
||||||
|
interface LineStyleSelectorProps {
|
||||||
|
value: LineStyle;
|
||||||
|
onChange: (value: LineStyle) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LineStyleSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: LineStyleSelectorProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="line-style-selector control-container">
|
||||||
|
<Typography.Text className="section-heading">Line style</Typography.Text>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={value}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
onValueChange={(newValue): void => {
|
||||||
|
if (newValue) {
|
||||||
|
onChange(newValue as LineStyle);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value={LineStyle.Solid} aria-label="Solid" title="Solid">
|
||||||
|
<svg
|
||||||
|
className="line-style-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M8 24 L40 24" />
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">Solid</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={LineStyle.Dashed}
|
||||||
|
aria-label="Dashed"
|
||||||
|
title="Dashed"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="line-style-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeDasharray="6 4"
|
||||||
|
>
|
||||||
|
<path d="M8 24 L40 24" />
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">Dashed</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,34 @@
|
|||||||
.right-container {
|
.right-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
padding-bottom: 48px;
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 138.462% */
|
||||||
|
letter-spacing: 0.52px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-small {
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
word-break: initial;
|
||||||
|
line-height: 16px; /* 133.333% */
|
||||||
|
letter-spacing: 0.48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-type-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -24,86 +52,35 @@
|
|||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.control-container {
|
||||||
.name-description {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 12px 12px 16px 12px;
|
|
||||||
border-top: 1px solid var(--bg-slate-500);
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.typography {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 138.462% */
|
|
||||||
letter-spacing: 0.52px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-input {
|
|
||||||
display: flex;
|
|
||||||
padding: 6px 6px 6px 8px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
flex: 1 0 0;
|
|
||||||
align-self: stretch;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid var(--bg-slate-400);
|
|
||||||
background: var(--bg-ink-300);
|
|
||||||
color: var(--bg-vanilla-100);
|
|
||||||
font-family: Inter;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 128.571% */
|
|
||||||
letter-spacing: -0.07px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description-input {
|
|
||||||
border-style: unset;
|
|
||||||
.ant-input {
|
|
||||||
display: flex;
|
|
||||||
height: 80px;
|
|
||||||
padding: 6px 6px 6px 8px;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid var(--bg-slate-400);
|
|
||||||
background: var(--bg-ink-300);
|
|
||||||
color: var(--bg-vanilla-100);
|
|
||||||
font-family: Inter;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 128.571% */
|
|
||||||
letter-spacing: -0.07px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-config {
|
.panel-config {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 12px 12px 16px 12px;
|
|
||||||
gap: 8px;
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
|
|
||||||
.typography {
|
.toggle-card {
|
||||||
color: var(--bg-vanilla-400);
|
display: flex;
|
||||||
font-family: 'Space Mono';
|
flex-direction: row;
|
||||||
font-size: 13px;
|
justify-content: space-between;
|
||||||
font-style: normal;
|
align-items: center;
|
||||||
font-weight: 400;
|
padding: 12px;
|
||||||
line-height: 18px; /* 138.462% */
|
border-radius: 2px;
|
||||||
letter-spacing: 0.52px;
|
border: 1px solid var(--bg-slate-400);
|
||||||
text-transform: uppercase;
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-card-text-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-type-select {
|
.panel-type-select {
|
||||||
|
width: 100%;
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -115,98 +92,32 @@
|
|||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
background: var(--bg-ink-300);
|
background: var(--bg-ink-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
.icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display {
|
|
||||||
color: var(--bg-vanilla-100);
|
|
||||||
font-family: Inter;
|
|
||||||
font-size: 12px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 16px; /* 133.333% */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill-gaps {
|
.toggle-card-description {
|
||||||
margin-top: 16px;
|
color: var(--bg-vanilla-400);
|
||||||
display: flex;
|
font-family: Inter;
|
||||||
padding: 12px;
|
font-size: 12px;
|
||||||
justify-content: space-between;
|
font-style: normal;
|
||||||
align-items: center;
|
font-weight: 400;
|
||||||
border-radius: 2px;
|
opacity: 0.6;
|
||||||
border: 1px solid var(--bg-slate-400);
|
line-height: 16px; /* 133.333% */
|
||||||
background: var(--bg-ink-400);
|
|
||||||
|
|
||||||
.fill-gaps-text {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 138.462% */
|
|
||||||
letter-spacing: 0.52px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-scale,
|
.log-scale,
|
||||||
.decimal-precision-selector {
|
.decimal-precision-selector,
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-position {
|
.legend-position {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-colors {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-time-text {
|
|
||||||
margin-top: 16px;
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 138.462% */
|
|
||||||
letter-spacing: 0.52px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.y-axis-unit-selector,
|
.y-axis-unit-selector,
|
||||||
.y-axis-unit-selector-v2 {
|
.y-axis-unit-selector-v2 {
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
color: var(--bg-vanilla-400);
|
@extend .section-heading;
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 138.462% */
|
|
||||||
letter-spacing: 0.52px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@@ -259,7 +170,6 @@
|
|||||||
|
|
||||||
.text {
|
.text {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -278,113 +188,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stack-chart {
|
.stack-chart {
|
||||||
margin-top: 16px;
|
flex-direction: row;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 138.462% */
|
|
||||||
letter-spacing: 0.52px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bucket-config {
|
|
||||||
margin-top: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 138.462% */
|
|
||||||
letter-spacing: 0.52px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bucket-size-label {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bucket-input {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: 32px;
|
|
||||||
padding: 6px 6px 6px 8px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
align-self: stretch;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid var(--bg-slate-400);
|
|
||||||
background: var(--bg-ink-300);
|
|
||||||
|
|
||||||
.ant-input {
|
|
||||||
background: var(--bg-ink-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.combine-hist {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 8px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px; /* 138.462% */
|
|
||||||
letter-spacing: 0.52px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-links {
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alerts {
|
|
||||||
display: flex;
|
|
||||||
padding: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.left-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.bell-icon {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alerts-text {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: Inter;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 20px; /* 142.857% */
|
|
||||||
letter-spacing: 0.14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.plus-icon {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,42 +216,16 @@
|
|||||||
.lightMode {
|
.lightMode {
|
||||||
.right-container {
|
.right-container {
|
||||||
background-color: var(--bg-vanilla-100);
|
background-color: var(--bg-vanilla-100);
|
||||||
|
.section-heading {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
.header {
|
.header {
|
||||||
.header-text {
|
.header-text {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-description {
|
|
||||||
border-top: 1px solid var(--bg-vanilla-300);
|
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.typography {
|
|
||||||
color: var(--bg-ink-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-input {
|
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
|
||||||
background: var(--bg-vanilla-300);
|
|
||||||
color: var(--bg-ink-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
.description-input {
|
|
||||||
.ant-input {
|
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
|
||||||
background: var(--bg-vanilla-300);
|
|
||||||
color: var(--bg-ink-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-config {
|
.panel-config {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.typography {
|
|
||||||
color: var(--bg-ink-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-type-select {
|
.panel-type-select {
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
@@ -471,28 +250,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fill-gaps {
|
.toggle-card {
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
background: var(--bg-vanilla-300);
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
.fill-gaps-text {
|
.fill-gaps-text {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
}
|
.toggle-card-description {
|
||||||
|
|
||||||
.bucket-config {
|
|
||||||
.label {
|
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-input {
|
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
|
||||||
background: var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.ant-input {
|
|
||||||
background: var(--bg-vanilla-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-time-text {
|
.panel-time-text {
|
||||||
@@ -528,27 +295,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alerts {
|
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
|
|
||||||
.left-section {
|
|
||||||
.bell-icon {
|
|
||||||
color: var(--bg-ink-300);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alerts-text {
|
|
||||||
color: var(--bg-ink-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.plus-icon {
|
|
||||||
color: var(--bg-ink-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-links {
|
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
.select-option {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.alerts-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 44px;
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.alerts-section__left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.alerts-section__bell-icon {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-section__text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alerts-section__plus-icon {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.alerts-section {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.alerts-section__left {
|
||||||
|
.alerts-section__bell-icon {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alerts-section__text {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alerts-section__plus-icon {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import { ConciergeBell, Plus, SquareArrowOutUpRight } from 'lucide-react';
|
||||||
|
|
||||||
|
import './AlertsSection.styles.scss';
|
||||||
|
|
||||||
|
interface AlertsSectionProps {
|
||||||
|
onCreateAlertsHandler: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AlertsSection({
|
||||||
|
onCreateAlertsHandler,
|
||||||
|
}: AlertsSectionProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="alerts-section" onClick={onCreateAlertsHandler}>
|
||||||
|
<div className="alerts-section__left">
|
||||||
|
<ConciergeBell size={14} className="alerts-section__bell-icon" />
|
||||||
|
<Typography.Text className="alerts-section__text">Alerts</Typography.Text>
|
||||||
|
<SquareArrowOutUpRight size={10} className="info-icon" />
|
||||||
|
</div>
|
||||||
|
<Plus size={14} className="alerts-section__plus-icon" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { InputNumber, Select, Typography } from 'antd';
|
||||||
|
import { Axis3D, LineChart, Spline } from 'lucide-react';
|
||||||
|
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
|
||||||
|
enum LogScale {
|
||||||
|
LINEAR = 'linear',
|
||||||
|
LOGARITHMIC = 'logarithmic',
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface AxesSectionProps {
|
||||||
|
allowSoftMinMax: boolean;
|
||||||
|
allowLogScale: boolean;
|
||||||
|
softMin: number | null;
|
||||||
|
softMax: number | null;
|
||||||
|
setSoftMin: Dispatch<SetStateAction<number | null>>;
|
||||||
|
setSoftMax: Dispatch<SetStateAction<number | null>>;
|
||||||
|
isLogScale: boolean;
|
||||||
|
setIsLogScale: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AxesSection({
|
||||||
|
allowSoftMinMax,
|
||||||
|
allowLogScale,
|
||||||
|
softMin,
|
||||||
|
softMax,
|
||||||
|
setSoftMin,
|
||||||
|
setSoftMax,
|
||||||
|
isLogScale,
|
||||||
|
setIsLogScale,
|
||||||
|
}: AxesSectionProps): JSX.Element {
|
||||||
|
const softMinHandler = (value: number | null): void => {
|
||||||
|
setSoftMin(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const softMaxHandler = (value: number | null): void => {
|
||||||
|
setSoftMax(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
|
||||||
|
{allowSoftMinMax && (
|
||||||
|
<section className="soft-min-max">
|
||||||
|
<section className="container">
|
||||||
|
<Typography.Text className="text">Soft Min</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
|
type="number"
|
||||||
|
value={softMin}
|
||||||
|
onChange={softMinHandler}
|
||||||
|
rootClassName="input"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section className="container">
|
||||||
|
<Typography.Text className="text">Soft Max</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
|
value={softMax}
|
||||||
|
type="number"
|
||||||
|
rootClassName="input"
|
||||||
|
onChange={softMaxHandler}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowLogScale && (
|
||||||
|
<section className="log-scale control-container">
|
||||||
|
<Typography.Text className="section-heading">Y Axis Scale</Typography.Text>
|
||||||
|
<Select
|
||||||
|
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
|
||||||
|
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
|
||||||
|
className="panel-type-select"
|
||||||
|
defaultValue={LogScale.LINEAR}
|
||||||
|
>
|
||||||
|
<Option value={LogScale.LINEAR}>
|
||||||
|
<div className="select-option">
|
||||||
|
<div className="icon">
|
||||||
|
<LineChart size={16} />
|
||||||
|
</div>
|
||||||
|
<Typography.Text className="display">Linear</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
<Option value={LogScale.LOGARITHMIC}>
|
||||||
|
<div className="select-option">
|
||||||
|
<div className="icon">
|
||||||
|
<Spline size={16} />
|
||||||
|
</div>
|
||||||
|
<Typography.Text className="display">Logarithmic</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { Switch, Typography } from 'antd';
|
||||||
|
import {
|
||||||
|
FillMode,
|
||||||
|
LineInterpolation,
|
||||||
|
LineStyle,
|
||||||
|
} from 'lib/uPlotV2/config/types';
|
||||||
|
import { Paintbrush } from 'lucide-react';
|
||||||
|
|
||||||
|
import { FillModeSelector } from '../../components/FillModeSelector/FillModeSelector';
|
||||||
|
import { LineInterpolationSelector } from '../../components/LineInterpolationSelector/LineInterpolationSelector';
|
||||||
|
import { LineStyleSelector } from '../../components/LineStyleSelector/LineStyleSelector';
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
|
||||||
|
interface ChartAppearanceSectionProps {
|
||||||
|
fillMode: FillMode;
|
||||||
|
setFillMode: Dispatch<SetStateAction<FillMode>>;
|
||||||
|
lineStyle: LineStyle;
|
||||||
|
setLineStyle: Dispatch<SetStateAction<LineStyle>>;
|
||||||
|
lineInterpolation: LineInterpolation;
|
||||||
|
setLineInterpolation: Dispatch<SetStateAction<LineInterpolation>>;
|
||||||
|
showPoints: boolean;
|
||||||
|
setShowPoints: Dispatch<SetStateAction<boolean>>;
|
||||||
|
allowFillMode: boolean;
|
||||||
|
allowLineStyle: boolean;
|
||||||
|
allowLineInterpolation: boolean;
|
||||||
|
allowShowPoints: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChartAppearanceSection({
|
||||||
|
fillMode,
|
||||||
|
setFillMode,
|
||||||
|
lineStyle,
|
||||||
|
setLineStyle,
|
||||||
|
lineInterpolation,
|
||||||
|
setLineInterpolation,
|
||||||
|
showPoints,
|
||||||
|
setShowPoints,
|
||||||
|
allowFillMode,
|
||||||
|
allowLineStyle,
|
||||||
|
allowLineInterpolation,
|
||||||
|
allowShowPoints,
|
||||||
|
}: ChartAppearanceSectionProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<SettingsSection title="Chart Appearance" icon={<Paintbrush size={14} />}>
|
||||||
|
{allowFillMode && (
|
||||||
|
<FillModeSelector value={fillMode} onChange={setFillMode} />
|
||||||
|
)}
|
||||||
|
{allowLineStyle && (
|
||||||
|
<LineStyleSelector value={lineStyle} onChange={setLineStyle} />
|
||||||
|
)}
|
||||||
|
{allowLineInterpolation && (
|
||||||
|
<LineInterpolationSelector
|
||||||
|
value={lineInterpolation}
|
||||||
|
onChange={setLineInterpolation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{allowShowPoints && (
|
||||||
|
<section className="show-points toggle-card">
|
||||||
|
<div className="toggle-card-text-container">
|
||||||
|
<Typography.Text className="section-heading">Show points</Typography.Text>
|
||||||
|
<Typography.Text className="toggle-card-description">
|
||||||
|
Display individual data points on the chart
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Switch size="small" checked={showPoints} onChange={setShowPoints} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.context-links-section {
|
||||||
|
padding: 12px 12px 16px 12px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.context-links-section {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { Link as LinkIcon } from 'lucide-react';
|
||||||
|
import { ContextLinksData, Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
import ContextLinks from '../../ContextLinks';
|
||||||
|
|
||||||
|
import './ContextLinksSection.styles.scss';
|
||||||
|
|
||||||
|
interface ContextLinksSectionProps {
|
||||||
|
contextLinks: ContextLinksData;
|
||||||
|
setContextLinks: Dispatch<SetStateAction<ContextLinksData>>;
|
||||||
|
selectedWidget?: Widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContextLinksSection({
|
||||||
|
contextLinks,
|
||||||
|
setContextLinks,
|
||||||
|
selectedWidget,
|
||||||
|
}: ContextLinksSectionProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
title="Context Links"
|
||||||
|
icon={<LinkIcon size={14} />}
|
||||||
|
defaultOpen={!!contextLinks.linksData.length}
|
||||||
|
>
|
||||||
|
<div className="context-links-section">
|
||||||
|
<ContextLinks
|
||||||
|
contextLinks={contextLinks}
|
||||||
|
setContextLinks={setContextLinks}
|
||||||
|
selectedWidget={selectedWidget}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { Select, Typography } from 'antd';
|
||||||
|
import { PrecisionOption } from 'components/Graph/types';
|
||||||
|
import { PanelDisplay } from 'constants/queryBuilder';
|
||||||
|
import { SlidersHorizontal } from 'lucide-react';
|
||||||
|
import { ColumnUnit } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import { ColumnUnitSelector } from '../../ColumnUnitSelector/ColumnUnitSelector';
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
import DashboardYAxisUnitSelectorWrapper from '../../DashboardYAxisUnitSelectorWrapper';
|
||||||
|
|
||||||
|
interface FormattingUnitsSectionProps {
|
||||||
|
selectedPanelDisplay: PanelDisplay | '';
|
||||||
|
yAxisUnit: string;
|
||||||
|
setYAxisUnit: Dispatch<SetStateAction<string>>;
|
||||||
|
isNewDashboard: boolean;
|
||||||
|
decimalPrecision: PrecisionOption;
|
||||||
|
setDecimalPrecision: Dispatch<SetStateAction<PrecisionOption>>;
|
||||||
|
columnUnits: ColumnUnit;
|
||||||
|
setColumnUnits: Dispatch<SetStateAction<ColumnUnit>>;
|
||||||
|
allowYAxisUnit: boolean;
|
||||||
|
allowDecimalPrecision: boolean;
|
||||||
|
allowPanelColumnPreference: boolean;
|
||||||
|
decimapPrecisionOptions: { label: string; value: PrecisionOption }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormattingUnitsSection({
|
||||||
|
selectedPanelDisplay,
|
||||||
|
yAxisUnit,
|
||||||
|
setYAxisUnit,
|
||||||
|
isNewDashboard,
|
||||||
|
decimalPrecision,
|
||||||
|
setDecimalPrecision,
|
||||||
|
columnUnits,
|
||||||
|
setColumnUnits,
|
||||||
|
allowYAxisUnit,
|
||||||
|
allowDecimalPrecision,
|
||||||
|
allowPanelColumnPreference,
|
||||||
|
decimapPrecisionOptions,
|
||||||
|
}: FormattingUnitsSectionProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
title="Formatting & Units"
|
||||||
|
icon={<SlidersHorizontal size={14} />}
|
||||||
|
>
|
||||||
|
{allowYAxisUnit && (
|
||||||
|
<DashboardYAxisUnitSelectorWrapper
|
||||||
|
onSelect={setYAxisUnit}
|
||||||
|
value={yAxisUnit || ''}
|
||||||
|
fieldLabel={
|
||||||
|
selectedPanelDisplay === PanelDisplay.VALUE ||
|
||||||
|
selectedPanelDisplay === PanelDisplay.PIE
|
||||||
|
? 'Unit'
|
||||||
|
: 'Y Axis Unit'
|
||||||
|
}
|
||||||
|
shouldUpdateYAxisUnit={isNewDashboard}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowDecimalPrecision && (
|
||||||
|
<section className="decimal-precision-selector control-container">
|
||||||
|
<Typography.Text className="section-heading">
|
||||||
|
Decimal Precision
|
||||||
|
</Typography.Text>
|
||||||
|
<Select
|
||||||
|
options={decimapPrecisionOptions}
|
||||||
|
value={decimalPrecision}
|
||||||
|
className="panel-type-select"
|
||||||
|
defaultValue={decimapPrecisionOptions[0]?.value}
|
||||||
|
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowPanelColumnPreference && (
|
||||||
|
<ColumnUnitSelector
|
||||||
|
columnUnits={columnUnits}
|
||||||
|
setColumnUnits={setColumnUnits}
|
||||||
|
isNewDashboard={isNewDashboard}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.general-settings__name-description {
|
||||||
|
padding: 0 0 4px 0;
|
||||||
|
|
||||||
|
.general-settings__name-input {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 6px 6px 8px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings__description-input {
|
||||||
|
border-style: unset;
|
||||||
|
.ant-input {
|
||||||
|
display: flex;
|
||||||
|
height: 80px;
|
||||||
|
padding: 6px 6px 6px 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.general-settings__name-description {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.general-settings__name-input {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings__description-input {
|
||||||
|
.ant-input {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import type { InputRef } from 'antd';
|
||||||
|
import { AutoComplete, Input, Typography } from 'antd';
|
||||||
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
|
||||||
|
import './GeneralSettingsSection.styles.scss';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
interface VariableOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneralSettingsSectionProps {
|
||||||
|
title: string;
|
||||||
|
setTitle: Dispatch<SetStateAction<string>>;
|
||||||
|
description: string;
|
||||||
|
setDescription: Dispatch<SetStateAction<string>>;
|
||||||
|
dashboardVariables: Record<string, { name?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GeneralSettingsSection({
|
||||||
|
title,
|
||||||
|
setTitle,
|
||||||
|
description,
|
||||||
|
setDescription,
|
||||||
|
dashboardVariables,
|
||||||
|
}: GeneralSettingsSectionProps): JSX.Element {
|
||||||
|
const [inputValue, setInputValue] = useState(title);
|
||||||
|
const [autoCompleteOpen, setAutoCompleteOpen] = useState(false);
|
||||||
|
const [cursorPos, setCursorPos] = useState(0);
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
|
||||||
|
const onChangeHandler = (
|
||||||
|
setFunc: Dispatch<SetStateAction<string>>,
|
||||||
|
value: string,
|
||||||
|
): void => {
|
||||||
|
setFunc(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
|
||||||
|
return Object.entries(dashboardVariables).map(([, value]) => ({
|
||||||
|
value: value.name || '',
|
||||||
|
label: value.name || '',
|
||||||
|
}));
|
||||||
|
}, [dashboardVariables]);
|
||||||
|
|
||||||
|
const updateCursorAndDropdown = useCallback(
|
||||||
|
(value: string, pos: number): void => {
|
||||||
|
setCursorPos(pos);
|
||||||
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
|
setAutoCompleteOpen(lastDollar !== -1 && pos >= lastDollar + 1);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
(value: string): void => {
|
||||||
|
setInputValue(value);
|
||||||
|
onChangeHandler(setTitle, value);
|
||||||
|
setTimeout(() => {
|
||||||
|
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
||||||
|
updateCursorAndDropdown(value, pos);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
[setTitle, updateCursorAndDropdown],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelect = useCallback(
|
||||||
|
(selectedValue: string): void => {
|
||||||
|
const pos = cursorPos;
|
||||||
|
const value = inputValue;
|
||||||
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
|
const textBeforeDollar = value.substring(0, lastDollar);
|
||||||
|
const textAfterDollar = value.substring(lastDollar + 1);
|
||||||
|
const match = textAfterDollar.match(/^([a-zA-Z0-9_.]*)/);
|
||||||
|
const rest = textAfterDollar.substring(match ? match[1].length : 0);
|
||||||
|
const newValue = `${textBeforeDollar}$${selectedValue}${rest}`;
|
||||||
|
setInputValue(newValue);
|
||||||
|
onChangeHandler(setTitle, newValue);
|
||||||
|
setAutoCompleteOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
const newCursor = `${textBeforeDollar}$${selectedValue}`.length;
|
||||||
|
inputRef.current?.input?.setSelectionRange(newCursor, newCursor);
|
||||||
|
setCursorPos(newCursor);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
[cursorPos, inputValue, setTitle],
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterOption = useCallback(
|
||||||
|
(currentInputValue: string, option?: VariableOption): boolean => {
|
||||||
|
const pos = cursorPos;
|
||||||
|
const value = currentInputValue;
|
||||||
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
|
if (lastDollar === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const afterDollar = value.substring(lastDollar + 1, pos).toLowerCase();
|
||||||
|
return option?.value.toLowerCase().startsWith(afterDollar) || false;
|
||||||
|
},
|
||||||
|
[cursorPos],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputCursor = useCallback((): void => {
|
||||||
|
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
||||||
|
updateCursorAndDropdown(inputValue, pos);
|
||||||
|
}, [inputValue, updateCursorAndDropdown]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection title="General" defaultOpen icon={null}>
|
||||||
|
<section className="general-settings__name-description control-container">
|
||||||
|
<Typography.Text className="section-heading">Name</Typography.Text>
|
||||||
|
<AutoComplete
|
||||||
|
options={dashboardVariableOptions}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={onInputChange}
|
||||||
|
onSelect={onSelect}
|
||||||
|
filterOption={filterOption}
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
placeholder="Enter the panel name here..."
|
||||||
|
open={autoCompleteOpen}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
rootClassName="general-settings__name-input"
|
||||||
|
ref={inputRef}
|
||||||
|
onSelect={handleInputCursor}
|
||||||
|
onClick={handleInputCursor}
|
||||||
|
onBlur={(): void => setAutoCompleteOpen(false)}
|
||||||
|
/>
|
||||||
|
</AutoComplete>
|
||||||
|
<Typography.Text className="section-heading">Description</Typography.Text>
|
||||||
|
<TextArea
|
||||||
|
placeholder="Enter the panel description here..."
|
||||||
|
bordered
|
||||||
|
allowClear
|
||||||
|
value={description}
|
||||||
|
onChange={(event): void =>
|
||||||
|
onChangeHandler(setDescription, event.target.value)
|
||||||
|
}
|
||||||
|
rootClassName="general-settings__description-input"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.histogram-settings__bucket-config {
|
||||||
|
.histogram-settings__bucket-size-label {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-settings__bucket-input {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
padding: 6px 6px 6px 8px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-settings__combine-hist {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.histogram-settings__merge-label {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 138.462% */
|
||||||
|
letter-spacing: 0.52px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.histogram-settings__bucket-config {
|
||||||
|
.histogram-settings__merge-label {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-settings__bucket-input {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { InputNumber, Switch, Typography } from 'antd';
|
||||||
|
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
|
||||||
|
import './HistogramBucketsSection.styles.scss';
|
||||||
|
|
||||||
|
interface HistogramBucketsSectionProps {
|
||||||
|
bucketCount: number;
|
||||||
|
setBucketCount: Dispatch<SetStateAction<number>>;
|
||||||
|
bucketWidth: number;
|
||||||
|
setBucketWidth: Dispatch<SetStateAction<number>>;
|
||||||
|
combineHistogram: boolean;
|
||||||
|
setCombineHistogram: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HistogramBucketsSection({
|
||||||
|
bucketCount,
|
||||||
|
setBucketCount,
|
||||||
|
bucketWidth,
|
||||||
|
setBucketWidth,
|
||||||
|
combineHistogram,
|
||||||
|
setCombineHistogram,
|
||||||
|
}: HistogramBucketsSectionProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<SettingsSection title="Histogram / Buckets">
|
||||||
|
<section className="histogram-settings__bucket-config control-container">
|
||||||
|
<Typography.Text className="section-heading">
|
||||||
|
Number of buckets
|
||||||
|
</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
|
value={bucketCount || null}
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
rootClassName="bucket-input"
|
||||||
|
placeholder="Default: 30"
|
||||||
|
onChange={(val): void => {
|
||||||
|
setBucketCount(val || 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography.Text className="section-heading histogram-settings__bucket-size-label">
|
||||||
|
Bucket width
|
||||||
|
</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
|
value={bucketWidth || null}
|
||||||
|
type="number"
|
||||||
|
precision={2}
|
||||||
|
placeholder="Default: Auto"
|
||||||
|
step={0.1}
|
||||||
|
min={0.0}
|
||||||
|
rootClassName="histogram-settings__bucket-input"
|
||||||
|
onChange={(val): void => {
|
||||||
|
setBucketWidth(val || 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<section className="histogram-settings__combine-hist">
|
||||||
|
<Typography.Text className="section-heading">
|
||||||
|
<span className="histogram-settings__merge-label">
|
||||||
|
Merge all series into one
|
||||||
|
</span>
|
||||||
|
</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
checked={combineHistogram}
|
||||||
|
size="small"
|
||||||
|
onChange={(checked): void => setCombineHistogram(checked)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import type { UseQueryResult } from 'react-query';
|
||||||
|
import { Select, Typography } from 'antd';
|
||||||
|
import { Layers } from 'lucide-react';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { LegendPosition } from 'types/api/dashboard/getAll';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
import LegendColors from '../../LegendColors/LegendColors';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface LegendSectionProps {
|
||||||
|
allowLegendPosition: boolean;
|
||||||
|
allowLegendColors: boolean;
|
||||||
|
legendPosition: LegendPosition;
|
||||||
|
setLegendPosition: Dispatch<SetStateAction<LegendPosition>>;
|
||||||
|
customLegendColors: Record<string, string>;
|
||||||
|
setCustomLegendColors: Dispatch<SetStateAction<Record<string, string>>>;
|
||||||
|
queryResponse?: UseQueryResult<
|
||||||
|
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LegendSection({
|
||||||
|
allowLegendPosition,
|
||||||
|
allowLegendColors,
|
||||||
|
legendPosition,
|
||||||
|
setLegendPosition,
|
||||||
|
customLegendColors,
|
||||||
|
setCustomLegendColors,
|
||||||
|
queryResponse,
|
||||||
|
}: LegendSectionProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<SettingsSection title="Legend" icon={<Layers size={14} />}>
|
||||||
|
{allowLegendPosition && (
|
||||||
|
<section className="legend-position control-container">
|
||||||
|
<Typography.Text className="section-heading">Position</Typography.Text>
|
||||||
|
<Select
|
||||||
|
onChange={(value: LegendPosition): void => setLegendPosition(value)}
|
||||||
|
value={legendPosition}
|
||||||
|
className="panel-type-select"
|
||||||
|
defaultValue={LegendPosition.BOTTOM}
|
||||||
|
>
|
||||||
|
<Option value={LegendPosition.BOTTOM}>
|
||||||
|
<div className="select-option">
|
||||||
|
<Typography.Text className="display">Bottom</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
<Option value={LegendPosition.RIGHT}>
|
||||||
|
<div className="select-option">
|
||||||
|
<Typography.Text className="display">Right</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowLegendColors && (
|
||||||
|
<section className="legend-colors">
|
||||||
|
<LegendColors
|
||||||
|
customLegendColors={customLegendColors}
|
||||||
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.thresholds-section {
|
||||||
|
padding: 12px 12px 16px 12px;
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.thresholds-section {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { Antenna } from 'lucide-react';
|
||||||
|
import { ColumnUnit } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
import ThresholdSelector from '../../Threshold/ThresholdSelector';
|
||||||
|
import { ThresholdProps } from '../../Threshold/types';
|
||||||
|
|
||||||
|
import './ThresholdsSection.styles.scss';
|
||||||
|
|
||||||
|
interface ThresholdsSectionProps {
|
||||||
|
thresholds: ThresholdProps[];
|
||||||
|
setThresholds: Dispatch<SetStateAction<ThresholdProps[]>>;
|
||||||
|
yAxisUnit: string;
|
||||||
|
selectedGraph: PANEL_TYPES;
|
||||||
|
columnUnits: ColumnUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThresholdsSection({
|
||||||
|
thresholds,
|
||||||
|
setThresholds,
|
||||||
|
yAxisUnit,
|
||||||
|
selectedGraph,
|
||||||
|
columnUnits,
|
||||||
|
}: ThresholdsSectionProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
title="Thresholds"
|
||||||
|
icon={<Antenna size={14} />}
|
||||||
|
defaultOpen={!!thresholds.length}
|
||||||
|
>
|
||||||
|
<ThresholdSelector
|
||||||
|
thresholds={thresholds}
|
||||||
|
setThresholds={setThresholds}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
selectedGraph={selectedGraph}
|
||||||
|
columnUnits={columnUnits}
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
|
import { Select, Switch, Typography } from 'antd';
|
||||||
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import {
|
||||||
|
ItemsProps,
|
||||||
|
PanelTypesWithData,
|
||||||
|
} from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { LayoutDashboard } from 'lucide-react';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import SettingsSection from '../../components/SettingsSection/SettingsSection';
|
||||||
|
import { timePreferance } from '../../timeItems';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface VisualizationSettingsSectionProps {
|
||||||
|
selectedGraph: PANEL_TYPES;
|
||||||
|
setGraphHandler: (type: PANEL_TYPES) => void;
|
||||||
|
selectedTime: timePreferance;
|
||||||
|
setSelectedTime: Dispatch<SetStateAction<timePreferance>>;
|
||||||
|
stackedBarChart: boolean;
|
||||||
|
setStackedBarChart: Dispatch<SetStateAction<boolean>>;
|
||||||
|
isFillSpans: boolean;
|
||||||
|
setIsFillSpans: Dispatch<SetStateAction<boolean>>;
|
||||||
|
allowPanelTimePreference: boolean;
|
||||||
|
allowStackingBarChart: boolean;
|
||||||
|
allowFillSpans: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VisualizationSettingsSection({
|
||||||
|
selectedGraph,
|
||||||
|
setGraphHandler,
|
||||||
|
selectedTime,
|
||||||
|
setSelectedTime,
|
||||||
|
stackedBarChart,
|
||||||
|
setStackedBarChart,
|
||||||
|
isFillSpans,
|
||||||
|
setIsFillSpans,
|
||||||
|
allowPanelTimePreference,
|
||||||
|
allowStackingBarChart,
|
||||||
|
allowFillSpans,
|
||||||
|
}: VisualizationSettingsSectionProps): JSX.Element {
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(PanelTypesWithData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const queryContainsMetricsDataSource = currentQuery.builder.queryData.some(
|
||||||
|
(query) => query.dataSource === DataSource.METRICS,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (queryContainsMetricsDataSource) {
|
||||||
|
setGraphTypes((prev) =>
|
||||||
|
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setGraphTypes(PanelTypesWithData);
|
||||||
|
}
|
||||||
|
}, [currentQuery]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
title="Visualization"
|
||||||
|
defaultOpen
|
||||||
|
icon={<LayoutDashboard size={14} />}
|
||||||
|
>
|
||||||
|
<section className="panel-type control-container">
|
||||||
|
<Typography.Text className="section-heading">Panel Type</Typography.Text>
|
||||||
|
<Select
|
||||||
|
onChange={setGraphHandler}
|
||||||
|
value={selectedGraph}
|
||||||
|
className="panel-type-select"
|
||||||
|
data-testid="panel-change-select"
|
||||||
|
data-stacking-state={stackedBarChart ? 'true' : 'false'}
|
||||||
|
>
|
||||||
|
{graphTypes.map((item) => (
|
||||||
|
<Option key={item.name} value={item.name}>
|
||||||
|
<div className="select-option">
|
||||||
|
<div className="icon">{item.icon}</div>
|
||||||
|
<Typography.Text className="display">{item.display}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{allowPanelTimePreference && (
|
||||||
|
<section className="panel-time-preference control-container">
|
||||||
|
<Typography.Text className="section-heading">
|
||||||
|
Panel Time Preference
|
||||||
|
</Typography.Text>
|
||||||
|
<TimePreference
|
||||||
|
{...{
|
||||||
|
selectedTime,
|
||||||
|
setSelectedTime,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowStackingBarChart && (
|
||||||
|
<section className="stack-chart control-container">
|
||||||
|
<Typography.Text className="section-heading">Stack series</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
checked={stackedBarChart}
|
||||||
|
size="small"
|
||||||
|
onChange={(checked): void => setStackedBarChart(checked)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowFillSpans && (
|
||||||
|
<section className="fill-gaps toggle-card">
|
||||||
|
<div className="toggle-card-text-container">
|
||||||
|
<Typography className="section-heading">Fill gaps</Typography>
|
||||||
|
<Typography.Text className="toggle-card-description">
|
||||||
|
Fill gaps in data with 0 for continuity
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isFillSpans}
|
||||||
|
size="small"
|
||||||
|
onChange={(checked): void => setIsFillSpans(checked)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
.threshold-selector-container {
|
.threshold-selector-container {
|
||||||
padding: 12px;
|
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
|
|
||||||
.threshold-select {
|
.threshold-select {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { Typography } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
||||||
import { Antenna, Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import Threshold from './Threshold';
|
import Threshold from './Threshold';
|
||||||
@@ -68,11 +68,14 @@ function ThresholdSelector({
|
|||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<div className="threshold-selector-container">
|
<div className="threshold-selector-container">
|
||||||
<div className="threshold-select" onClick={addThresholdHandler}>
|
<div className="threshold-select" onClick={addThresholdHandler}>
|
||||||
<div className="left-section">
|
<Button
|
||||||
<Antenna size={14} className="icon" />
|
type="default"
|
||||||
<Typography.Text className="text">Thresholds</Typography.Text>
|
icon={<Plus size={14} />}
|
||||||
</div>
|
style={{ width: '100%' }}
|
||||||
<Plus size={14} onClick={addThresholdHandler} className="icon" />
|
onClick={addThresholdHandler}
|
||||||
|
>
|
||||||
|
Add Threshold
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{thresholds.map((threshold, idx) => (
|
{thresholds.map((threshold, idx) => (
|
||||||
<Threshold
|
<Threshold
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import { MemoryRouter } from 'react-router-dom';
|
|||||||
import { render as rtlRender, screen } from '@testing-library/react';
|
import { render as rtlRender, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import {
|
||||||
|
FillMode,
|
||||||
|
LineInterpolation,
|
||||||
|
LineStyle,
|
||||||
|
} from 'lib/uPlotV2/config/types';
|
||||||
import { AppContext } from 'providers/App/App';
|
import { AppContext } from 'providers/App/App';
|
||||||
import { IAppContext } from 'providers/App/types';
|
import { IAppContext } from 'providers/App/types';
|
||||||
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||||
@@ -165,6 +170,14 @@ describe('RightContainer - Alerts Section', () => {
|
|||||||
setContextLinks: jest.fn(),
|
setContextLinks: jest.fn(),
|
||||||
enableDrillDown: false,
|
enableDrillDown: false,
|
||||||
isNewDashboard: false,
|
isNewDashboard: false,
|
||||||
|
lineInterpolation: LineInterpolation.Spline,
|
||||||
|
fillMode: FillMode.None,
|
||||||
|
lineStyle: LineStyle.Solid,
|
||||||
|
setLineInterpolation: jest.fn(),
|
||||||
|
setFillMode: jest.fn(),
|
||||||
|
setLineStyle: jest.fn(),
|
||||||
|
showPoints: false,
|
||||||
|
setShowPoints: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -176,7 +189,7 @@ describe('RightContainer - Alerts Section', () => {
|
|||||||
|
|
||||||
const alertsSection = screen.getByText('Alerts').closest('section');
|
const alertsSection = screen.getByText('Alerts').closest('section');
|
||||||
expect(alertsSection).toBeInTheDocument();
|
expect(alertsSection).toBeInTheDocument();
|
||||||
expect(alertsSection).toHaveClass('alerts');
|
expect(alertsSection).toHaveClass('alerts-section');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders alerts section with correct text and SquareArrowOutUpRight icon', () => {
|
it('renders alerts section with correct text and SquareArrowOutUpRight icon', () => {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.fill-mode-selector {
|
||||||
|
.fill-mode-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-mode-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.fill-mode-selector {
|
||||||
|
.fill-mode-label {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { FillMode } from 'lib/uPlotV2/config/types';
|
||||||
|
|
||||||
|
import './FillModeSelector.styles.scss';
|
||||||
|
|
||||||
|
interface FillModeSelectorProps {
|
||||||
|
value: FillMode;
|
||||||
|
onChange: (value: FillMode) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FillModeSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: FillModeSelectorProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="fill-mode-selector control-container">
|
||||||
|
<Typography.Text className="section-heading">Fill mode</Typography.Text>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={value}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
onValueChange={(newValue): void => {
|
||||||
|
if (newValue) {
|
||||||
|
onChange(newValue as FillMode);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value={FillMode.None} aria-label="None" title="None">
|
||||||
|
<svg
|
||||||
|
className="fill-mode-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="8" y="16" width="32" height="16" stroke="#888" fill="none" />
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">None</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value={FillMode.Solid} aria-label="Solid" title="Solid">
|
||||||
|
<svg
|
||||||
|
className="fill-mode-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="8" y="16" width="32" height="16" fill="#888" />
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">Solid</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={FillMode.Gradient}
|
||||||
|
aria-label="Gradient"
|
||||||
|
title="Gradient"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="fill-mode-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="fill-gradient" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stopColor="#888" stopOpacity="0.2" />
|
||||||
|
<stop offset="100%" stopColor="#888" stopOpacity="0.8" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
x="8"
|
||||||
|
y="16"
|
||||||
|
width="32"
|
||||||
|
height="16"
|
||||||
|
fill="url(#fill-gradient)"
|
||||||
|
stroke="#888"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">
|
||||||
|
Gradient
|
||||||
|
</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.line-interpolation-selector {
|
||||||
|
.line-interpolation-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-interpolation-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.line-interpolation-selector {
|
||||||
|
.line-interpolation-label {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { LineInterpolation } from 'lib/uPlotV2/config/types';
|
||||||
|
|
||||||
|
import './LineInterpolationSelector.styles.scss';
|
||||||
|
|
||||||
|
interface LineInterpolationSelectorProps {
|
||||||
|
value: LineInterpolation;
|
||||||
|
onChange: (value: LineInterpolation) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LineInterpolationSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: LineInterpolationSelectorProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="line-interpolation-selector control-container">
|
||||||
|
<Typography.Text className="section-heading">
|
||||||
|
Line interpolation
|
||||||
|
</Typography.Text>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={value}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
onValueChange={(newValue): void => {
|
||||||
|
if (newValue) {
|
||||||
|
onChange(newValue as LineInterpolation);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={LineInterpolation.Linear}
|
||||||
|
aria-label="Linear"
|
||||||
|
title="Linear"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="line-interpolation-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="32" r="3" fill="#888" />
|
||||||
|
<circle cx="24" cy="16" r="3" fill="#888" />
|
||||||
|
<circle cx="40" cy="32" r="3" fill="#888" />
|
||||||
|
<path d="M8 32 L24 16 L40 32" stroke="#888" />
|
||||||
|
</svg>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value={LineInterpolation.Spline} aria-label="Spline">
|
||||||
|
<svg
|
||||||
|
className="line-interpolation-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="32" r="3" fill="#888" />
|
||||||
|
<circle cx="24" cy="16" r="3" fill="#888" />
|
||||||
|
<circle cx="40" cy="32" r="3" fill="#888" />
|
||||||
|
<path d="M8 32 C16 8, 32 8, 40 32" />
|
||||||
|
</svg>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={LineInterpolation.StepAfter}
|
||||||
|
aria-label="Step After"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="line-interpolation-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="32" r="3" fill="#888" />
|
||||||
|
<circle cx="24" cy="16" r="3" fill="#888" />
|
||||||
|
<circle cx="40" cy="32" r="3" fill="#888" />
|
||||||
|
<path d="M8 32 V16 H24 V32 H40" />
|
||||||
|
</svg>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={LineInterpolation.StepBefore}
|
||||||
|
aria-label="Step Before"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="line-interpolation-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="32" r="3" fill="#888" />
|
||||||
|
<circle cx="24" cy="16" r="3" fill="#888" />
|
||||||
|
<circle cx="40" cy="32" r="3" fill="#888" />
|
||||||
|
<path d="M8 32 H24 V16 H40 V32" />
|
||||||
|
</svg>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.line-style-selector {
|
||||||
|
.line-style-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-style-label {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.line-style-selector {
|
||||||
|
.line-style-label {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { LineStyle } from 'lib/uPlotV2/config/types';
|
||||||
|
|
||||||
|
import './LineStyleSelector.styles.scss';
|
||||||
|
|
||||||
|
interface LineStyleSelectorProps {
|
||||||
|
value: LineStyle;
|
||||||
|
onChange: (value: LineStyle) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LineStyleSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: LineStyleSelectorProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="line-style-selector control-container">
|
||||||
|
<Typography.Text className="section-heading">Line style</Typography.Text>
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={value}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
onValueChange={(newValue): void => {
|
||||||
|
if (newValue) {
|
||||||
|
onChange(newValue as LineStyle);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value={LineStyle.Solid} aria-label="Solid" title="Solid">
|
||||||
|
<svg
|
||||||
|
className="line-style-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M8 24 L40 24" />
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">Solid</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem
|
||||||
|
value={LineStyle.Dashed}
|
||||||
|
aria-label="Dashed"
|
||||||
|
title="Dashed"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="line-style-icon"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="#888"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeDasharray="6 4"
|
||||||
|
>
|
||||||
|
<path d="M8 24 L40 24" />
|
||||||
|
</svg>
|
||||||
|
<Typography.Text className="section-heading-small">Dashed</Typography.Text>
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.settings-section {
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 12px;
|
||||||
|
min-height: 44px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.settings-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-icon {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-content {
|
||||||
|
padding: 0 12px 0 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: max-height 0.1s ease, opacity 0.1s ease, padding 0.1s ease;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
padding-bottom: 24px;
|
||||||
|
max-height: 1000px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.settings-section-header {
|
||||||
|
.chevron-icon {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
.settings-section-title {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ReactNode, useState } from 'react';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
import './SettingsSection.styles.scss';
|
||||||
|
|
||||||
|
export interface SettingsSectionProps {
|
||||||
|
title: string;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSection({
|
||||||
|
title,
|
||||||
|
defaultOpen = false,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
}: SettingsSectionProps): JSX.Element {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
|
||||||
|
const toggleOpen = (): void => {
|
||||||
|
setIsOpen((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="settings-section">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="settings-section-header"
|
||||||
|
onClick={toggleOpen}
|
||||||
|
>
|
||||||
|
<span className="settings-section-title">
|
||||||
|
{icon ? icon : null} {title}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={isOpen ? 'chevron-icon open' : 'chevron-icon'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isOpen ? 'settings-section-content open' : 'settings-section-content'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsSection;
|
||||||
@@ -206,3 +206,59 @@ export const panelTypeVsDecimalPrecision: {
|
|||||||
[PANEL_TYPES.TRACE]: false,
|
[PANEL_TYPES.TRACE]: false,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const panelTypeVsLineInterpolation: {
|
||||||
|
[key in PANEL_TYPES]: boolean;
|
||||||
|
} = {
|
||||||
|
[PANEL_TYPES.TIME_SERIES]: true,
|
||||||
|
[PANEL_TYPES.VALUE]: false,
|
||||||
|
[PANEL_TYPES.TABLE]: false,
|
||||||
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
|
[PANEL_TYPES.BAR]: false,
|
||||||
|
[PANEL_TYPES.HISTOGRAM]: false,
|
||||||
|
[PANEL_TYPES.TRACE]: false,
|
||||||
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const panelTypeVsLineStyle: {
|
||||||
|
[key in PANEL_TYPES]: boolean;
|
||||||
|
} = {
|
||||||
|
[PANEL_TYPES.TIME_SERIES]: true,
|
||||||
|
[PANEL_TYPES.VALUE]: false,
|
||||||
|
[PANEL_TYPES.TABLE]: false,
|
||||||
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
|
[PANEL_TYPES.BAR]: false,
|
||||||
|
[PANEL_TYPES.HISTOGRAM]: false,
|
||||||
|
[PANEL_TYPES.TRACE]: false,
|
||||||
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const panelTypeVsFillMode: {
|
||||||
|
[key in PANEL_TYPES]: boolean;
|
||||||
|
} = {
|
||||||
|
[PANEL_TYPES.TIME_SERIES]: true,
|
||||||
|
[PANEL_TYPES.VALUE]: false,
|
||||||
|
[PANEL_TYPES.TABLE]: false,
|
||||||
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
|
[PANEL_TYPES.BAR]: false,
|
||||||
|
[PANEL_TYPES.HISTOGRAM]: false,
|
||||||
|
[PANEL_TYPES.TRACE]: false,
|
||||||
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const panelTypeVsShowPoints: {
|
||||||
|
[key in PANEL_TYPES]: boolean;
|
||||||
|
} = {
|
||||||
|
[PANEL_TYPES.TIME_SERIES]: true,
|
||||||
|
[PANEL_TYPES.VALUE]: false,
|
||||||
|
[PANEL_TYPES.TABLE]: false,
|
||||||
|
[PANEL_TYPES.LIST]: false,
|
||||||
|
[PANEL_TYPES.PIE]: false,
|
||||||
|
[PANEL_TYPES.BAR]: false,
|
||||||
|
[PANEL_TYPES.HISTOGRAM]: false,
|
||||||
|
[PANEL_TYPES.TRACE]: false,
|
||||||
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -1,39 +1,16 @@
|
|||||||
import {
|
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import type { InputRef } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import {
|
|
||||||
AutoComplete,
|
|
||||||
Input,
|
|
||||||
InputNumber,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Switch,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
|
||||||
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
|
||||||
import GraphTypes, {
|
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
|
||||||
ItemsProps,
|
|
||||||
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
|
|
||||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|
||||||
import {
|
import {
|
||||||
ConciergeBell,
|
FillMode,
|
||||||
LineChart,
|
LineInterpolation,
|
||||||
Plus,
|
LineStyle,
|
||||||
Spline,
|
} from 'lib/uPlotV2/config/types';
|
||||||
SquareArrowOutUpRight,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
ColumnUnit,
|
ColumnUnit,
|
||||||
@@ -42,55 +19,55 @@ import {
|
|||||||
Widgets,
|
Widgets,
|
||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
|
||||||
|
|
||||||
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
|
||||||
import {
|
import {
|
||||||
panelTypeVsBucketConfig,
|
panelTypeVsBucketConfig,
|
||||||
panelTypeVsColumnUnitPreferences,
|
panelTypeVsColumnUnitPreferences,
|
||||||
panelTypeVsContextLinks,
|
panelTypeVsContextLinks,
|
||||||
panelTypeVsCreateAlert,
|
panelTypeVsCreateAlert,
|
||||||
panelTypeVsDecimalPrecision,
|
panelTypeVsDecimalPrecision,
|
||||||
|
panelTypeVsFillMode,
|
||||||
panelTypeVsFillSpan,
|
panelTypeVsFillSpan,
|
||||||
panelTypeVsLegendColors,
|
panelTypeVsLegendColors,
|
||||||
panelTypeVsLegendPosition,
|
panelTypeVsLegendPosition,
|
||||||
|
panelTypeVsLineInterpolation,
|
||||||
|
panelTypeVsLineStyle,
|
||||||
panelTypeVsLogScale,
|
panelTypeVsLogScale,
|
||||||
panelTypeVsPanelTimePreferences,
|
panelTypeVsPanelTimePreferences,
|
||||||
|
panelTypeVsShowPoints,
|
||||||
panelTypeVsSoftMinMax,
|
panelTypeVsSoftMinMax,
|
||||||
panelTypeVsStackingChartPreferences,
|
panelTypeVsStackingChartPreferences,
|
||||||
panelTypeVsThreshold,
|
panelTypeVsThreshold,
|
||||||
panelTypeVsYAxisUnit,
|
panelTypeVsYAxisUnit,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import ContextLinks from './ContextLinks';
|
import AlertsSection from './SettingSections/AlertsSection/AlertsSection';
|
||||||
import DashboardYAxisUnitSelectorWrapper from './DashboardYAxisUnitSelectorWrapper';
|
import AxesSection from './SettingSections/AxesSection/AxesSection';
|
||||||
import LegendColors from './LegendColors/LegendColors';
|
import ChartAppearanceSection from './SettingSections/ChartAppearanceSection/ChartAppearanceSection';
|
||||||
import ThresholdSelector from './Threshold/ThresholdSelector';
|
import ContextLinksSection from './SettingSections/ContextLinksSection/ContextLinksSection';
|
||||||
|
import FormattingUnitsSection from './SettingSections/FormattingUnitsSection/FormattingUnitsSection';
|
||||||
|
import GeneralSettingsSection from './SettingSections/GeneralSettingsSection/GeneralSettingsSection';
|
||||||
|
import HistogramBucketsSection from './SettingSections/HistogramBucketsSection/HistogramBucketsSection';
|
||||||
|
import LegendSection from './SettingSections/LegendSection/LegendSection';
|
||||||
|
import ThresholdsSection from './SettingSections/ThresholdsSection/ThresholdsSection';
|
||||||
|
import VisualizationSettingsSection from './SettingSections/VisualizationSettingsSection/VisualizationSettingsSection';
|
||||||
import { ThresholdProps } from './Threshold/types';
|
import { ThresholdProps } from './Threshold/types';
|
||||||
import { timePreferance } from './timeItems';
|
import { timePreferance } from './timeItems';
|
||||||
|
|
||||||
import './RightContainer.styles.scss';
|
import './RightContainer.styles.scss';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
enum LogScale {
|
|
||||||
LINEAR = 'linear',
|
|
||||||
LOGARITHMIC = 'logarithmic',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VariableOption {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
function RightContainer({
|
function RightContainer({
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
setTitle,
|
setTitle,
|
||||||
title,
|
title,
|
||||||
selectedGraph,
|
selectedGraph,
|
||||||
|
lineInterpolation,
|
||||||
|
setLineInterpolation,
|
||||||
|
fillMode,
|
||||||
|
setFillMode,
|
||||||
|
lineStyle,
|
||||||
|
setLineStyle,
|
||||||
|
showPoints,
|
||||||
|
setShowPoints,
|
||||||
bucketCount,
|
bucketCount,
|
||||||
bucketWidth,
|
bucketWidth,
|
||||||
stackedBarChart,
|
stackedBarChart,
|
||||||
@@ -130,20 +107,10 @@ function RightContainer({
|
|||||||
isNewDashboard,
|
isNewDashboard,
|
||||||
}: RightContainerProps): JSX.Element {
|
}: RightContainerProps): JSX.Element {
|
||||||
const { dashboardVariables } = useDashboardVariables();
|
const { dashboardVariables } = useDashboardVariables();
|
||||||
const [inputValue, setInputValue] = useState(title);
|
|
||||||
const [autoCompleteOpen, setAutoCompleteOpen] = useState(false);
|
|
||||||
const [cursorPos, setCursorPos] = useState(0);
|
|
||||||
const inputRef = useRef<InputRef>(null);
|
|
||||||
|
|
||||||
const onChangeHandler = useCallback(
|
const selectedPanelDisplay = PanelTypesWithData.find(
|
||||||
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
|
(e) => e.name === selectedGraph,
|
||||||
setFunc(value);
|
)?.display as PanelDisplay;
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedGraphType =
|
|
||||||
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
|
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
|
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
|
||||||
|
|
||||||
@@ -167,399 +134,173 @@ function RightContainer({
|
|||||||
panelTypeVsContextLinks[selectedGraph] && enableDrillDown;
|
panelTypeVsContextLinks[selectedGraph] && enableDrillDown;
|
||||||
const allowDecimalPrecision = panelTypeVsDecimalPrecision[selectedGraph];
|
const allowDecimalPrecision = panelTypeVsDecimalPrecision[selectedGraph];
|
||||||
|
|
||||||
const { currentQuery } = useQueryBuilder();
|
const allowLineInterpolation = panelTypeVsLineInterpolation[selectedGraph];
|
||||||
|
const allowLineStyle = panelTypeVsLineStyle[selectedGraph];
|
||||||
|
const allowFillMode = panelTypeVsFillMode[selectedGraph];
|
||||||
|
const allowShowPoints = panelTypeVsShowPoints[selectedGraph];
|
||||||
|
|
||||||
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
|
const decimapPrecisionOptions = useMemo(
|
||||||
|
() => [
|
||||||
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
|
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
|
||||||
return Object.entries(dashboardVariables).map(([, value]) => ({
|
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
|
||||||
value: value.name || '',
|
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
|
||||||
label: value.name || '',
|
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
|
||||||
}));
|
],
|
||||||
}, [dashboardVariables]);
|
[],
|
||||||
|
|
||||||
const updateCursorAndDropdown = (value: string, pos: number): void => {
|
|
||||||
setCursorPos(pos);
|
|
||||||
const lastDollar = value.lastIndexOf('$', pos - 1);
|
|
||||||
setAutoCompleteOpen(lastDollar !== -1 && pos >= lastDollar + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInputChange = (value: string): void => {
|
|
||||||
setInputValue(value);
|
|
||||||
onChangeHandler(setTitle, value);
|
|
||||||
setTimeout(() => {
|
|
||||||
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
|
||||||
updateCursorAndDropdown(value, pos);
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputCursor = (): void => {
|
|
||||||
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
|
||||||
updateCursorAndDropdown(inputValue, pos);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSelect = (selectedValue: string): void => {
|
|
||||||
const pos = cursorPos;
|
|
||||||
const value = inputValue;
|
|
||||||
const lastDollar = value.lastIndexOf('$', pos - 1);
|
|
||||||
const textBeforeDollar = value.substring(0, lastDollar);
|
|
||||||
const textAfterDollar = value.substring(lastDollar + 1);
|
|
||||||
const match = textAfterDollar.match(/^([a-zA-Z0-9_.]*)/);
|
|
||||||
const rest = textAfterDollar.substring(match ? match[1].length : 0);
|
|
||||||
const newValue = `${textBeforeDollar}$${selectedValue}${rest}`;
|
|
||||||
setInputValue(newValue);
|
|
||||||
onChangeHandler(setTitle, newValue);
|
|
||||||
setAutoCompleteOpen(false);
|
|
||||||
setTimeout(() => {
|
|
||||||
const newCursor = `${textBeforeDollar}$${selectedValue}`.length;
|
|
||||||
inputRef.current?.input?.setSelectionRange(newCursor, newCursor);
|
|
||||||
setCursorPos(newCursor);
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterOption = (
|
|
||||||
inputValue: string,
|
|
||||||
option?: VariableOption,
|
|
||||||
): boolean => {
|
|
||||||
const pos = cursorPos;
|
|
||||||
const value = inputValue;
|
|
||||||
const lastDollar = value.lastIndexOf('$', pos - 1);
|
|
||||||
if (lastDollar === -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const afterDollar = value.substring(lastDollar + 1, pos).toLowerCase();
|
|
||||||
return option?.value.toLowerCase().startsWith(afterDollar) || false;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const queryContainsMetricsDataSource = currentQuery.builder.queryData.some(
|
|
||||||
(query) => query.dataSource === DataSource.METRICS,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (queryContainsMetricsDataSource) {
|
|
||||||
setGraphTypes((prev) =>
|
|
||||||
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setGraphTypes(GraphTypes);
|
|
||||||
}
|
|
||||||
}, [currentQuery]);
|
|
||||||
|
|
||||||
const softMinHandler = useCallback(
|
|
||||||
(value: number | null) => {
|
|
||||||
setSoftMin(value);
|
|
||||||
},
|
|
||||||
[setSoftMin],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const softMaxHandler = useCallback(
|
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
|
||||||
(value: number | null) => {
|
allowSoftMinMax,
|
||||||
setSoftMax(value);
|
allowLogScale,
|
||||||
},
|
]);
|
||||||
[setSoftMax],
|
|
||||||
|
const isFormattingSectionVisible = useMemo(
|
||||||
|
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
|
||||||
|
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLegendSectionVisible = useMemo(
|
||||||
|
() => allowLegendPosition || allowLegendColors,
|
||||||
|
[allowLegendPosition, allowLegendColors],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isChartAppearanceSectionVisible = useMemo(
|
||||||
|
() =>
|
||||||
|
/**
|
||||||
|
* Disabled for now as we are not done with other settings in chart appearance section
|
||||||
|
* TODO: @ahrefabhi Enable this after we are done other settings in chart appearance section
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/no-redundant-boolean
|
||||||
|
false &&
|
||||||
|
(allowFillMode ||
|
||||||
|
allowLineStyle ||
|
||||||
|
allowLineInterpolation ||
|
||||||
|
allowShowPoints),
|
||||||
|
[allowFillMode, allowLineStyle, allowLineInterpolation, allowShowPoints],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="right-container">
|
<div className="right-container">
|
||||||
<section className="header">
|
<section className="header">
|
||||||
<div className="purple-dot" />
|
<div className="purple-dot" />
|
||||||
<Typography.Text className="header-text">Panel details</Typography.Text>
|
<Typography.Text className="header-text">Panel Settings</Typography.Text>
|
||||||
</section>
|
|
||||||
<section className="name-description">
|
|
||||||
<Typography.Text className="typography">Name</Typography.Text>
|
|
||||||
<AutoComplete
|
|
||||||
options={dashboardVariableOptions}
|
|
||||||
value={inputValue}
|
|
||||||
onChange={onInputChange}
|
|
||||||
onSelect={onSelect}
|
|
||||||
filterOption={filterOption}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
getPopupContainer={popupContainer}
|
|
||||||
placeholder="Enter the panel name here..."
|
|
||||||
open={autoCompleteOpen}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
rootClassName="name-input"
|
|
||||||
ref={inputRef}
|
|
||||||
onSelect={handleInputCursor}
|
|
||||||
onClick={handleInputCursor}
|
|
||||||
onBlur={(): void => setAutoCompleteOpen(false)}
|
|
||||||
/>
|
|
||||||
</AutoComplete>
|
|
||||||
<Typography.Text className="typography">Description</Typography.Text>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Enter the panel description here..."
|
|
||||||
bordered
|
|
||||||
allowClear
|
|
||||||
value={description}
|
|
||||||
onChange={(event): void =>
|
|
||||||
onChangeHandler(setDescription, event.target.value)
|
|
||||||
}
|
|
||||||
rootClassName="description-input"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<GeneralSettingsSection
|
||||||
|
title={title}
|
||||||
|
setTitle={setTitle}
|
||||||
|
description={description}
|
||||||
|
setDescription={setDescription}
|
||||||
|
dashboardVariables={dashboardVariables}
|
||||||
|
/>
|
||||||
|
|
||||||
<section className="panel-config">
|
<section className="panel-config">
|
||||||
<Typography.Text className="typography">Panel Type</Typography.Text>
|
<VisualizationSettingsSection
|
||||||
<Select
|
selectedGraph={selectedGraph}
|
||||||
onChange={setGraphHandler}
|
setGraphHandler={setGraphHandler}
|
||||||
value={selectedGraph}
|
selectedTime={selectedTime}
|
||||||
style={{ width: '100%' }}
|
setSelectedTime={setSelectedTime}
|
||||||
className="panel-type-select"
|
stackedBarChart={stackedBarChart}
|
||||||
data-testid="panel-change-select"
|
setStackedBarChart={setStackedBarChart}
|
||||||
data-stacking-state={stackedBarChart ? 'true' : 'false'}
|
isFillSpans={isFillSpans}
|
||||||
>
|
setIsFillSpans={setIsFillSpans}
|
||||||
{graphTypes.map((item) => (
|
allowPanelTimePreference={allowPanelTimePreference}
|
||||||
<Option key={item.name} value={item.name}>
|
allowStackingBarChart={allowStackingBarChart}
|
||||||
<div className="select-option">
|
allowFillSpans={allowFillSpans}
|
||||||
<div className="icon">{item.icon}</div>
|
/>
|
||||||
<Typography.Text className="display">{item.display}</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{allowFillSpans && (
|
{isFormattingSectionVisible && (
|
||||||
<Space className="fill-gaps">
|
<FormattingUnitsSection
|
||||||
<Typography className="fill-gaps-text">Fill gaps</Typography>
|
selectedPanelDisplay={selectedPanelDisplay}
|
||||||
<Switch
|
yAxisUnit={yAxisUnit}
|
||||||
checked={isFillSpans}
|
setYAxisUnit={setYAxisUnit}
|
||||||
size="small"
|
isNewDashboard={isNewDashboard}
|
||||||
onChange={(checked): void => setIsFillSpans(checked)}
|
decimalPrecision={decimalPrecision}
|
||||||
/>
|
setDecimalPrecision={setDecimalPrecision}
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowPanelTimePreference && (
|
|
||||||
<>
|
|
||||||
<Typography.Text className="panel-time-text">
|
|
||||||
Panel Time Preference
|
|
||||||
</Typography.Text>
|
|
||||||
<TimePreference
|
|
||||||
{...{
|
|
||||||
selectedTime,
|
|
||||||
setSelectedTime,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowPanelColumnPreference && (
|
|
||||||
<ColumnUnitSelector
|
|
||||||
columnUnits={columnUnits}
|
columnUnits={columnUnits}
|
||||||
setColumnUnits={setColumnUnits}
|
setColumnUnits={setColumnUnits}
|
||||||
isNewDashboard={isNewDashboard}
|
allowYAxisUnit={allowYAxisUnit}
|
||||||
|
allowDecimalPrecision={allowDecimalPrecision}
|
||||||
|
allowPanelColumnPreference={allowPanelColumnPreference}
|
||||||
|
decimapPrecisionOptions={decimapPrecisionOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowYAxisUnit && (
|
{isChartAppearanceSectionVisible && (
|
||||||
<DashboardYAxisUnitSelectorWrapper
|
<ChartAppearanceSection
|
||||||
onSelect={setYAxisUnit}
|
fillMode={fillMode}
|
||||||
value={yAxisUnit || ''}
|
setFillMode={setFillMode}
|
||||||
fieldLabel={
|
lineStyle={lineStyle}
|
||||||
selectedGraphType === PanelDisplay.VALUE ||
|
setLineStyle={setLineStyle}
|
||||||
selectedGraphType === PanelDisplay.PIE
|
lineInterpolation={lineInterpolation}
|
||||||
? 'Unit'
|
setLineInterpolation={setLineInterpolation}
|
||||||
: 'Y Axis Unit'
|
showPoints={showPoints}
|
||||||
}
|
setShowPoints={setShowPoints}
|
||||||
// Only update the y-axis unit value automatically in create mode
|
allowFillMode={allowFillMode}
|
||||||
shouldUpdateYAxisUnit={isNewDashboard}
|
allowLineStyle={allowLineStyle}
|
||||||
|
allowLineInterpolation={allowLineInterpolation}
|
||||||
|
allowShowPoints={allowShowPoints}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowDecimalPrecision && (
|
{isAxisSectionVisible && (
|
||||||
<section className="decimal-precision-selector">
|
<AxesSection
|
||||||
<Typography.Text className="typography">
|
allowSoftMinMax={allowSoftMinMax}
|
||||||
Decimal Precision
|
allowLogScale={allowLogScale}
|
||||||
</Typography.Text>
|
softMin={softMin}
|
||||||
<Select
|
softMax={softMax}
|
||||||
options={[
|
setSoftMin={setSoftMin}
|
||||||
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
|
setSoftMax={setSoftMax}
|
||||||
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
|
isLogScale={isLogScale}
|
||||||
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
|
setIsLogScale={setIsLogScale}
|
||||||
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
|
/>
|
||||||
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
|
|
||||||
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
|
|
||||||
]}
|
|
||||||
value={decimalPrecision}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
|
||||||
defaultValue={PrecisionOptionsEnum.TWO}
|
|
||||||
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowSoftMinMax && (
|
{isLegendSectionVisible && (
|
||||||
<section className="soft-min-max">
|
<LegendSection
|
||||||
<section className="container">
|
allowLegendPosition={allowLegendPosition}
|
||||||
<Typography.Text className="text">Soft Min</Typography.Text>
|
allowLegendColors={allowLegendColors}
|
||||||
<InputNumber
|
legendPosition={legendPosition}
|
||||||
type="number"
|
setLegendPosition={setLegendPosition}
|
||||||
value={softMin}
|
customLegendColors={customLegendColors}
|
||||||
onChange={softMinHandler}
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
rootClassName="input"
|
queryResponse={queryResponse}
|
||||||
/>
|
/>
|
||||||
</section>
|
|
||||||
<section className="container">
|
|
||||||
<Typography.Text className="text">Soft Max</Typography.Text>
|
|
||||||
<InputNumber
|
|
||||||
value={softMax}
|
|
||||||
type="number"
|
|
||||||
rootClassName="input"
|
|
||||||
onChange={softMaxHandler}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowStackingBarChart && (
|
|
||||||
<section className="stack-chart">
|
|
||||||
<Typography.Text className="label">Stack series</Typography.Text>
|
|
||||||
<Switch
|
|
||||||
checked={stackedBarChart}
|
|
||||||
size="small"
|
|
||||||
onChange={(checked): void => setStackedBarChart(checked)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowBucketConfig && (
|
{allowBucketConfig && (
|
||||||
<section className="bucket-config">
|
<HistogramBucketsSection
|
||||||
<Typography.Text className="label">Number of buckets</Typography.Text>
|
bucketCount={bucketCount}
|
||||||
<InputNumber
|
setBucketCount={setBucketCount}
|
||||||
value={bucketCount || null}
|
bucketWidth={bucketWidth}
|
||||||
type="number"
|
setBucketWidth={setBucketWidth}
|
||||||
min={0}
|
combineHistogram={combineHistogram}
|
||||||
rootClassName="bucket-input"
|
setCombineHistogram={setCombineHistogram}
|
||||||
placeholder="Default: 30"
|
/>
|
||||||
onChange={(val): void => {
|
|
||||||
setBucketCount(val || 0);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography.Text className="label bucket-size-label">
|
|
||||||
Bucket width
|
|
||||||
</Typography.Text>
|
|
||||||
<InputNumber
|
|
||||||
value={bucketWidth || null}
|
|
||||||
type="number"
|
|
||||||
precision={2}
|
|
||||||
placeholder="Default: Auto"
|
|
||||||
step={0.1}
|
|
||||||
min={0.0}
|
|
||||||
rootClassName="bucket-input"
|
|
||||||
onChange={(val): void => {
|
|
||||||
setBucketWidth(val || 0);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<section className="combine-hist">
|
|
||||||
<Typography.Text className="label">
|
|
||||||
Merge all series into one
|
|
||||||
</Typography.Text>
|
|
||||||
<Switch
|
|
||||||
checked={combineHistogram}
|
|
||||||
size="small"
|
|
||||||
onChange={(checked): void => setCombineHistogram(checked)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowLogScale && (
|
|
||||||
<section className="log-scale">
|
|
||||||
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
|
|
||||||
<Select
|
|
||||||
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
|
|
||||||
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
|
||||||
defaultValue={LogScale.LINEAR}
|
|
||||||
>
|
|
||||||
<Option value={LogScale.LINEAR}>
|
|
||||||
<div className="select-option">
|
|
||||||
<div className="icon">
|
|
||||||
<LineChart size={16} />
|
|
||||||
</div>
|
|
||||||
<Typography.Text className="display">Linear</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
<Option value={LogScale.LOGARITHMIC}>
|
|
||||||
<div className="select-option">
|
|
||||||
<div className="icon">
|
|
||||||
<Spline size={16} />
|
|
||||||
</div>
|
|
||||||
<Typography.Text className="display">Logarithmic</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowLegendPosition && (
|
|
||||||
<section className="legend-position">
|
|
||||||
<Typography.Text className="typography">Legend Position</Typography.Text>
|
|
||||||
<Select
|
|
||||||
onChange={(value: LegendPosition): void => setLegendPosition(value)}
|
|
||||||
value={legendPosition}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
|
||||||
defaultValue={LegendPosition.BOTTOM}
|
|
||||||
>
|
|
||||||
<Option value={LegendPosition.BOTTOM}>
|
|
||||||
<div className="select-option">
|
|
||||||
<Typography.Text className="display">Bottom</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
<Option value={LegendPosition.RIGHT}>
|
|
||||||
<div className="select-option">
|
|
||||||
<Typography.Text className="display">Right</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowLegendColors && (
|
|
||||||
<section className="legend-colors">
|
|
||||||
<LegendColors
|
|
||||||
customLegendColors={customLegendColors}
|
|
||||||
setCustomLegendColors={setCustomLegendColors}
|
|
||||||
queryResponse={queryResponse}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{allowCreateAlerts && (
|
{allowCreateAlerts && (
|
||||||
<section className="alerts" onClick={onCreateAlertsHandler}>
|
<AlertsSection onCreateAlertsHandler={onCreateAlertsHandler} />
|
||||||
<div className="left-section">
|
|
||||||
<ConciergeBell size={14} className="bell-icon" />
|
|
||||||
<Typography.Text className="alerts-text">Alerts</Typography.Text>
|
|
||||||
<SquareArrowOutUpRight size={10} className="info-icon" />
|
|
||||||
</div>
|
|
||||||
<Plus size={14} className="plus-icon" />
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowContextLinks && (
|
{allowContextLinks && (
|
||||||
<section className="context-links">
|
<ContextLinksSection
|
||||||
<ContextLinks
|
contextLinks={contextLinks}
|
||||||
contextLinks={contextLinks}
|
setContextLinks={setContextLinks}
|
||||||
setContextLinks={setContextLinks}
|
selectedWidget={selectedWidget}
|
||||||
selectedWidget={selectedWidget}
|
/>
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowThreshold && (
|
{allowThreshold && (
|
||||||
<section>
|
<ThresholdsSection
|
||||||
<ThresholdSelector
|
thresholds={thresholds}
|
||||||
thresholds={thresholds}
|
setThresholds={setThresholds}
|
||||||
setThresholds={setThresholds}
|
yAxisUnit={yAxisUnit}
|
||||||
yAxisUnit={yAxisUnit}
|
selectedGraph={selectedGraph}
|
||||||
selectedGraph={selectedGraph}
|
columnUnits={columnUnits}
|
||||||
columnUnits={columnUnits}
|
/>
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -615,6 +356,14 @@ export interface RightContainerProps {
|
|||||||
setContextLinks: Dispatch<SetStateAction<ContextLinksData>>;
|
setContextLinks: Dispatch<SetStateAction<ContextLinksData>>;
|
||||||
enableDrillDown?: boolean;
|
enableDrillDown?: boolean;
|
||||||
isNewDashboard: boolean;
|
isNewDashboard: boolean;
|
||||||
|
lineInterpolation: LineInterpolation;
|
||||||
|
setLineInterpolation: Dispatch<SetStateAction<LineInterpolation>>;
|
||||||
|
fillMode: FillMode;
|
||||||
|
setFillMode: Dispatch<SetStateAction<FillMode>>;
|
||||||
|
lineStyle: LineStyle;
|
||||||
|
setLineStyle: Dispatch<SetStateAction<LineStyle>>;
|
||||||
|
showPoints: boolean;
|
||||||
|
setShowPoints: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
RightContainer.defaultProps = {
|
RightContainer.defaultProps = {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const checkStackSeriesState = (
|
|||||||
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
const stackSeriesSection = container.querySelector(
|
const stackSeriesSection = container.querySelector(
|
||||||
'section > .stack-chart',
|
'.stack-chart',
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
expect(stackSeriesSection).toBeInTheDocument();
|
expect(stackSeriesSection).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
|
|||||||
expect(getByText('Stack series')).toBeInTheDocument();
|
expect(getByText('Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify section exists
|
// Verify section exists
|
||||||
const section = container.querySelector('section > .stack-chart');
|
const section = container.querySelector('.stack-chart');
|
||||||
expect(section).toBeInTheDocument();
|
expect(section).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify switch is present and enabled (ant-switch-checked)
|
// Verify switch is present and enabled (ant-switch-checked)
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import { UseQueryResult } from 'react-query';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
import { WarningOutlined } from '@ant-design/icons';
|
import { WarningOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from '@signozhq/resizable';
|
||||||
import { Button, Flex, Modal, Space, Typography } from 'antd';
|
import { Button, Flex, Modal, Space, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||||
@@ -24,12 +29,16 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
|||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||||
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
||||||
|
import {
|
||||||
|
FillMode,
|
||||||
|
LineInterpolation,
|
||||||
|
LineStyle,
|
||||||
|
} from 'lib/uPlotV2/config/types';
|
||||||
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
|
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
|
||||||
import { Check, X } from 'lucide-react';
|
import { Check, X } from 'lucide-react';
|
||||||
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
|
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
|
||||||
@@ -63,12 +72,7 @@ import QueryTypeTag from './LeftContainer/QueryTypeTag';
|
|||||||
import RightContainer from './RightContainer';
|
import RightContainer from './RightContainer';
|
||||||
import { ThresholdProps } from './RightContainer/Threshold/types';
|
import { ThresholdProps } from './RightContainer/Threshold/types';
|
||||||
import TimeItems, { timePreferance } from './RightContainer/timeItems';
|
import TimeItems, { timePreferance } from './RightContainer/timeItems';
|
||||||
import {
|
import { Container, PanelContainer } from './styles';
|
||||||
Container,
|
|
||||||
LeftContainerWrapper,
|
|
||||||
PanelContainer,
|
|
||||||
RightContainerWrapper,
|
|
||||||
} from './styles';
|
|
||||||
import { NewWidgetProps } from './types';
|
import { NewWidgetProps } from './types';
|
||||||
import {
|
import {
|
||||||
getDefaultWidgetData,
|
getDefaultWidgetData,
|
||||||
@@ -204,6 +208,18 @@ function NewWidget({
|
|||||||
const [legendPosition, setLegendPosition] = useState<LegendPosition>(
|
const [legendPosition, setLegendPosition] = useState<LegendPosition>(
|
||||||
selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||||
);
|
);
|
||||||
|
const [lineInterpolation, setLineInterpolation] = useState<LineInterpolation>(
|
||||||
|
selectedWidget?.lineInterpolation || LineInterpolation.Spline,
|
||||||
|
);
|
||||||
|
const [fillMode, setFillMode] = useState<FillMode>(
|
||||||
|
selectedWidget?.fillMode || FillMode.None,
|
||||||
|
);
|
||||||
|
const [lineStyle, setLineStyle] = useState<LineStyle>(
|
||||||
|
selectedWidget?.lineStyle || LineStyle.Solid,
|
||||||
|
);
|
||||||
|
const [showPoints, setShowPoints] = useState<boolean>(
|
||||||
|
selectedWidget?.showPoints ?? false,
|
||||||
|
);
|
||||||
const [customLegendColors, setCustomLegendColors] = useState<
|
const [customLegendColors, setCustomLegendColors] = useState<
|
||||||
Record<string, string>
|
Record<string, string>
|
||||||
>(selectedWidget?.customLegendColors || {});
|
>(selectedWidget?.customLegendColors || {});
|
||||||
@@ -269,6 +285,10 @@ function NewWidget({
|
|||||||
softMin,
|
softMin,
|
||||||
softMax,
|
softMax,
|
||||||
fillSpans: isFillSpans,
|
fillSpans: isFillSpans,
|
||||||
|
lineInterpolation,
|
||||||
|
fillMode,
|
||||||
|
lineStyle,
|
||||||
|
showPoints,
|
||||||
columnUnits,
|
columnUnits,
|
||||||
bucketCount,
|
bucketCount,
|
||||||
stackedBarChart,
|
stackedBarChart,
|
||||||
@@ -304,6 +324,10 @@ function NewWidget({
|
|||||||
stackedBarChart,
|
stackedBarChart,
|
||||||
isLogScale,
|
isLogScale,
|
||||||
legendPosition,
|
legendPosition,
|
||||||
|
lineInterpolation,
|
||||||
|
fillMode,
|
||||||
|
lineStyle,
|
||||||
|
showPoints,
|
||||||
customLegendColors,
|
customLegendColors,
|
||||||
contextLinks,
|
contextLinks,
|
||||||
selectedWidget.columnWidths,
|
selectedWidget.columnWidths,
|
||||||
@@ -439,6 +463,19 @@ function NewWidget({
|
|||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const navigateToDashboardPage = useCallback(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
const urlVariablesQueryString = query.get(QueryParams.variables);
|
||||||
|
if (urlVariablesQueryString) {
|
||||||
|
params.set(QueryParams.variables, urlVariablesQueryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = params.toString() ? `?${params.toString()}` : '';
|
||||||
|
|
||||||
|
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }) + search);
|
||||||
|
}, [dashboardId, query, safeNavigate]);
|
||||||
|
|
||||||
const onClickSaveHandler = useCallback(() => {
|
const onClickSaveHandler = useCallback(() => {
|
||||||
if (!selectedDashboard) {
|
if (!selectedDashboard) {
|
||||||
return;
|
return;
|
||||||
@@ -554,9 +591,7 @@ function NewWidget({
|
|||||||
updateDashboardMutation.mutateAsync(dashboard, {
|
updateDashboardMutation.mutateAsync(dashboard, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setToScrollWidgetId(selectedWidget?.id || '');
|
setToScrollWidgetId(selectedWidget?.id || '');
|
||||||
safeNavigate({
|
navigateToDashboardPage();
|
||||||
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
@@ -572,7 +607,7 @@ function NewWidget({
|
|||||||
updateDashboardMutation,
|
updateDashboardMutation,
|
||||||
widgets,
|
widgets,
|
||||||
setToScrollWidgetId,
|
setToScrollWidgetId,
|
||||||
safeNavigate,
|
navigateToDashboardPage,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -581,12 +616,12 @@ function NewWidget({
|
|||||||
setDiscardModal(true);
|
setDiscardModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
navigateToDashboardPage();
|
||||||
}, [dashboardId, isQueryModified, safeNavigate]);
|
}, [isQueryModified, navigateToDashboardPage]);
|
||||||
|
|
||||||
const discardChanges = useCallback(() => {
|
const discardChanges = useCallback(() => {
|
||||||
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
navigateToDashboardPage();
|
||||||
}, [dashboardId, safeNavigate]);
|
}, [navigateToDashboardPage]);
|
||||||
|
|
||||||
const setGraphHandler = (type: PANEL_TYPES): void => {
|
const setGraphHandler = (type: PANEL_TYPES): void => {
|
||||||
setIsLoadingPanelData(true);
|
setIsLoadingPanelData(true);
|
||||||
@@ -728,12 +763,14 @@ function NewWidget({
|
|||||||
}
|
}
|
||||||
const widgetId = query.get('widgetId') || '';
|
const widgetId = query.get('widgetId') || '';
|
||||||
const graphType = query.get('graphType') || '';
|
const graphType = query.get('graphType') || '';
|
||||||
|
const variables = query.get(QueryParams.variables) || '';
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
[QueryParams.expandedWidgetId]: widgetId,
|
[QueryParams.expandedWidgetId]: widgetId,
|
||||||
[QueryParams.graphType]: graphType,
|
[QueryParams.graphType]: graphType,
|
||||||
[QueryParams.compositeQuery]: encodeURIComponent(
|
[QueryParams.compositeQuery]: encodeURIComponent(
|
||||||
JSON.stringify(currentQuery),
|
JSON.stringify(currentQuery),
|
||||||
),
|
),
|
||||||
|
[QueryParams.variables]: variables,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedSearch = createQueryParams(queryParams);
|
const updatedSearch = createQueryParams(queryParams);
|
||||||
@@ -744,7 +781,7 @@ function NewWidget({
|
|||||||
}, [query, safeNavigate, dashboardId, currentQuery]);
|
}, [query, safeNavigate, dashboardId, currentQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className="new-widget-container">
|
||||||
<div className="edit-header">
|
<div className="edit-header">
|
||||||
<div className="left-header">
|
<div className="left-header">
|
||||||
<X
|
<X
|
||||||
@@ -798,31 +835,44 @@ function NewWidget({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PanelContainer>
|
<PanelContainer>
|
||||||
<LeftContainerWrapper isDarkMode={useIsDarkMode()}>
|
<ResizablePanelGroup
|
||||||
<OverlayScrollbar>
|
direction="horizontal"
|
||||||
{selectedWidget && (
|
className="widget-resizable-panel-group"
|
||||||
<LeftContainer
|
autoSaveId="panel-editor"
|
||||||
selectedGraph={graphType}
|
>
|
||||||
selectedLogFields={selectedLogFields}
|
<ResizablePanel
|
||||||
setSelectedLogFields={setSelectedLogFields}
|
minSize={70}
|
||||||
selectedTracesFields={selectedTracesFields}
|
maxSize={80}
|
||||||
setSelectedTracesFields={setSelectedTracesFields}
|
defaultSize={80}
|
||||||
selectedWidget={selectedWidget}
|
className="resizable-panel-left-container"
|
||||||
selectedTime={selectedTime}
|
>
|
||||||
requestData={requestData}
|
<OverlayScrollbar>
|
||||||
setRequestData={setRequestData}
|
{selectedWidget && (
|
||||||
isLoadingPanelData={isLoadingPanelData}
|
<LeftContainer
|
||||||
setQueryResponse={setQueryResponse}
|
selectedDashboard={selectedDashboard}
|
||||||
enableDrillDown={enableDrillDown}
|
selectedGraph={graphType}
|
||||||
selectedDashboard={selectedDashboard}
|
selectedLogFields={selectedLogFields}
|
||||||
isNewPanel={isNewPanel}
|
setSelectedLogFields={setSelectedLogFields}
|
||||||
/>
|
selectedTracesFields={selectedTracesFields}
|
||||||
)}
|
setSelectedTracesFields={setSelectedTracesFields}
|
||||||
</OverlayScrollbar>
|
selectedWidget={selectedWidget}
|
||||||
</LeftContainerWrapper>
|
selectedTime={selectedTime}
|
||||||
|
requestData={requestData}
|
||||||
<RightContainerWrapper>
|
setRequestData={setRequestData}
|
||||||
<OverlayScrollbar>
|
isLoadingPanelData={isLoadingPanelData}
|
||||||
|
setQueryResponse={setQueryResponse}
|
||||||
|
enableDrillDown={enableDrillDown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OverlayScrollbar>
|
||||||
|
</ResizablePanel>
|
||||||
|
<ResizableHandle withHandle className="widget-resizable-handle" />
|
||||||
|
<ResizablePanel
|
||||||
|
minSize={20}
|
||||||
|
maxSize={30}
|
||||||
|
defaultSize={20}
|
||||||
|
className="resizable-panel-right-container"
|
||||||
|
>
|
||||||
<RightContainer
|
<RightContainer
|
||||||
setGraphHandler={setGraphHandler}
|
setGraphHandler={setGraphHandler}
|
||||||
title={title}
|
title={title}
|
||||||
@@ -831,6 +881,14 @@ function NewWidget({
|
|||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
stackedBarChart={stackedBarChart}
|
stackedBarChart={stackedBarChart}
|
||||||
setStackedBarChart={setStackedBarChart}
|
setStackedBarChart={setStackedBarChart}
|
||||||
|
lineInterpolation={lineInterpolation}
|
||||||
|
setLineInterpolation={setLineInterpolation}
|
||||||
|
fillMode={fillMode}
|
||||||
|
setFillMode={setFillMode}
|
||||||
|
lineStyle={lineStyle}
|
||||||
|
setLineStyle={setLineStyle}
|
||||||
|
showPoints={showPoints}
|
||||||
|
setShowPoints={setShowPoints}
|
||||||
opacity={opacity}
|
opacity={opacity}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
columnUnits={columnUnits}
|
columnUnits={columnUnits}
|
||||||
@@ -871,8 +929,8 @@ function NewWidget({
|
|||||||
enableDrillDown={enableDrillDown}
|
enableDrillDown={enableDrillDown}
|
||||||
isNewDashboard={isNewDashboard}
|
isNewDashboard={isNewDashboard}
|
||||||
/>
|
/>
|
||||||
</OverlayScrollbar>
|
</ResizablePanel>
|
||||||
</RightContainerWrapper>
|
</ResizablePanelGroup>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Col, Tag as AntDTag } from 'antd';
|
import { Tag as AntDTag } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled.div`
|
||||||
@@ -8,35 +8,6 @@ export const Container = styled.div`
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RightContainerWrapper = styled(Col)`
|
|
||||||
&&& {
|
|
||||||
max-width: 400px;
|
|
||||||
width: 30%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface LeftContainerWrapperProps {
|
|
||||||
isDarkMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LeftContainerWrapper = styled(Col)<LeftContainerWrapperProps>`
|
|
||||||
&&& {
|
|
||||||
width: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
border-right: ${({ isDarkMode }): string =>
|
|
||||||
isDarkMode
|
|
||||||
? '1px solid var(--bg-slate-300)'
|
|
||||||
: '1px solid var(--bg-vanilla-300)'};
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ButtonContainer = styled.div`
|
export const ButtonContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -11,11 +11,8 @@ import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
|
|||||||
import {
|
import {
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
|
||||||
import {
|
|
||||||
listViewInitialLogQuery,
|
|
||||||
PANEL_TYPES_INITIAL_QUERY,
|
PANEL_TYPES_INITIAL_QUERY,
|
||||||
} from 'container/DashboardContainer/ComponentsSlider/constants';
|
} from 'constants/queryBuilder';
|
||||||
import {
|
import {
|
||||||
defaultLogsSelectedColumns,
|
defaultLogsSelectedColumns,
|
||||||
defaultTraceSelectedColumns,
|
defaultTraceSelectedColumns,
|
||||||
@@ -549,10 +546,7 @@ export const getDefaultWidgetData = (
|
|||||||
nullZeroValues: '',
|
nullZeroValues: '',
|
||||||
opacity: '',
|
opacity: '',
|
||||||
panelTypes: name,
|
panelTypes: name,
|
||||||
query:
|
query: PANEL_TYPES_INITIAL_QUERY[name],
|
||||||
name === PANEL_TYPES.LIST
|
|
||||||
? listViewInitialLogQuery
|
|
||||||
: PANEL_TYPES_INITIAL_QUERY[name],
|
|
||||||
timePreferance: 'GLOBAL_TIME',
|
timePreferance: 'GLOBAL_TIME',
|
||||||
softMax: null,
|
softMax: null,
|
||||||
softMin: null,
|
softMin: null,
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
usePatchRole,
|
usePatchRole,
|
||||||
} from 'api/generated/services/role';
|
} from 'api/generated/services/role';
|
||||||
import {
|
import {
|
||||||
|
AuthtypesPostableRoleDTO,
|
||||||
RenderErrorResponseDTO,
|
RenderErrorResponseDTO,
|
||||||
RoletypesPostableRoleDTO,
|
|
||||||
} from 'api/generated/services/sigNoz.schemas';
|
} from 'api/generated/services/sigNoz.schemas';
|
||||||
import { ErrorType } from 'api/generatedAPIInstance';
|
import { ErrorType } from 'api/generatedAPIInstance';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@@ -114,7 +114,7 @@ function CreateRoleModal({
|
|||||||
data: { description: values.description || '' },
|
data: { description: values.description || '' },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const data: RoletypesPostableRoleDTO = {
|
const data: AuthtypesPostableRoleDTO = {
|
||||||
name: values.name,
|
name: values.name,
|
||||||
...(values.description ? { description: values.description } : {}),
|
...(values.description ? { description: values.description } : {}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Pagination, Skeleton } from 'antd';
|
import { Pagination, Skeleton } from 'antd';
|
||||||
import { useListRoles } from 'api/generated/services/role';
|
import { useListRoles } from 'api/generated/services/role';
|
||||||
import { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@@ -20,7 +20,7 @@ const PAGE_SIZE = 20;
|
|||||||
|
|
||||||
type DisplayItem =
|
type DisplayItem =
|
||||||
| { type: 'section'; label: string; count?: number }
|
| { type: 'section'; label: string; count?: number }
|
||||||
| { type: 'role'; role: RoletypesRoleDTO };
|
| { type: 'role'; role: AuthtypesRoleDTO };
|
||||||
|
|
||||||
interface RolesListingTableProps {
|
interface RolesListingTableProps {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@@ -187,7 +187,7 @@ function RolesListingTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// todo: use table from periscope when its available for consumption
|
// todo: use table from periscope when its available for consumption
|
||||||
const renderRow = (role: RoletypesRoleDTO): JSX.Element => (
|
const renderRow = (role: AuthtypesRoleDTO): JSX.Element => (
|
||||||
<div
|
<div
|
||||||
key={role.id}
|
key={role.id}
|
||||||
className={`roles-table-row ${
|
className={`roles-table-row ${
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||||
|
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||||
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
|
||||||
|
jest.mock('hooks/dashboard/useVariablesFromUrl');
|
||||||
|
|
||||||
|
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
|
||||||
|
typeof useDashboardVariablesFromLocalStorage
|
||||||
|
>;
|
||||||
|
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
|
||||||
|
typeof useVariablesFromUrl
|
||||||
|
>;
|
||||||
|
|
||||||
|
const makeVariable = (
|
||||||
|
overrides: Partial<IDashboardVariable> = {},
|
||||||
|
): IDashboardVariable => ({
|
||||||
|
id: 'existing-id',
|
||||||
|
name: 'env',
|
||||||
|
description: '',
|
||||||
|
type: 'QUERY',
|
||||||
|
sort: 'DISABLED',
|
||||||
|
multiSelect: false,
|
||||||
|
showALLOption: false,
|
||||||
|
selectedValue: 'prod',
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeDashboard = (
|
||||||
|
variables: Record<string, IDashboardVariable>,
|
||||||
|
): Dashboard => ({
|
||||||
|
id: 'dash-1',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
createdBy: '',
|
||||||
|
updatedBy: '',
|
||||||
|
data: {
|
||||||
|
title: 'Test',
|
||||||
|
variables,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupHook = (
|
||||||
|
currentDashboard: Record<string, any> = {},
|
||||||
|
urlVariables: Record<string, any> = {},
|
||||||
|
): ReturnType<typeof useTransformDashboardVariables> => {
|
||||||
|
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
|
||||||
|
currentDashboard,
|
||||||
|
updateLocalStorageDashboardVariables: jest.fn(),
|
||||||
|
});
|
||||||
|
mockUseVariablesFromUrl.mockReturnValue({
|
||||||
|
getUrlVariables: () => urlVariables,
|
||||||
|
setUrlVariables: jest.fn(),
|
||||||
|
updateUrlVariable: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTransformDashboardVariables('dash-1'));
|
||||||
|
return result.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useTransformDashboardVariables', () => {
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
describe('order assignment', () => {
|
||||||
|
it('assigns order starting from 0 to variables that have none', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
|
||||||
|
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
const orders = Object.values(result.data.variables).map((v) => v.order);
|
||||||
|
expect(orders).toContain(0);
|
||||||
|
expect(orders).toContain(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing order values', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: 5 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.order).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assigns unique orders across multiple variables that all lack an order', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
|
||||||
|
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
|
||||||
|
v3: makeVariable({ id: 'id3', name: 'v3', order: undefined }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
const orders = Object.values(result.data.variables).map((v) => v.order);
|
||||||
|
// All three newly assigned orders must be distinct
|
||||||
|
expect(new Set(orders).size).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ID assignment', () => {
|
||||||
|
it('assigns a UUID to variables that have no id', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const variable = makeVariable({ name: 'v1' });
|
||||||
|
(variable as any).id = undefined;
|
||||||
|
const dashboard = makeDashboard({ v1: variable });
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.id).toMatch(
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing IDs', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'keep-me', name: 'v1' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.id).toBe('keep-me');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TEXTBOX backward compatibility', () => {
|
||||||
|
it('copies textboxValue to defaultValue when defaultValue is missing', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'v1',
|
||||||
|
type: 'TEXTBOX',
|
||||||
|
textboxValue: 'hello',
|
||||||
|
defaultValue: undefined,
|
||||||
|
order: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.defaultValue).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not overwrite an existing defaultValue', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'v1',
|
||||||
|
type: 'TEXTBOX',
|
||||||
|
textboxValue: 'old',
|
||||||
|
defaultValue: 'keep',
|
||||||
|
order: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.defaultValue).toBe('keep');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('localStorage merge', () => {
|
||||||
|
it('applies localStorage selectedValue over DB value', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: 'staging', allSelected: false },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('staging');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies localStorage allSelected over DB value', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: undefined, allSelected: true },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
allSelected: false,
|
||||||
|
showALLOption: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL variable override', () => {
|
||||||
|
it('sets allSelected=true when URL value is __ALL__', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: 'prod', allSelected: false } },
|
||||||
|
{ env: '__ALL__' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: true,
|
||||||
|
allSelected: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets selectedValue from URL and clears allSelected when showALLOption is true', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: undefined, allSelected: true } },
|
||||||
|
{ env: 'dev' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: true,
|
||||||
|
allSelected: true,
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('dev');
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set allSelected=false when showALLOption is false', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{ env: { selectedValue: undefined, allSelected: true } },
|
||||||
|
{ env: 'dev' },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
showALLOption: false,
|
||||||
|
allSelected: true,
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('dev');
|
||||||
|
expect(result.data.variables.v1.allSelected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes array URL value to single value for single-select variable', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{},
|
||||||
|
{ env: ['prod', 'dev'] },
|
||||||
|
);
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
multiSelect: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('prod');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps single URL value in array for multi-select variable', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({}, { env: 'prod' });
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({
|
||||||
|
id: 'id1',
|
||||||
|
name: 'env',
|
||||||
|
multiSelect: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toEqual(['prod']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('looks up URL variable by variable id when name is absent', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook(
|
||||||
|
{},
|
||||||
|
{ 'var-uuid': 'fallback' },
|
||||||
|
);
|
||||||
|
const variable = makeVariable({ id: 'var-uuid', multiSelect: false });
|
||||||
|
delete variable.name;
|
||||||
|
const dashboard = makeDashboard({ v1: variable });
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables.v1.selectedValue).toBe('fallback');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('returns data unchanged when there are no variables', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook();
|
||||||
|
const dashboard = makeDashboard({});
|
||||||
|
|
||||||
|
const result = transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(result.data.variables).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate the original dashboard', () => {
|
||||||
|
const { transformDashboardVariables } = setupHook({
|
||||||
|
env: { selectedValue: 'staging', allSelected: false },
|
||||||
|
});
|
||||||
|
const dashboard = makeDashboard({
|
||||||
|
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
|
||||||
|
});
|
||||||
|
const originalValue = dashboard.data.variables.v1.selectedValue;
|
||||||
|
|
||||||
|
transformDashboardVariables(dashboard);
|
||||||
|
|
||||||
|
expect(dashboard.data.variables.v1.selectedValue).toBe(originalValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,7 +15,7 @@ interface DashboardLocalStorageVariables {
|
|||||||
[id: string]: LocalStoreDashboardVariables;
|
[id: string]: LocalStoreDashboardVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDashboardVariablesFromLocalStorageReturn {
|
export interface UseDashboardVariablesFromLocalStorageReturn {
|
||||||
currentDashboard: LocalStoreDashboardVariables;
|
currentDashboard: LocalStoreDashboardVariables;
|
||||||
updateLocalStorageDashboardVariables: (
|
updateLocalStorageDashboardVariables: (
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user