Compare commits

..

3 Commits

Author SHA1 Message Date
nityanandagohain
0ee31ce440 chore: fix tests 2026-03-17 18:16:51 +05:30
nityanandagohain
63e681b87b chore: add integration tests 2026-03-17 15:38:00 +05:30
nityanandagohain
28375c8c1e chore: send all data for trace list api 2026-03-13 19:31:59 +05:30
242 changed files with 3683 additions and 10569 deletions

14
.github/CODEOWNERS vendored
View File

@@ -1,6 +1,8 @@
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
# Owners are automatically requested for review for PRs that changes code that they own.
# Owners are automatically requested for review for PRs that changes code
# that they own.
/frontend/ @SigNoz/frontend-maintainers
@@ -9,10 +11,8 @@
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
# CI
/deploy/ @therealpandey
.github @therealpandey
go.mod @therealpandey
/deploy/ @SigNoz/devops
.github @SigNoz/devops
# Scaffold Owners
@@ -105,10 +105,6 @@ go.mod @therealpandey
/pkg/modules/authdomain/ @vikrantgupta25
/pkg/modules/role/ @vikrantgupta25
# IdentN Owners
/pkg/identn/ @vikrantgupta25
/pkg/http/middleware/identn.go @vikrantgupta25
# Integration tests
/tests/integration/ @vikrantgupta25

View File

@@ -102,3 +102,13 @@ jobs:
run: |
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)
- 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)

View File

@@ -52,16 +52,16 @@ jobs:
with:
PRIMUS_REF: main
JS_SRC: frontend
languages:
md-languages:
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
- name: checkout
uses: actions/checkout@v4
- name: run
- name: validate md languages
run: bash frontend/scripts/validate-md-languages.sh
authz:
if: |
@@ -70,55 +70,44 @@ jobs:
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
- name: Checkout code
uses: actions/checkout@v5
- name: node-install
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: "22"
- name: deps-install
- name: Install frontend dependencies
working-directory: ./frontend
run: |
yarn install
- name: uv-install
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: uv-deps
- name: Install Python dependencies
working-directory: ./tests/integration
run: |
uv sync
- name: setup-test
- name: Start test environment
run: |
make py-test-setup
- name: generate
- name: Generate permissions.type.ts
working-directory: ./frontend
run: |
yarn generate:permissions-type
- name: teardown-test
- name: Teardown test environment
if: always()
run: |
make py-test-teardown
- name: validate
- name: Check for changes
run: |
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)"
exit 1
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)

View File

@@ -1,60 +0,0 @@
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."

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/version"
"github.com/spf13/cobra"
"go.uber.org/zap" //nolint:depguard
)
var RootCmd = &cobra.Command{
@@ -18,6 +19,12 @@ var RootCmd = &cobra.Command{
}
func Execute(logger *slog.Logger) {
zapLogger := newZapLogger()
zap.ReplaceGlobals(zapLogger)
defer func() {
_ = zapLogger.Sync()
}()
err := RootCmd.Execute()
if err != nil {
logger.ErrorContext(RootCmd.Context(), "error running command", "error", err)

110
cmd/zap.go Normal file
View File

@@ -0,0 +1,110 @@
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)
}

View File

@@ -321,19 +321,3 @@ user:
org:
name: default
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

View File

@@ -0,0 +1,38 @@
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

View File

@@ -0,0 +1,69 @@
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

View File

@@ -0,0 +1,102 @@
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]

View File

@@ -0,0 +1,103 @@
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]

View File

@@ -0,0 +1,39 @@
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

View File

@@ -0,0 +1,43 @@
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

View File

@@ -0,0 +1,139 @@
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]

View File

@@ -220,13 +220,6 @@ components:
- additions
- deletions
type: object
AuthtypesPatchableRole:
properties:
description:
type: string
required:
- description
type: object
AuthtypesPostableAuthDomain:
properties:
config:
@@ -243,15 +236,6 @@ components:
password:
type: string
type: object
AuthtypesPostableRole:
properties:
description:
type: string
name:
type: string
required:
- name
type: object
AuthtypesPostableRotateToken:
properties:
refreshToken:
@@ -267,31 +251,6 @@ components:
- name
- type
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:
properties:
defaultRole:
@@ -1763,6 +1722,47 @@ components:
- status
- error
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:
properties:
createdAt:
@@ -4234,7 +4234,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesRole'
$ref: '#/components/schemas/RoletypesRole'
type: array
status:
type: string
@@ -4277,7 +4277,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPostableRole'
$ref: '#/components/schemas/RoletypesPostableRole'
responses:
"201":
content:
@@ -4422,7 +4422,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesRole'
$ref: '#/components/schemas/RoletypesRole'
status:
type: string
required:
@@ -4470,7 +4470,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPatchableRole'
$ref: '#/components/schemas/RoletypesPatchableRole'
responses:
"204":
content:

View File

@@ -13,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
@@ -22,7 +23,7 @@ type provider struct {
pkgAuthzService authz.AuthZ
openfgaServer *openfgaserver.Server
licensing licensing.Licensing
store authtypes.RoleStore
store roletypes.Store
registry []authz.RegisterTypeable
}
@@ -81,23 +82,23 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl
return provider.openfgaServer.Write(ctx, additions, deletions)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.List(ctx, orgID)
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndIDs(ctx, orgID, ids)
}
@@ -113,7 +114,7 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
return provider.pkgAuthzService.Revoke(ctx, orgID, names, subject)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*authtypes.Role) error {
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*roletypes.Role) error {
return provider.pkgAuthzService.CreateManagedRoles(ctx, orgID, managedRoles)
}
@@ -135,16 +136,16 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
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 provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
return provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
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())
@@ -158,10 +159,10 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
}
if existingRole != nil {
return authtypes.NewRoleFromStorableRole(existingRole), nil
return roletypes.NewRoleFromStorableRole(existingRole), nil
}
err = provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
err = provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
if err != nil {
return nil, err
}
@@ -216,13 +217,13 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return objects, nil
}
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
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 provider.store.Update(ctx, orgID, authtypes.NewStorableRoleFromRole(role))
return provider.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
}
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
@@ -231,12 +232,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())
}
additionTuples, err := authtypes.GetAdditionTuples(name, orgID, relation, additions)
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
if err != nil {
return err
}
deletionTuples, err := authtypes.GetDeletionTuples(name, orgID, relation, deletions)
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
if err != nil {
return err
}
@@ -260,7 +261,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return err
}
role := authtypes.NewRoleFromStorableRole(storableRole)
role := roletypes.NewRoleFromStorableRole(storableRole)
err = role.ErrIfManaged()
if err != nil {
return err
@@ -270,7 +271,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
@@ -282,7 +283,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
adminSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
},
orgID,
)
@@ -297,7 +298,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
anonymousSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAnonymousRoleName),
},
orgID,
)

View File

@@ -19,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -223,7 +224,7 @@ func (module *module) MustGetTypeables() []authtypes.Typeable {
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
authtypes.SigNozAnonymousRoleName: {
roletypes.SigNozAnonymousRoleName: {
{
ID: valuer.GenerateUUID(),
Relation: authtypes.RelationRead,

View File

@@ -2,7 +2,6 @@ package anomaly
import (
"context"
"log/slog"
"math"
"time"
@@ -14,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
var (
@@ -67,7 +67,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
instrumentationtypes.CodeNamespace: "anomaly",
instrumentationtypes.CodeFunctionName: "getResults",
})
slog.InfoContext(ctx, "fetching results for current period", "current_period_query", params.CurrentPeriodQuery)
zap.L().Info("fetching results for current period", zap.Any("currentPeriodQuery", params.CurrentPeriodQuery))
currentPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentPeriodQuery)
if err != nil {
return nil, err
@@ -78,7 +78,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for past period", "past_period_query", params.PastPeriodQuery)
zap.L().Info("fetching results for past period", zap.Any("pastPeriodQuery", params.PastPeriodQuery))
pastPeriodResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.PastPeriodQuery)
if err != nil {
return nil, err
@@ -89,7 +89,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for current season", "current_season_query", params.CurrentSeasonQuery)
zap.L().Info("fetching results for current season", zap.Any("currentSeasonQuery", params.CurrentSeasonQuery))
currentSeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.CurrentSeasonQuery)
if err != nil {
return nil, err
@@ -100,7 +100,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for past season", "past_season_query", params.PastSeasonQuery)
zap.L().Info("fetching results for past season", zap.Any("pastSeasonQuery", params.PastSeasonQuery))
pastSeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.PastSeasonQuery)
if err != nil {
return nil, err
@@ -111,7 +111,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for past 2 season", "past_2_season_query", params.Past2SeasonQuery)
zap.L().Info("fetching results for past 2 season", zap.Any("past2SeasonQuery", params.Past2SeasonQuery))
past2SeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.Past2SeasonQuery)
if err != nil {
return nil, err
@@ -122,7 +122,7 @@ func (p *BaseSeasonalProvider) getResults(ctx context.Context, orgID valuer.UUID
return nil, err
}
slog.InfoContext(ctx, "fetching results for past 3 season", "past_3_season_query", params.Past3SeasonQuery)
zap.L().Info("fetching results for past 3 season", zap.Any("past3SeasonQuery", params.Past3SeasonQuery))
past3SeasonResults, _, err := p.querierV2.QueryRange(ctx, orgID, params.Past3SeasonQuery)
if err != nil {
return nil, err
@@ -235,17 +235,17 @@ func (p *BaseSeasonalProvider) getPredictedSeries(
if predictedValue < 0 {
// 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
slog.Warn("predicted value is less than 0", "predicted_value", predictedValue, "labels", series.Labels)
zap.L().Warn("predictedValue is less than 0", zap.Float64("predictedValue", predictedValue), zap.Any("labels", series.Labels))
predictedValue = p.getMovingAvg(prevSeries, movingAvgWindowSize, idx)
}
slog.Debug("predicted series",
"moving_avg", movingAvg,
"avg", avg,
"mean", mean,
"labels", series.Labels,
"predicted_value", predictedValue,
"curr", curr.Value,
zap.L().Debug("predictedSeries",
zap.Float64("movingAvg", movingAvg),
zap.Float64("avg", avg),
zap.Float64("mean", mean),
zap.Any("labels", series.Labels),
zap.Float64("predictedValue", predictedValue),
zap.Float64("curr", curr.Value),
)
predictedSeries.Points = append(predictedSeries.Points, v3.Point{
Timestamp: curr.Timestamp,
@@ -418,7 +418,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UU
for _, series := range result.Series {
stdDev := p.getStdDev(series)
slog.InfoContext(ctx, "computed standard deviation", "std_dev", stdDev, "labels", series.Labels)
zap.L().Info("stdDev", zap.Float64("stdDev", stdDev), zap.Any("labels", series.Labels))
pastPeriodSeries := p.getMatchingSeries(pastPeriodResult, series)
currentSeasonSeries := p.getMatchingSeries(currentSeasonResult, series)
@@ -431,7 +431,7 @@ func (p *BaseSeasonalProvider) getAnomalies(ctx context.Context, orgID valuer.UU
pastSeasonSeriesAvg := p.getAvg(pastSeasonSeries)
past2SeasonSeriesAvg := p.getAvg(past2SeasonSeries)
past3SeasonSeriesAvg := p.getAvg(past3SeasonSeries)
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)
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))
predictedSeries := p.getPredictedSeries(
series,

View File

@@ -18,7 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"log/slog"
"go.uber.org/zap"
)
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.
// 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.
slog.InfoContext(r.Context(), "ingestion params and signoz api url can not be deduced since no license was found")
zap.L().Info("ingestion params and signoz api url can not be deduced since no license was found")
ah.Respond(w, result)
return
}
@@ -103,7 +103,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
result.IngestionKey = ingestionKey
} else {
slog.InfoContext(r.Context(), "ingestion key can't be deduced since no gateway url has been configured")
zap.L().Info("ingestion key can't be deduced since no gateway url has been configured")
}
ah.Respond(w, result)
@@ -138,8 +138,9 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
}
}
slog.InfoContext(ctx, "no PAT found for cloud integration, creating a new one",
"cloud_provider", cloudProvider,
zap.L().Info(
"no PAT found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
)
newPAT, err := types.NewStorableAPIKey(
@@ -286,8 +287,9 @@ func getOrCreateCloudProviderIngestionKey(
}
}
slog.InfoContext(ctx, "no existing ingestion key found for cloud integration, creating a new one",
"cloud_provider", cloudProvider,
zap.L().Info(
"no existing ingestion key found for cloud integration, creating a new one",
zap.String("cloudProvider", cloudProvider),
)
createKeyResult, apiErr := requestGateway[createIngestionKeyResponse](
ctx, gatewayUrl, licenseKey, "/v1/workspaces/me/keys",

View File

@@ -15,7 +15,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"log/slog"
"go.uber.org/zap"
)
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" {
slog.DebugContext(ctx, "fetching license")
zap.L().Debug("fetching license")
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
if err != nil {
slog.ErrorContext(ctx, "failed to fetch license", "error", err)
zap.L().Error("failed to fetch license", zap.Error(err))
} else if license == nil {
slog.DebugContext(ctx, "no active license found")
zap.L().Debug("no active license found")
} else {
licenseKey := license.Key
slog.DebugContext(ctx, "fetching zeus features")
zap.L().Debug("fetching zeus features")
zeusFeatures, err := fetchZeusFeatures(constants.ZeusFeaturesURL, licenseKey)
if err == nil {
slog.DebugContext(ctx, "fetched zeus features", "features", zeusFeatures)
zap.L().Debug("fetched zeus features", zap.Any("features", zeusFeatures))
// merge featureSet and zeusFeatures in featureSet with higher priority to zeusFeatures
featureSet = MergeFeatureSets(zeusFeatures, featureSet)
} else {
slog.ErrorContext(ctx, "failed to fetch zeus features", "error", err)
zap.L().Error("failed to fetch zeus features", zap.Error(err))
}
}
}

View File

@@ -14,7 +14,7 @@ import (
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"log/slog"
"go.uber.org/zap"
)
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)
if apiErrorObj != nil {
slog.ErrorContext(r.Context(), "error parsing metric query range params", "error", apiErrorObj.Err)
zap.L().Error("error parsing metric query range params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}
@@ -44,7 +44,7 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
// add temporality for each metric
temporalityErr := aH.PopulateTemporality(r.Context(), orgID, queryRangeParams)
if temporalityErr != nil {
slog.ErrorContext(r.Context(), "error while adding temporality for metrics", "error", temporalityErr)
zap.L().Error("Error while adding temporality for metrics", zap.Error(temporalityErr))
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil)
return
}

View File

@@ -47,7 +47,7 @@ import (
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils"
"log/slog"
"go.uber.org/zap"
)
// Server runs HTTP, Mux and a grpc server
@@ -83,7 +83,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
}
reader := clickhouseReader.NewReader(
signoz.Instrumentation.Logger(),
signoz.SQLStore,
signoz.TelemetryStore,
signoz.Prometheus,
@@ -217,7 +216,8 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
}),
otelmux.WithPublicEndpoint(),
))
r.Use(middleware.NewIdentN(s.signoz.IdentNResolver, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Tokenizer, 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(),
s.config.APIServer.Timeout.ExcludedRoutes,
s.config.APIServer.Timeout.Default,
@@ -278,7 +278,7 @@ func (s *Server) initListeners() error {
return err
}
slog.Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
zap.L().Info(fmt.Sprintf("Query server started listening on %s...", s.httpHostPort))
return nil
}
@@ -298,31 +298,31 @@ func (s *Server) Start(ctx context.Context) error {
}
go func() {
slog.Info("Starting HTTP server", "port", httpPort, "addr", s.httpHostPort)
zap.L().Info("Starting HTTP server", zap.Int("port", httpPort), zap.String("addr", s.httpHostPort))
switch err := s.httpServer.Serve(s.httpConn); err {
case nil, http.ErrServerClosed, cmux.ErrListenerClosed:
// normal exit, nothing to do
default:
slog.Error("Could not start HTTP server", "error", err)
zap.L().Error("Could not start HTTP server", zap.Error(err))
}
s.unavailableChannel <- healthcheck.Unavailable
}()
go func() {
slog.Info("Starting pprof server", "addr", baseconst.DebugHttpPort)
zap.L().Info("Starting pprof server", zap.String("addr", baseconst.DebugHttpPort))
err = http.ListenAndServe(baseconst.DebugHttpPort, nil)
if err != nil {
slog.Error("Could not start pprof server", "error", err)
zap.L().Error("Could not start pprof server", zap.Error(err))
}
}()
go func() {
slog.Info("Starting OpAmp Websocket server", "addr", baseconst.OpAmpWsEndpoint)
zap.L().Info("Starting OpAmp Websocket server", zap.String("addr", baseconst.OpAmpWsEndpoint))
err := s.opampServer.Start(baseconst.OpAmpWsEndpoint)
if err != nil {
slog.Error("opamp ws server failed to start", "error", err)
zap.L().Error("opamp ws server failed to start", zap.Error(err))
s.unavailableChannel <- healthcheck.Unavailable
}
}()
@@ -358,9 +358,10 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
Querier: querier,
Logger: providerSettings.Logger,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
@@ -379,7 +380,7 @@ func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertma
return nil, fmt.Errorf("rule manager error: %v", err)
}
slog.Info("rules manager is ready")
zap.L().Info("rules manager is ready")
return manager, nil
}

View File

@@ -2,7 +2,6 @@ package rules
import (
"context"
"log/slog"
"testing"
"time"
@@ -117,7 +116,7 @@ func TestAnomalyRule_NoData_AlertOnAbsent(t *testing.T) {
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
options := clickhouseReader.NewOptions("primaryNamespace")
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
rule, err := NewAnomalyRule(
"test-anomaly-rule",
@@ -248,7 +247,7 @@ func TestAnomalyRule_NoData_AbsentFor(t *testing.T) {
telemetryStore := telemetrystoretest.New(telemetrystore.Config{}, nil)
options := clickhouseReader.NewOptions("primaryNamespace")
reader := clickhouseReader.NewReader(slog.Default(), nil, telemetryStore, nil, "", time.Second, nil, nil, options)
reader := clickhouseReader.NewReader(nil, telemetryStore, nil, "", time.Second, nil, nil, options)
rule, err := NewAnomalyRule("test-anomaly-rule", valuer.GenerateUUID(), &postableRule, reader, nil, logger, nil)
require.NoError(t, err)

View File

@@ -13,7 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid"
"log/slog"
"go.uber.org/zap"
)
func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error) {
@@ -34,7 +34,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.SLogger,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
@@ -57,7 +57,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
ruleId,
opts.OrgID,
opts.Rule,
opts.Logger,
opts.SLogger,
opts.Reader,
opts.ManagerOpts.Prometheus,
baserules.WithSQLStore(opts.SQLStore),
@@ -82,7 +82,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
opts.Rule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.SLogger,
opts.Cache,
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
baserules.WithSQLStore(opts.SQLStore),
@@ -142,7 +142,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
parsedRule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.SLogger,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
baserules.WithSQLStore(opts.SQLStore),
@@ -151,7 +151,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
)
if err != nil {
slog.Error("failed to prepare a new threshold rule for test", "name", alertname, "error", err)
zap.L().Error("failed to prepare a new threshold rule for test", zap.String("name", alertname), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
@@ -162,7 +162,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
alertname,
opts.OrgID,
parsedRule,
opts.Logger,
opts.SLogger,
opts.Reader,
opts.ManagerOpts.Prometheus,
baserules.WithSendAlways(),
@@ -173,7 +173,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
)
if err != nil {
slog.Error("failed to prepare a new promql rule for test", "name", alertname, "error", err)
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", alertname), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
@@ -184,7 +184,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
parsedRule,
opts.Reader,
opts.Querier,
opts.Logger,
opts.SLogger,
opts.Cache,
baserules.WithSendAlways(),
baserules.WithSendUnmatched(),
@@ -193,7 +193,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
)
if err != nil {
slog.Error("failed to prepare a new anomaly rule for test", "name", alertname, "error", err)
zap.L().Error("failed to prepare a new anomaly rule for test", zap.String("name", alertname), zap.Error(err))
return 0, basemodel.BadRequest(err)
}
} else {
@@ -205,7 +205,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
alertsFound, err := rule.Eval(ctx, ts)
if err != nil {
slog.Error("evaluating rule failed", "rule", rule.Name(), "error", err)
zap.L().Error("evaluating rule failed", zap.String("rule", rule.Name()), zap.Error(err))
return 0, basemodel.InternalError(fmt.Errorf("rule evaluation failed"))
}
rule.SendAlerts(ctx, ts, 0, time.Minute, opts.NotifyFunc)

View File

@@ -80,21 +80,6 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
alertDataRows := cmock.NewRows(cols, tc.Values)
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
evalTime := time.Now().UTC()

View File

@@ -8,12 +8,12 @@ import (
"sync/atomic"
"time"
"log/slog"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/go-co-op/gocron"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/licensing"
"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) {
organizations, err := lm.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
slog.ErrorContext(ctx, "failed to get organizations", "error", err)
zap.L().Error("failed to get organizations", zap.Error(err))
return
}
for _, organization := range organizations {
// check if license is present or not
license, err := lm.licenseService.GetActive(ctx, organization.ID)
if err != nil {
slog.ErrorContext(ctx, "failed to get active license", "error", err)
zap.L().Error("failed to get active license", zap.Error(err))
return
}
if license == nil {
// we will not start the usage reporting if license is not present.
slog.InfoContext(ctx, "no license present, skipping usage reporting")
zap.L().Info("no license present, skipping usage reporting")
return
}
@@ -115,7 +115,7 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
dbusages := []model.UsageDB{}
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") {
slog.ErrorContext(ctx, "failed to get usage from clickhouse", "error", err)
zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err))
return
}
for _, u := range dbusages {
@@ -125,24 +125,24 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
}
if len(usages) <= 0 {
slog.InfoContext(ctx, "no snapshots to upload, skipping")
zap.L().Info("no snapshots to upload, skipping.")
return
}
slog.InfoContext(ctx, "uploading usage data")
zap.L().Info("uploading usage data")
usagesPayload := []model.Usage{}
for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
if err != nil {
slog.ErrorContext(ctx, "error while decrypting usage data", "error", err)
zap.L().Error("error while decrypting usage data: %v", zap.Error(err))
return
}
usageData := model.Usage{}
err = json.Unmarshal(usageDataBytes, &usageData)
if err != nil {
slog.ErrorContext(ctx, "error while unmarshalling usage data", "error", err)
zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err))
return
}
@@ -163,13 +163,13 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
body, errv2 := json.Marshal(payload)
if errv2 != nil {
slog.ErrorContext(ctx, "error while marshalling usage payload", "error", errv2)
zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2))
return
}
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
if errv2 != nil {
slog.ErrorContext(ctx, "failed to upload usage", "error", errv2)
zap.L().Error("failed to upload usage: %v", zap.Error(errv2))
// not returning error here since it is captured in the failed count
return
}
@@ -179,7 +179,7 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
func (lm *Manager) Stop(ctx context.Context) {
lm.scheduler.Stop()
slog.InfoContext(ctx, "sending usage data before shutting down")
zap.L().Info("sending usage data before shutting down")
// send usage before shutting down
lm.UploadUsage(ctx)
atomic.StoreUint32(&locker, stateUnlocked)

View File

@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
@@ -33,9 +32,9 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
fmter: fmter,
settings: settings,
operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
SCreateAndDropConstraint: true,
SAlterTableAddAndDropColumnIfNotExistsAndExists: true,
SAlterTableAlterColumnSetAndDrop: true,
DropConstraint: true,
ColumnIfNotExistsExists: true,
AlterColumnSetNotNull: true,
}),
}, nil
}
@@ -73,9 +72,8 @@ WHERE
if err != nil {
return nil, nil, err
}
if len(columns) == 0 {
return nil, nil, provider.sqlstore.WrapNotFoundErrf(sql.ErrNoRows, errors.CodeNotFound, "table (%s) not found", tableName)
return nil, nil, sql.ErrNoRows
}
sqlschemaColumns := make([]*sqlschema.Column, 0)
@@ -222,9 +220,7 @@ SELECT
ci.relname AS index_name,
i.indisunique AS unique,
i.indisprimary AS primary,
a.attname AS column_name,
array_position(i.indkey, a.attnum) AS column_position,
pg_get_expr(i.indpred, i.indrelid) AS predicate
a.attname AS column_name
FROM
pg_index i
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
@@ -235,10 +231,9 @@ WHERE
a.attnum = ANY(i.indkey)
AND con.oid IS NULL
AND ct.relkind = 'r'
AND ct.relname = ?
ORDER BY index_name, column_position`, string(name))
AND ct.relname = ?`, string(name))
if err != nil {
return nil, provider.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "no indices for table (%s) found", name)
return nil, err
}
defer func() {
@@ -247,12 +242,7 @@ ORDER BY index_name, column_position`, string(name))
}
}()
type indexEntry struct {
columns []sqlschema.ColumnName
predicate *string
}
uniqueIndicesMap := make(map[string]*indexEntry)
uniqueIndicesMap := make(map[string]*sqlschema.UniqueIndex)
for rows.Next() {
var (
tableName string
@@ -260,53 +250,27 @@ ORDER BY index_name, column_position`, string(name))
unique bool
primary bool
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, &columnPosition, &predicate); err != nil {
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName); err != nil {
return nil, err
}
if unique {
if _, ok := uniqueIndicesMap[indexName]; !ok {
uniqueIndicesMap[indexName] = &indexEntry{
columns: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
predicate: predicate,
uniqueIndicesMap[indexName] = &sqlschema.UniqueIndex{
TableName: name,
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
}
} else {
uniqueIndicesMap[indexName].columns = append(uniqueIndicesMap[indexName].columns, sqlschema.ColumnName(columnName))
uniqueIndicesMap[indexName].ColumnNames = append(uniqueIndicesMap[indexName].ColumnNames, sqlschema.ColumnName(columnName))
}
}
}
indices := make([]sqlschema.Index, 0)
for indexName, entry := range uniqueIndicesMap {
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))
}
}
for _, index := range uniqueIndicesMap {
indices = append(indices, index)
}
return indices, nil

View File

@@ -1,29 +0,0 @@
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} />;
}

View File

@@ -14,7 +14,6 @@ const config: Config.InitialOptions = {
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
@@ -24,8 +23,7 @@ const config: Config.InitialOptions = {
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
'^@signozhq/(?!ui$)([^/]+)$':
'<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
'^@signozhq/([^/]+)$': '<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
},
extensionsToTreatAsEsm: ['.ts'],
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],

View File

@@ -11,7 +11,6 @@
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:generated": "eslint ./src/api/generated --fix",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
@@ -65,9 +64,8 @@
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/toggle-group": "0.0.1",
"@signozhq/toggle-group": "^0.0.1",
"@signozhq/tooltip": "0.0.2",
"@signozhq/ui": "0.0.4",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
@@ -285,4 +283,4 @@
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}
}

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 214 KiB

View File

@@ -25,7 +25,7 @@ echo "\n✅ Prettier formatting successful"
# Fix linting issues
echo "\n\n---\nRunning eslint...\n"
if ! yarn lint:generated; then
if ! yarn lint --fix --quiet src/api/generated; then
echo "ESLint check failed! Please fix linting errors before proceeding."
exit 1
fi

View File

@@ -21,8 +21,6 @@ import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
AuthtypesPatchableObjectsDTO,
AuthtypesPatchableRoleDTO,
AuthtypesPostableRoleDTO,
CreateRole201,
DeleteRolePathParameters,
GetObjects200,
@@ -33,6 +31,8 @@ import type {
PatchObjectsPathParameters,
PatchRolePathParameters,
RenderErrorResponseDTO,
RoletypesPatchableRoleDTO,
RoletypesPostableRoleDTO,
} from '../sigNoz.schemas';
/**
@@ -118,14 +118,14 @@ export const invalidateListRoles = async (
* @summary Create role
*/
export const createRole = (
authtypesPostableRoleDTO: BodyType<AuthtypesPostableRoleDTO>,
roletypesPostableRoleDTO: BodyType<RoletypesPostableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRole201>({
url: `/api/v1/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: authtypesPostableRoleDTO,
data: roletypesPostableRoleDTO,
signal,
});
};
@@ -137,13 +137,13 @@ export const getCreateRoleMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<AuthtypesPostableRoleDTO> },
{ data: BodyType<RoletypesPostableRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<AuthtypesPostableRoleDTO> },
{ data: BodyType<RoletypesPostableRoleDTO> },
TContext
> => {
const mutationKey = ['createRole'];
@@ -157,7 +157,7 @@ export const getCreateRoleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRole>>,
{ data: BodyType<AuthtypesPostableRoleDTO> }
{ data: BodyType<RoletypesPostableRoleDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -170,7 +170,7 @@ export const getCreateRoleMutationOptions = <
export type CreateRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createRole>>
>;
export type CreateRoleMutationBody = BodyType<AuthtypesPostableRoleDTO>;
export type CreateRoleMutationBody = BodyType<RoletypesPostableRoleDTO>;
export type CreateRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -183,13 +183,13 @@ export const useCreateRole = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<AuthtypesPostableRoleDTO> },
{ data: BodyType<RoletypesPostableRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<AuthtypesPostableRoleDTO> },
{ data: BodyType<RoletypesPostableRoleDTO> },
TContext
> => {
const mutationOptions = getCreateRoleMutationOptions(options);
@@ -370,13 +370,13 @@ export const invalidateGetRole = async (
*/
export const patchRole = (
{ id }: PatchRolePathParameters,
authtypesPatchableRoleDTO: BodyType<AuthtypesPatchableRoleDTO>,
roletypesPatchableRoleDTO: BodyType<RoletypesPatchableRoleDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: authtypesPatchableRoleDTO,
data: roletypesPatchableRoleDTO,
});
};
@@ -389,7 +389,7 @@ export const getPatchRoleMutationOptions = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
},
TContext
>;
@@ -398,7 +398,7 @@ export const getPatchRoleMutationOptions = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
},
TContext
> => {
@@ -415,7 +415,7 @@ export const getPatchRoleMutationOptions = <
Awaited<ReturnType<typeof patchRole>>,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -429,7 +429,7 @@ export const getPatchRoleMutationOptions = <
export type PatchRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof patchRole>>
>;
export type PatchRoleMutationBody = BodyType<AuthtypesPatchableRoleDTO>;
export type PatchRoleMutationBody = BodyType<RoletypesPatchableRoleDTO>;
export type PatchRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -444,7 +444,7 @@ export const usePatchRole = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
},
TContext
>;
@@ -453,7 +453,7 @@ export const usePatchRole = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
},
TContext
> => {

View File

@@ -278,13 +278,6 @@ export interface AuthtypesPatchableObjectsDTO {
deletions: AuthtypesGettableObjectsDTO[] | null;
}
export interface AuthtypesPatchableRoleDTO {
/**
* @type string
*/
description: string;
}
export interface AuthtypesPostableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
/**
@@ -308,17 +301,6 @@ export interface AuthtypesPostableEmailPasswordSessionDTO {
password?: string;
}
export interface AuthtypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
}
export interface AuthtypesPostableRotateTokenDTO {
/**
* @type string
@@ -337,39 +319,6 @@ export interface AuthtypesResourceDTO {
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
*/
@@ -2090,6 +2039,57 @@ export interface RenderErrorResponseDTO {
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 {
/**
* @type string
@@ -3163,7 +3163,7 @@ export type ListRoles200 = {
/**
* @type array
*/
data: AuthtypesRoleDTO[];
data: RoletypesRoleDTO[];
/**
* @type string
*/
@@ -3185,7 +3185,7 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data: AuthtypesRoleDTO;
data: RoletypesRoleDTO;
/**
* @type string
*/

View File

@@ -30,4 +30,3 @@ import '@signozhq/switch';
import '@signozhq/table';
import '@signozhq/toggle-group';
import '@signozhq/tooltip';
import '@signozhq/ui';

View File

@@ -1,31 +1,6 @@
.custom-time-picker {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
.zoom-out-btn {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--foreground);
border: 1px solid var(--border);
border-radius: 2px;
box-shadow: none;
padding: 10px;
height: 33px;
&:hover:not(:disabled) {
color: var(--bg-vanilla-100);
background: var(--primary);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
flex-direction: column;
.timeSelection-input {
&:hover {

View File

@@ -16,15 +16,6 @@ jest.mock('react-router-dom', () => {
};
});
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn(() => jest.fn()),
useSelector: jest.fn(() => ({
minTime: 0,
maxTime: Date.now(),
})),
}));
jest.mock('providers/Timezone', () => {
const actual = jest.requireActual('providers/Timezone');

View File

@@ -7,11 +7,9 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/button';
import { Input, InputRef, Popover, Tooltip } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
FixedDurationSuggestionOptions,
@@ -19,11 +17,9 @@ import {
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/constants';
import dayjs from 'dayjs';
import { useZoomOut } from 'hooks/useZoomOut';
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
import { isZoomOutDisabled } from 'lib/zoomOutUtils';
import { defaultTo, isFunction, noop } from 'lodash-es';
import { ChevronDown, ChevronUp, ZoomOut } from 'lucide-react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { getTimeDifference, validateEpochRange } from 'utils/epochUtils';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -70,8 +66,6 @@ interface CustomTimePickerProps {
showRecentlyUsed?: boolean;
minTime: number;
maxTime: number;
/** When true, zoom-out button is hidden (e.g. in drawer/modal time selection) */
isModalTimeSelection?: boolean;
}
function CustomTimePicker({
@@ -94,7 +88,6 @@ function CustomTimePicker({
showRecentlyUsed = true,
minTime,
maxTime,
isModalTimeSelection = false,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
@@ -123,14 +116,6 @@ function CustomTimePicker({
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
const durationMs = (maxTime - minTime) / 1e6;
const zoomOutDisabled = showLiveLogs || isZoomOutDisabled(durationMs);
const handleZoomOut = useZoomOut({
isDisabled: zoomOutDisabled,
urlParamsToDelete: [QueryParams.activeLogId],
});
// function to get selected time in Last 1m, Last 2h, Last 3d, Last 4w format
// 1m, 2h, 3d, 4w -> Last 1 minute, Last 2 hours, Last 3 days, Last 4 weeks
const getSelectedTimeRangeLabelInRelativeFormat = (
@@ -297,11 +282,7 @@ function CustomTimePicker({
resetErrorStatus();
};
const handleInputPressEnter = (
event?: React.KeyboardEvent<HTMLInputElement>,
): void => {
event?.preventDefault();
event?.stopPropagation();
const handleInputPressEnter = (): void => {
// check if the entered time is in the format of 1m, 2h, 3d, 4w
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
@@ -650,23 +631,6 @@ function CustomTimePicker({
/>
</Popover>
</Tooltip>
{!showLiveLogs && !isModalTimeSelection && (
<Tooltip
title={
zoomOutDisabled ? 'Zoom out time range is limited to 1 month' : 'Zoom out'
}
>
<span>
<Button
className="zoom-out-btn"
onClick={handleZoomOut}
disabled={zoomOutDisabled}
data-testid="zoom-out-btn"
prefixIcon={<ZoomOut size={14} />}
/>
</span>
</Tooltip>
)}
</div>
);
}

View File

@@ -1,169 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryParams } from 'constants/query';
import { GlobalReducer } from 'types/reducer/globalTime';
import CustomTimePicker from '../CustomTimePicker';
const MS_PER_MIN = 60 * 1000;
const NOW_MS = 1705312800000;
const mockDispatch = jest.fn();
const mockSafeNavigate = jest.fn();
const mockUrlQueryDelete = jest.fn();
const mockUrlQuerySet = jest.fn();
interface MockAppState {
globalTime: Pick<GlobalReducer, 'minTime' | 'maxTime'>;
}
jest.mock('react-redux', () => ({
useDispatch: (): jest.Mock => mockDispatch,
useSelector: (selector: (state: MockAppState) => unknown): unknown => {
const mockState: MockAppState = {
globalTime: {
minTime: (NOW_MS - 15 * MS_PER_MIN) * 1e6,
maxTime: NOW_MS * 1e6,
},
};
return selector(mockState);
},
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
interface MockUrlQuery {
delete: typeof mockUrlQueryDelete;
set: typeof mockUrlQuerySet;
get: () => null;
toString: () => string;
}
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: (): MockUrlQuery => ({
delete: mockUrlQueryDelete,
set: mockUrlQuerySet,
get: (): null => null,
toString: (): string => 'relativeTime=45m',
}),
}));
jest.mock('providers/Timezone', () => ({
useTimezone: (): { timezone: { value: string; offset: string } } => ({
timezone: { value: 'UTC', offset: 'UTC' },
}),
}));
jest.mock('react-router-dom', () => ({
useLocation: (): { pathname: string } => ({ pathname: '/logs-explorer' }),
}));
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const now = Date.now();
const defaultProps = {
onSelect: jest.fn(),
onError: jest.fn(),
selectedValue: '15m',
selectedTime: '15m',
onValidCustomDateChange: jest.fn(),
open: false,
setOpen: jest.fn(),
items: [
{ value: '15m', label: 'Last 15 minutes' },
{ value: '1h', label: 'Last 1 hour' },
],
minTime: (now - 15 * 60 * 1000) * 1e6,
maxTime: now * 1e6,
};
describe('CustomTimePicker - zoom out button', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render zoom out button when showLiveLogs is false', () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={false} />);
expect(screen.getByTestId('zoom-out-btn')).toBeInTheDocument();
});
it('should not render zoom out button when showLiveLogs is true', () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={true} />);
expect(screen.queryByTestId('zoom-out-btn')).not.toBeInTheDocument();
});
it('should not render zoom out button when isModalTimeSelection is true', () => {
render(
<CustomTimePicker
{...defaultProps}
showLiveLogs={false}
isModalTimeSelection={true}
/>,
);
expect(screen.queryByTestId('zoom-out-btn')).not.toBeInTheDocument();
});
it('should call handleZoomOut when zoom out button is clicked', async () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={false} />);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
await userEvent.click(zoomOutBtn);
expect(mockDispatch).toHaveBeenCalled();
expect(mockUrlQuerySet).toHaveBeenCalledWith(QueryParams.relativeTime, '45m');
expect(mockSafeNavigate).toHaveBeenCalledWith(
expect.stringMatching(/\/logs-explorer\?relativeTime=45m/),
);
});
it('should use real ladder logic: 15m range zooms to 45m preset and updates URL', async () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={false} />);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
await userEvent.click(zoomOutBtn);
expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.startTime);
expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.endTime);
expect(mockUrlQuerySet).toHaveBeenCalledWith(QueryParams.relativeTime, '45m');
expect(mockSafeNavigate).toHaveBeenCalledWith(
expect.stringMatching(/\/logs-explorer\?relativeTime=45m/),
);
expect(mockDispatch).toHaveBeenCalled();
});
it('should delete activeLogId when zoom out is clicked', async () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={false} />);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
await userEvent.click(zoomOutBtn);
expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.activeLogId);
});
it('should disable zoom button when time range is >= 1 month', () => {
const now = Date.now();
render(
<CustomTimePicker
{...defaultProps}
minTime={(now - 31 * MS_PER_DAY) * 1e6}
maxTime={now * 1e6}
showLiveLogs={false}
/>,
);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
expect(zoomOutBtn).toBeDisabled();
});
});

View File

@@ -1,6 +1,5 @@
// ** Helpers
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
@@ -549,49 +548,3 @@ export const DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY: Record<
[DataTypes.ArrayBool]: 'boolAttributeValues',
[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,
},
],
},
};

View File

@@ -1,4 +1,4 @@
.panel-type-selection-modal {
.graph-selection {
.ant-modal-content {
width: 515px;
max-height: 646px;
@@ -76,11 +76,6 @@
content: none;
}
}
.panel-type-text {
text-align: center;
margin-top: 1rem;
}
}
}
@@ -119,7 +114,7 @@
}
.lightMode {
.panel-type-selection-modal {
.graph-selection {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -0,0 +1,50 @@
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,
},
],
},
};

View File

@@ -0,0 +1,94 @@
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;

View File

@@ -9,7 +9,7 @@ import {
Table,
} from 'lucide-react';
export const PanelTypesWithData: ItemsProps[] = [
const Items: ItemsProps[] = [
{
name: PANEL_TYPES.TIME_SERIES,
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
@@ -52,3 +52,5 @@ export interface ItemsProps {
icon: JSX.Element;
display: string;
}
export default Items;

View File

@@ -0,0 +1,41 @@
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;
`;

View File

@@ -182,7 +182,9 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const mockContextValue: IDashboardContext = {
isDashboardSliderOpen: false,
isDashboardLocked: false,
handleToggleDashboardSlider: jest.fn(),
handleDashboardLockToggle: jest.fn(),
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,

View File

@@ -40,7 +40,6 @@ import {
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { sortLayout } from 'providers/Dashboard/util';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
@@ -49,10 +48,10 @@ import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
import DashboardGraphSlider from '../ComponentsSlider';
import DashboardSettings from '../DashboardSettings';
import { Base64Icons } from '../DashboardSettings/General/utils';
import DashboardVariableSelection from '../DashboardVariablesSelection';
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
import SettingsDrawer from './SettingsDrawer';
import { VariablesSettingsTab } from './types';
import {
@@ -70,9 +69,6 @@ interface DashboardDescriptionProps {
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { handle } = props;
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const {
selectedDashboard,
panelMap,
@@ -81,6 +77,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
setLayouts,
isDashboardLocked,
setSelectedDashboard,
handleToggleDashboardSlider,
handleDashboardLockToggle,
} = useDashboard();
@@ -148,14 +145,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setIsPanelTypeSelectionModalOpen(true);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsPanelTypeSelectionModalOpen]);
}, [handleToggleDashboardSlider]);
const handleLockDashboardToggle = (): void => {
setIsDashbordSettingsOpen(false);
@@ -524,7 +521,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DashboardVariableSelection />
</section>
)}
<PanelTypeSelectionModal />
<DashboardGraphSlider />
<Modal
open={isRenameDashboardOpen}

View File

@@ -1,68 +0,0 @@
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);

View File

@@ -224,7 +224,7 @@ describe('TimeSeriesPanel utils', () => {
});
});
it('uses DrawStyle.Line and showPoints false when series has multiple valid points', () => {
it('uses DrawStyle.Line and VisibilityMode.Never when series has multiple valid points', () => {
const apiResponse = createApiResponse([
{
metric: {},

View File

@@ -10,9 +10,9 @@ import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import {
DrawStyle,
FillMode,
LineInterpolation,
LineStyle,
VisibilityMode,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { isInvalidPlotValue } from 'lib/uPlotV2/utils/dataUtils';
@@ -123,13 +123,13 @@ export const prepareUPlotConfig = ({
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
label: label,
colorMapping: widget.customLegendColors ?? {},
spanGaps: widget.spanGaps ?? true,
lineStyle: widget.lineStyle || LineStyle.Solid,
lineInterpolation: widget.lineInterpolation || LineInterpolation.Spline,
showPoints:
widget.showPoints || hasSingleValidPoint ? true : !!widget.showPoints,
spanGaps: true,
lineStyle: LineStyle.Solid,
lineInterpolation: LineInterpolation.Spline,
showPoints: hasSingleValidPoint
? VisibilityMode.Always
: VisibilityMode.Never,
pointSize: 5,
fillMode: widget.fillMode || FillMode.None,
isDarkMode,
});
});

View File

@@ -9,18 +9,17 @@ import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
import useComponentPermission from 'hooks/useComponentPermission';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
import './DashboardEmptyState.styles.scss';
export default function DashboardEmptyState(): JSX.Element {
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const {
selectedDashboard,
isDashboardLocked,
handleToggleDashboardSlider,
} = useDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
@@ -42,14 +41,14 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setIsPanelTypeSelectionModalOpen(true);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsPanelTypeSelectionModalOpen]);
}, [handleToggleDashboardSlider]);
const onConfigureClick = useCallback((): void => {
setIsSettingsDrawerOpen(true);

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { Select, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
import GraphTypes from 'container/DashboardContainer/ComponentsSlider/menuItems';
import { handleQueryChange } from 'container/NewWidget/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -59,7 +59,7 @@ function PanelTypeSelector({
data-testid="panel-change-select"
disabled={disabled}
>
{PanelTypesWithData.map((item) => (
{GraphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="view-panel-select-option">
<div className="icon">{item.icon}</div>

View File

@@ -5,7 +5,6 @@ import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -35,11 +34,11 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
} = props;
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
} = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();
@@ -88,7 +87,7 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
}
setSelectedRowWidgetId(selectedDashboard.id, id);
setIsPanelTypeSelectionModalOpen(true);
handleToggleDashboardSlider(true);
}}
>
New Panel

View File

@@ -15,7 +15,6 @@ import ROUTES from 'constants/routes';
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import cloneDeep from 'lodash-es/cloneDeep';
import { AnimatePresence } from 'motion/react';
@@ -44,7 +43,6 @@ const homeInterval = 30 * 60 * 1000;
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function Home(): JSX.Element {
const { user } = useAppContext();
const isDarkMode = useIsDarkMode();
const [startTime, setStartTime] = useState<number | null>(null);
const [endTime, setEndTime] = useState<number | null>(null);
@@ -682,11 +680,7 @@ export default function Home(): JSX.Element {
<div className="checklist-img-container">
<img
src={
isDarkMode
? '/Images/allInOne.svg'
: '/Images/allInOneLightMode.svg'
}
src="/Images/allInOne.svg"
alt="checklist-img"
className="checklist-img"
/>

View File

@@ -65,35 +65,6 @@
}
}
.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 {
.edit-header {
border-bottom: 1px solid var(--bg-vanilla-300);
@@ -110,11 +81,4 @@
}
}
}
.widget-resizable-panel-group {
.bg-border {
background: var(--bg-vanilla-300);
border-color: var(--bg-vanilla-300);
}
}
}

View File

@@ -1,7 +1,5 @@
.column-unit-selector {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 16px;
.heading {
color: var(--bg-vanilla-400);
@@ -32,11 +30,6 @@
width: 100%;
}
}
&-content {
display: flex;
flex-direction: column;
gap: 12px;
}
}
.lightMode {

View File

@@ -72,24 +72,22 @@ export function ColumnUnitSelector(
return (
<section className="column-unit-selector">
<Typography.Text className="heading">Column Units</Typography.Text>
<div className="column-unit-selector-content">
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
</div>
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
</section>
);
}

View File

@@ -56,6 +56,9 @@ describe('ContextLinks Component', () => {
/>,
);
// Check that the component renders
expect(screen.getByText('Context Links')).toBeInTheDocument();
// Check that the add button is present
expect(
screen.getByRole('button', { name: /context link/i }),

View File

@@ -14,7 +14,7 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal } from 'antd';
import { Button, Modal, Typography } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
import {
@@ -134,16 +134,11 @@ function ContextLinks({
return (
<div className="context-links-container">
<Typography.Text className="context-links-text">
Context Links
</Typography.Text>
<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>
<DndContext
sensors={sensors}
@@ -165,6 +160,16 @@ function ContextLinks({
</SortableContext>
</DndContext>
</OverlayScrollbar>
{/* button to add context link */}
<Button
type="primary"
className="add-context-link-button"
icon={<Plus size={12} />}
onClick={handleAddContextLink}
>
Context Link
</Button>
</div>
<Modal

View File

@@ -2,6 +2,7 @@
display: flex;
flex-direction: column;
gap: 16px;
margin: 12px;
}
.context-links-text {
@@ -109,7 +110,10 @@
}
.add-context-link-button {
width: 100%;
display: flex;
align-items: center;
margin: auto;
width: fit-content;
}
.lightMode {

View File

@@ -1,21 +0,0 @@
.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);
}
}
}

View File

@@ -1,94 +0,0 @@
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>
);
}

View File

@@ -1,21 +0,0 @@
.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);
}
}
}

View File

@@ -1,110 +0,0 @@
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>
);
}

View File

@@ -1,21 +0,0 @@
.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);
}
}
}

View File

@@ -1,66 +0,0 @@
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>
);
}

View File

@@ -1,45 +1,6 @@
.right-container {
display: flex;
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%;
}
.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;
}
.header {
display: flex;
@@ -63,35 +24,86 @@
letter-spacing: -0.07px;
}
}
.control-container {
.name-description {
display: flex;
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;
.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 {
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
gap: 8px;
border-bottom: 1px solid var(--bg-slate-500);
.toggle-card {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
}
.toggle-card-text-container {
display: flex;
flex-direction: column;
gap: 4px;
.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;
}
.panel-type-select {
width: 100%;
.ant-select-selector {
display: flex;
height: 32px;
@@ -103,32 +115,98 @@
border: 1px solid var(--bg-slate-400);
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% */
}
}
}
.toggle-card-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
opacity: 0.6;
line-height: 16px; /* 133.333% */
.fill-gaps {
margin-top: 16px;
display: flex;
padding: 12px;
justify-content: space-between;
align-items: center;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
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,
.decimal-precision-selector,
.legend-position {
.decimal-precision-selector {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.legend-position {
margin-top: 16px;
display: flex;
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-v2 {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.heading {
@extend .section-heading;
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;
}
.input {
@@ -181,6 +259,7 @@
.text {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
@@ -199,8 +278,113 @@
}
.stack-chart {
flex-direction: row;
margin-top: 16px;
display: flex;
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);
}
}
}
@@ -227,16 +411,42 @@
.lightMode {
.right-container {
background-color: var(--bg-vanilla-100);
.section-heading {
color: var(--bg-ink-400);
}
.header {
.header-text {
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 {
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
.panel-type-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
@@ -261,16 +471,28 @@
}
}
.toggle-card {
.fill-gaps {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
.fill-gaps-text {
color: var(--bg-ink-400);
}
.toggle-card-description {
}
.bucket-config {
.label {
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 {
@@ -306,6 +528,27 @@
}
}
}
.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 {

View File

@@ -1,50 +0,0 @@
.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);
}
}
}

View File

@@ -1,23 +0,0 @@
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>
);
}

View File

@@ -1,98 +0,0 @@
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>
);
}

View File

@@ -1,87 +0,0 @@
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 DisconnectValuesSelector from '../../components/DisconnectValuesSelector/DisconnectValuesSelector';
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>>;
spanGaps: boolean | number;
setSpanGaps: Dispatch<SetStateAction<boolean | number>>;
allowFillMode: boolean;
allowLineStyle: boolean;
allowLineInterpolation: boolean;
allowShowPoints: boolean;
allowSpanGaps: boolean;
stepInterval: number;
}
export default function ChartAppearanceSection({
fillMode,
setFillMode,
lineStyle,
setLineStyle,
lineInterpolation,
setLineInterpolation,
showPoints,
setShowPoints,
spanGaps,
setSpanGaps,
allowFillMode,
allowLineStyle,
allowLineInterpolation,
allowShowPoints,
allowSpanGaps,
stepInterval,
}: 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>
)}
{allowSpanGaps && (
<DisconnectValuesSelector
value={spanGaps}
minValue={stepInterval}
onChange={setSpanGaps}
/>
)}
</SettingsSection>
);
}

View File

@@ -1,10 +0,0 @@
.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);
}
}

View File

@@ -1,36 +0,0 @@
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>
);
}

View File

@@ -1,84 +0,0 @@
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>
);
}

View File

@@ -1,64 +0,0 @@
.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);
}
}
}
}

View File

@@ -1,156 +0,0 @@
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>
);
}

View File

@@ -1,55 +0,0 @@
.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);
}
}
}
}

View File

@@ -1,71 +0,0 @@
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>
);
}

View File

@@ -1,72 +0,0 @@
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>
);
}

View File

@@ -1,10 +0,0 @@
.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);
}
}

View File

@@ -1,42 +0,0 @@
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>
);
}

View File

@@ -1,130 +0,0 @@
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>
);
}

View File

@@ -1,4 +1,5 @@
.threshold-selector-container {
padding: 12px;
padding-bottom: 80px;
.threshold-select {

View File

@@ -1,10 +1,10 @@
import { useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Button } from 'antd';
import { Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
import { Plus } from 'lucide-react';
import { Antenna, Plus } from 'lucide-react';
import { v4 as uuid } from 'uuid';
import Threshold from './Threshold';
@@ -68,14 +68,11 @@ function ThresholdSelector({
<DndProvider backend={HTML5Backend}>
<div className="threshold-selector-container">
<div className="threshold-select" onClick={addThresholdHandler}>
<Button
type="default"
icon={<Plus size={14} />}
style={{ width: '100%' }}
onClick={addThresholdHandler}
>
Add Threshold
</Button>
<div className="left-section">
<Antenna size={14} className="icon" />
<Typography.Text className="text">Thresholds</Typography.Text>
</div>
<Plus size={14} onClick={addThresholdHandler} className="icon" />
</div>
{thresholds.map((threshold, idx) => (
<Threshold

View File

@@ -6,11 +6,6 @@ import { MemoryRouter } from 'react-router-dom';
import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
@@ -170,16 +165,6 @@ describe('RightContainer - Alerts Section', () => {
setContextLinks: jest.fn(),
enableDrillDown: 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(),
spanGaps: false,
setSpanGaps: jest.fn(),
};
beforeEach(() => {
@@ -191,7 +176,7 @@ describe('RightContainer - Alerts Section', () => {
const alertsSection = screen.getByText('Alerts').closest('section');
expect(alertsSection).toBeInTheDocument();
expect(alertsSection).toHaveClass('alerts-section');
expect(alertsSection).toHaveClass('alerts');
});
it('renders alerts section with correct text and SquareArrowOutUpRight icon', () => {

View File

@@ -1,44 +0,0 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Typography } from 'antd';
import { DisconnectedValuesMode } from 'lib/uPlotV2/config/types';
interface DisconnectValuesModeToggleProps {
value: DisconnectedValuesMode;
onChange: (value: DisconnectedValuesMode) => void;
}
export default function DisconnectValuesModeToggle({
value,
onChange,
}: DisconnectValuesModeToggleProps): JSX.Element {
return (
<ToggleGroup
type="single"
value={value}
variant="outline"
size="lg"
onValueChange={(newValue): void => {
if (newValue) {
onChange(newValue as DisconnectedValuesMode);
}
}}
>
<ToggleGroupItem
value={DisconnectedValuesMode.Never}
aria-label="Never"
title="Never"
>
<Typography.Text className="section-heading-small">Never</Typography.Text>
</ToggleGroupItem>
<ToggleGroupItem
value={DisconnectedValuesMode.Threshold}
aria-label="Threshold"
title="Threshold"
>
<Typography.Text className="section-heading-small">
Threshold
</Typography.Text>
</ToggleGroupItem>
</ToggleGroup>
);
}

View File

@@ -1,21 +0,0 @@
.disconnect-values-selector {
.disconnect-values-input-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
.disconnect-values-threshold-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
.disconnect-values-threshold-input {
max-width: 160px;
height: auto;
.disconnect-values-threshold-prefix {
padding: 0 8px;
font-size: 20px;
}
}
}
}
}

View File

@@ -1,91 +0,0 @@
import { useEffect, useState } from 'react';
import { Typography } from 'antd';
import { DisconnectedValuesMode } from 'lib/uPlotV2/config/types';
import DisconnectValuesModeToggle from './DisconnectValuesModeToggle';
import DisconnectValuesThresholdInput from './DisconnectValuesThresholdInput';
import './DisconnectValuesSelector.styles.scss';
const DEFAULT_THRESHOLD_SECONDS = 60;
interface DisconnectValuesSelectorProps {
value: boolean | number;
minValue?: number;
onChange: (value: boolean | number) => void;
}
export default function DisconnectValuesSelector({
value,
minValue,
onChange,
}: DisconnectValuesSelectorProps): JSX.Element {
const [mode, setMode] = useState<DisconnectedValuesMode>(() => {
if (typeof value === 'number') {
return DisconnectedValuesMode.Threshold;
}
return DisconnectedValuesMode.Never;
});
const [thresholdSeconds, setThresholdSeconds] = useState<number>(
typeof value === 'number' ? value : minValue ?? DEFAULT_THRESHOLD_SECONDS,
);
useEffect(() => {
if (typeof value === 'boolean') {
setMode(DisconnectedValuesMode.Never);
} else if (typeof value === 'number') {
setMode(DisconnectedValuesMode.Threshold);
setThresholdSeconds(value);
}
}, [value]);
useEffect(() => {
if (minValue !== undefined) {
setThresholdSeconds(minValue);
if (mode === DisconnectedValuesMode.Threshold) {
onChange(minValue);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minValue]);
const handleModeChange = (newMode: DisconnectedValuesMode): void => {
setMode(newMode);
switch (newMode) {
case DisconnectedValuesMode.Never:
onChange(true);
break;
case DisconnectedValuesMode.Threshold:
onChange(thresholdSeconds);
break;
}
};
const handleThresholdChange = (seconds: number): void => {
setThresholdSeconds(seconds);
onChange(seconds);
};
return (
<section className="disconnect-values-selector control-container">
<Typography.Text className="section-heading">
Disconnect values
</Typography.Text>
<div className="disconnect-values-input-wrapper">
<DisconnectValuesModeToggle value={mode} onChange={handleModeChange} />
{mode === DisconnectedValuesMode.Threshold && (
<section className="control-container">
<Typography.Text className="section-heading">
Threshold Value
</Typography.Text>
<DisconnectValuesThresholdInput
value={thresholdSeconds}
minValue={minValue}
onChange={handleThresholdChange}
/>
</section>
)}
</div>
</section>
);
}

View File

@@ -1,87 +0,0 @@
import { useEffect, useState } from 'react';
import { rangeUtil } from '@grafana/data';
import { Callout } from '@signozhq/callout';
import { Input } from '@signozhq/ui';
interface DisconnectValuesThresholdInputProps {
value: number;
onChange: (seconds: number) => void;
minValue?: number;
}
export default function DisconnectValuesThresholdInput({
value,
onChange,
minValue,
}: DisconnectValuesThresholdInputProps): JSX.Element {
const [inputValue, setInputValue] = useState<string>(
rangeUtil.secondsToHms(value),
);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setInputValue(rangeUtil.secondsToHms(value));
setError(null);
}, [value]);
const commitValue = (txt: string): void => {
if (!txt) {
return;
}
try {
let seconds: number;
if (rangeUtil.isValidTimeSpan(txt)) {
seconds = rangeUtil.intervalToSeconds(txt);
} else {
const parsed = Number(txt);
if (Number.isNaN(parsed) || parsed <= 0) {
setError('Enter a valid duration (e.g. 1h, 10m, 1d)');
return;
}
seconds = parsed;
}
if (minValue !== undefined && seconds <= minValue) {
setError(`Threshold should be > ${rangeUtil.secondsToHms(minValue)}`);
return;
}
setError(null);
setInputValue(txt);
onChange(seconds);
} catch {
setError('Invalid threshold value');
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
commitValue(e.currentTarget.value);
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
commitValue(e.currentTarget.value);
};
return (
<div className="disconnect-values-threshold-wrapper">
<Input
name="disconnect-values-threshold"
type="text"
className="disconnect-values-threshold-input"
prefix={<span className="disconnect-values-threshold-prefix">&gt;</span>}
value={inputValue}
onChange={(e): void => {
setInputValue(e.currentTarget.value);
if (error) {
setError(null);
}
}}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
autoFocus={false}
aria-invalid={!!error}
aria-describedby={error ? 'threshold-error' : undefined}
/>
{error && <Callout type="error" size="small" showIcon description={error} />}
</div>
);
}

View File

@@ -1,21 +0,0 @@
.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);
}
}
}

View File

@@ -1,94 +0,0 @@
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 default 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>
);
}

View File

@@ -1,21 +0,0 @@
.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);
}
}
}

View File

@@ -1,110 +0,0 @@
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 default 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>
);
}

View File

@@ -1,21 +0,0 @@
.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);
}
}
}

View File

@@ -1,66 +0,0 @@
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 default 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>
);
}

View File

@@ -1,68 +0,0 @@
.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);
}
}

View File

@@ -1,51 +0,0 @@
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;

View File

@@ -206,73 +206,3 @@ export const panelTypeVsDecimalPrecision: {
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} 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;
export const panelTypeVsSpanGaps: {
[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;

View File

@@ -1,18 +1,39 @@
import { Dispatch, SetStateAction, useMemo } from 'react';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { UseQueryResult } from 'react-query';
import { Typography } from 'antd';
import { ExecStats } from 'api/v5/v5';
import type { InputRef } from 'antd';
import {
AutoComplete,
Input,
InputNumber,
Select,
Space,
Switch,
Typography,
} from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
import GraphTypes, {
ItemsProps,
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import get from 'lodash-es/get';
ConciergeBell,
LineChart,
Plus,
Spline,
SquareArrowOutUpRight,
} from 'lucide-react';
import { SuccessResponse } from 'types/api';
import {
ColumnUnit,
@@ -21,58 +42,55 @@ import {
Widgets,
} from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataSource } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
import {
panelTypeVsBucketConfig,
panelTypeVsColumnUnitPreferences,
panelTypeVsContextLinks,
panelTypeVsCreateAlert,
panelTypeVsDecimalPrecision,
panelTypeVsFillMode,
panelTypeVsFillSpan,
panelTypeVsLegendColors,
panelTypeVsLegendPosition,
panelTypeVsLineInterpolation,
panelTypeVsLineStyle,
panelTypeVsLogScale,
panelTypeVsPanelTimePreferences,
panelTypeVsShowPoints,
panelTypeVsSoftMinMax,
panelTypeVsSpanGaps,
panelTypeVsStackingChartPreferences,
panelTypeVsThreshold,
panelTypeVsYAxisUnit,
} from './constants';
import AlertsSection from './SettingSections/AlertsSection/AlertsSection';
import AxesSection from './SettingSections/AxesSection/AxesSection';
import ChartAppearanceSection from './SettingSections/ChartAppearanceSection/ChartAppearanceSection';
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 ContextLinks from './ContextLinks';
import DashboardYAxisUnitSelectorWrapper from './DashboardYAxisUnitSelectorWrapper';
import LegendColors from './LegendColors/LegendColors';
import ThresholdSelector from './Threshold/ThresholdSelector';
import { ThresholdProps } from './Threshold/types';
import { timePreferance } from './timeItems';
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({
description,
setDescription,
setTitle,
title,
selectedGraph,
lineInterpolation,
setLineInterpolation,
fillMode,
setFillMode,
lineStyle,
setLineStyle,
showPoints,
setShowPoints,
spanGaps,
setSpanGaps,
bucketCount,
bucketWidth,
stackedBarChart,
@@ -112,10 +130,20 @@ function RightContainer({
isNewDashboard,
}: RightContainerProps): JSX.Element {
const { dashboardVariables } = useDashboardVariables();
const [inputValue, setInputValue] = useState(title);
const [autoCompleteOpen, setAutoCompleteOpen] = useState(false);
const [cursorPos, setCursorPos] = useState(0);
const inputRef = useRef<InputRef>(null);
const selectedPanelDisplay = PanelTypesWithData.find(
(e) => e.name === selectedGraph,
)?.display as PanelDisplay;
const onChangeHandler = useCallback(
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
setFunc(value);
},
[],
);
const selectedGraphType =
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
@@ -139,194 +167,399 @@ function RightContainer({
panelTypeVsContextLinks[selectedGraph] && enableDrillDown;
const allowDecimalPrecision = panelTypeVsDecimalPrecision[selectedGraph];
const allowLineInterpolation = panelTypeVsLineInterpolation[selectedGraph];
const allowLineStyle = panelTypeVsLineStyle[selectedGraph];
const allowFillMode = panelTypeVsFillMode[selectedGraph];
const allowShowPoints = panelTypeVsShowPoints[selectedGraph];
const allowSpanGaps = panelTypeVsSpanGaps[selectedGraph];
const { currentQuery } = useQueryBuilder();
const decimapPrecisionOptions = useMemo(
() => [
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
],
[],
);
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
allowSoftMinMax,
allowLogScale,
]);
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
return Object.entries(dashboardVariables).map(([, value]) => ({
value: value.name || '',
label: value.name || '',
}));
}, [dashboardVariables]);
const isFormattingSectionVisible = useMemo(
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
);
const updateCursorAndDropdown = (value: string, pos: number): void => {
setCursorPos(pos);
const lastDollar = value.lastIndexOf('$', pos - 1);
setAutoCompleteOpen(lastDollar !== -1 && pos >= lastDollar + 1);
};
const isLegendSectionVisible = useMemo(
() => allowLegendPosition || allowLegendColors,
[allowLegendPosition, allowLegendColors],
);
const onInputChange = (value: string): void => {
setInputValue(value);
onChangeHandler(setTitle, value);
setTimeout(() => {
const pos = inputRef.current?.input?.selectionStart ?? 0;
updateCursorAndDropdown(value, pos);
}, 0);
};
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
*/
const handleInputCursor = (): void => {
const pos = inputRef.current?.input?.selectionStart ?? 0;
updateCursorAndDropdown(inputValue, pos);
};
// eslint-disable-next-line sonarjs/no-redundant-boolean
false &&
(allowFillMode ||
allowLineStyle ||
allowLineInterpolation ||
allowShowPoints ||
allowSpanGaps),
[
allowFillMode,
allowLineStyle,
allowLineInterpolation,
allowShowPoints,
allowSpanGaps,
],
);
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 stepInterval = useMemo(() => {
const stepIntervals: ExecStats['stepIntervals'] = get(
queryResponse,
'data.payload.data.newResult.meta.stepIntervals',
{},
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,
);
return Math.min(...Object.values(stepIntervals));
}, [queryResponse]);
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(
(value: number | null) => {
setSoftMax(value);
},
[setSoftMax],
);
return (
<div className="right-container">
<section className="header">
<div className="purple-dot" />
<Typography.Text className="header-text">Panel Settings</Typography.Text>
<Typography.Text className="header-text">Panel details</Typography.Text>
</section>
<GeneralSettingsSection
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
dashboardVariables={dashboardVariables}
/>
<section className="panel-config">
<VisualizationSettingsSection
selectedGraph={selectedGraph}
setGraphHandler={setGraphHandler}
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
allowPanelTimePreference={allowPanelTimePreference}
allowStackingBarChart={allowStackingBarChart}
allowFillSpans={allowFillSpans}
<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 className="panel-config">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
onChange={setGraphHandler}
value={selectedGraph}
style={{ width: '100%' }}
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>
{isFormattingSectionVisible && (
<FormattingUnitsSection
selectedPanelDisplay={selectedPanelDisplay}
yAxisUnit={yAxisUnit}
setYAxisUnit={setYAxisUnit}
isNewDashboard={isNewDashboard}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
{allowFillSpans && (
<Space className="fill-gaps">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
/>
</Space>
)}
{allowPanelTimePreference && (
<>
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
allowYAxisUnit={allowYAxisUnit}
allowDecimalPrecision={allowDecimalPrecision}
allowPanelColumnPreference={allowPanelColumnPreference}
decimapPrecisionOptions={decimapPrecisionOptions}
isNewDashboard={isNewDashboard}
/>
)}
{isChartAppearanceSectionVisible && (
<ChartAppearanceSection
fillMode={fillMode}
setFillMode={setFillMode}
lineStyle={lineStyle}
setLineStyle={setLineStyle}
lineInterpolation={lineInterpolation}
setLineInterpolation={setLineInterpolation}
showPoints={showPoints}
setShowPoints={setShowPoints}
spanGaps={spanGaps}
setSpanGaps={setSpanGaps}
allowFillMode={allowFillMode}
allowLineStyle={allowLineStyle}
allowLineInterpolation={allowLineInterpolation}
allowShowPoints={allowShowPoints}
allowSpanGaps={allowSpanGaps}
stepInterval={stepInterval}
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
)}
{isAxisSectionVisible && (
<AxesSection
allowSoftMinMax={allowSoftMinMax}
allowLogScale={allowLogScale}
softMin={softMin}
softMax={softMax}
setSoftMin={setSoftMin}
setSoftMax={setSoftMax}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
/>
{allowDecimalPrecision && (
<section className="decimal-precision-selector">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={[
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ 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>
)}
{isLegendSectionVisible && (
<LegendSection
allowLegendPosition={allowLegendPosition}
allowLegendColors={allowLegendColors}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
{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>
)}
{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 && (
<HistogramBucketsSection
bucketCount={bucketCount}
setBucketCount={setBucketCount}
bucketWidth={bucketWidth}
setBucketWidth={setBucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
/>
<section className="bucket-config">
<Typography.Text className="label">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="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>
{allowCreateAlerts && (
<AlertsSection onCreateAlertsHandler={onCreateAlertsHandler} />
<section className="alerts" onClick={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 && (
<ContextLinksSection
contextLinks={contextLinks}
setContextLinks={setContextLinks}
selectedWidget={selectedWidget}
/>
<section className="context-links">
<ContextLinks
contextLinks={contextLinks}
setContextLinks={setContextLinks}
selectedWidget={selectedWidget}
/>
</section>
)}
{allowThreshold && (
<ThresholdsSection
thresholds={thresholds}
setThresholds={setThresholds}
yAxisUnit={yAxisUnit}
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
<section>
<ThresholdSelector
thresholds={thresholds}
setThresholds={setThresholds}
yAxisUnit={yAxisUnit}
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
</section>
)}
</div>
);
@@ -382,16 +615,6 @@ export interface RightContainerProps {
setContextLinks: Dispatch<SetStateAction<ContextLinksData>>;
enableDrillDown?: 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>>;
spanGaps: boolean | number;
setSpanGaps: Dispatch<SetStateAction<boolean | number>>;
}
RightContainer.defaultProps = {

View File

@@ -36,7 +36,7 @@ const checkStackSeriesState = (
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
const stackSeriesSection = container.querySelector(
'.stack-chart',
'section > .stack-chart',
) as HTMLElement;
expect(stackSeriesSection).toBeInTheDocument();
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
expect(getByText('Stack series')).toBeInTheDocument();
// Verify section exists
const section = container.querySelector('.stack-chart');
const section = container.querySelector('section > .stack-chart');
expect(section).toBeInTheDocument();
// Verify switch is present and enabled (ant-switch-checked)

Some files were not shown because too many files have changed in this diff Show More