Merge pull request #6300 from SigNoz/release/signoz-0.57.0

Release/signoz 0.57.0
This commit is contained in:
Prashant Shahi 2024-10-29 00:45:14 +05:30 committed by GitHub
commit b10c22223b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
204 changed files with 9778 additions and 3720 deletions

View File

@ -31,6 +31,7 @@ jobs:
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
KAFKA_SPAN_EVAL: true
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"

View File

@ -79,7 +79,7 @@ build-query-service-static:
@if [ $(DEV_BUILD) != "" ]; then \
cd $(QUERY_SERVICE_DIRECTORY) && \
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
-ldflags "-linkmode external -extldflags '-static' -s -w ${LD_FLAGS} ${DEV_LD_FLAGS}"; \
else \
cd $(QUERY_SERVICE_DIRECTORY) && \
CGO_ENABLED=1 go build -tags timetzdata -a -o ./bin/query-service-${GOOS}-${GOARCH} \
@ -188,13 +188,4 @@ check-no-ee-references:
fi
test:
go test ./pkg/query-service/app/metrics/...
go test ./pkg/query-service/cache/...
go test ./pkg/query-service/app/...
go test ./pkg/query-service/app/querier/...
go test ./pkg/query-service/converter/...
go test ./pkg/query-service/formatter/...
go test ./pkg/query-service/tests/integration/...
go test ./pkg/query-service/rules/...
go test ./pkg/query-service/collectorsimulator/...
go test ./pkg/query-service/postprocess/...
go test ./pkg/query-service/...

200
README.md
View File

@ -1,8 +1,11 @@
<p align="center">
<img src="https://res.cloudinary.com/dcv3epinx/image/upload/v1618904450/signoz-images/LogoGithub_sigfbu.svg" alt="SigNoz-logo" width="240" />
<h1 align="center" style="border-bottom: none">
<a href="https://signoz.io" target="_blank">
<img alt="SigNoz" src="https://github.com/user-attachments/assets/ef9a33f7-12d7-4c94-8908-0a02b22f0c18" width="100" height="100">
</a>
<br>SigNoz
</h1>
<p align="center">Monitor your applications and troubleshoot problems in your deployed applications, an open-source alternative to DataDog, New Relic, etc.</p>
</p>
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
<p align="center">
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker Downloads"> </a>
@ -21,55 +24,115 @@
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
</h3>
##
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. With SigNoz, you can:
👉 Visualise Metrics, Traces and Logs in a single pane of glass
👉 You can see metrics like p99 latency, error rates for your services, external API calls and individual end points.
👉 You can find the root cause of the problem by going to the exact traces which are causing the problem and see detailed flamegraphs of individual request traces.
👉 Run aggregates on trace data to get business relevant metrics
👉 Filter and query logs, build dashboards and alerts based on attributes in logs
👉 Record exceptions automatically in Python, Java, Ruby, and Javascript
👉 Easy to set alerts with DIY query builder
## Features
### Application Metrics
### Application Performance Monitoring
![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png)
Use SigNoz APM to monitor your applications and services. It comes with out-of-box charts for key application metrics like p99 latency, error rate, Apdex and operations per second. You can also monitor the database and external calls made from your application. Read [more](https://signoz.io/application-performance-monitoring/).
You can [instrument](https://signoz.io/docs/instrumentation/) your application with OpenTelemetry to get started.
### Distributed Tracing
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
![apm-cover](https://github.com/user-attachments/assets/fa5c0396-0854-4c8b-b972-9b62fd2a70d2)
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
### Logs Management
<img width="2068" alt="logs_management" src="https://user-images.githubusercontent.com/83692067/226536482-b8a5c4af-b69c-43d5-969c-338bd5eaf1a5.png">
SigNoz can be used as a centralized log management solution. We use ClickHouse (used by likes of Uber & Cloudflare) as a datastore, ⎯ an extremely fast and highly optimized storage for logs data. Instantly search through all your logs using quick filters and a powerful query builder.
### Infrastructure Monitoring
You can also create charts on your logs and monitor them with customized dashboards. Read [more](https://signoz.io/log-management/).
<img width="2068" alt="infrastructure_monitoring" src="https://user-images.githubusercontent.com/83692067/226536496-f38c4dbf-e03c-4158-8be0-32d4a61158c7.png">
![logs-management-cover](https://github.com/user-attachments/assets/343588ee-98fb-4310-b3d2-c5bacf9c7384)
### Exceptions Monitoring
![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.png)
### Distributed Tracing
Distributed Tracing is essential to troubleshoot issues in microservices applications. Powered by OpenTelemetry, distributed tracing in SigNoz can help you track user requests across services to help you identify performance bottlenecks.
See user requests in a detailed breakdown with the help of Flamegraphs and Gantt Charts. Click on any span to see the entire trace represented beautifully, which will help you make sense of where issues actually occurred in the flow of requests.
Read [more](https://signoz.io/distributed-tracing/).
![distributed-tracing-cover](https://github.com/user-attachments/assets/9bfe060a-0c40-4922-9b55-8a97e1a4076c)
### Metrics and Dashboards
Ingest metrics from your infrastructure or applications and create customized dashboards to monitor them. Create visualization that suits your needs with a variety of panel types like pie chart, time-series, bar chart, etc.
Create queries on your metrics data quickly with an easy-to-use metrics query builder. Add multiple queries and combine those queries with formulae to create really complex queries quickly.
Read [more](https://signoz.io/metrics-and-dashboards/).
![metrics-n-dashboards-cover](https://github.com/user-attachments/assets/a536fd71-1d2c-4681-aa7e-516d754c47a5)
### Alerts
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
Use alerts in SigNoz to get notified when anything unusual happens in your application. You can set alerts on any type of telemetry signal (logs, metrics, traces), create thresholds and set up a notification channel to get notified. Advanced features like alert history and anomaly detection can help you create smarter alerts.
Alerts in SigNoz help you identify issues proactively so that you can address them before they reach your customers.
Read [more](https://signoz.io/alerts-management/).
![alerts-cover](https://github.com/user-attachments/assets/03873bb8-1b62-4adf-8f56-28bb7b1750ea)
### Exceptions Monitoring
Monitor exceptions automatically in Python, Java, Ruby, and Javascript. For other languages, just drop in a few lines of code and start monitoring exceptions.
See the detailed stack trace for all exceptions caught in your application. You can also log in custom attributes to add more context to your exceptions. For example, you can add attributes to identify users for which exceptions occurred.
Read [more](https://signoz.io/exceptions-monitoring/).
![exceptions-cover](https://github.com/user-attachments/assets/4be37864-59f2-4e8a-8d6e-e29ad04298c5)
<br /><br />
## Why SigNoz?
SigNoz is a single tool for all your monitoring and observability needs. Here are a few reasons why you should choose SigNoz:
- Single tool for observability(logs, metrics, and traces)
- Built on top of [OpenTelemetry](https://opentelemetry.io/), the open-source standard which frees you from any type of vendor lock-in
- Correlated logs, metrics and traces for much richer context while debugging
- Uses ClickHouse (used by likes of Uber & Cloudflare) as datastore - an extremely fast and highly optimized storage for observability data
- DIY Query builder, PromQL, and ClickHouse queries to fulfill all your use-cases around querying observability data
- Open-Source - you can use open-source, our [cloud service](https://signoz.io/teams/) or a mix of both based on your use case
## Getting Started
### Create a SigNoz Cloud Account
SigNoz cloud is the easiest way to get started with SigNoz. Our cloud service is for those users who want to spend more time in getting insights for their application performance without worrying about maintenance.
[Get started for free](https://signoz.io/teams/)
### Deploy using Docker(self-hosted)
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
<p>&nbsp </p>
### Deploy in Kubernetes using Helm(self-hosted)
Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_chart) to install using helm charts
<br /><br />
We also offer managed services in your infra. Check our [pricing plans](https://signoz.io/pricing/) for all details.
## Join our Slack community
@ -78,64 +141,22 @@ Come say Hi to us on [Slack](https://signoz.io/slack) 👋
<br /><br />
## Features:
- Unified UI for metrics, traces and logs. No need to switch from Prometheus to Jaeger to debug issues, or use a logs tool like Elastic separate from your metrics and traces stack.
- Application overview metrics like RPS, 50th/90th/99th Percentile latencies, and Error Rate
- Slowest endpoints in your application
- See exact request trace to figure out issues in downstream services, slow DB queries, call to 3rd party services like payment gateways, etc
- Filter traces by service name, operation, latency, error, tags/annotations.
- Run aggregates on trace data (events/spans) to get business relevant metrics. e.g. You can get error rate and 99th percentile latency of `customer_type: gold` or `deployment_version: v2` or `external_call: paypal`
- Native support for OpenTelemetry Logs, advanced log query builder, and automatic log collection from k8s cluster
- Lightning quick log analytics ([Logs Perf. Benchmark](https://signoz.io/blog/logs-performance-benchmark/))
- End-to-End visibility into infrastructure performance, ingest metrics from all kinds of host environments
- Easy to set alerts with DIY query builder
<br /><br />
## Why SigNoz?
Being developers, we found it annoying to rely on closed source SaaS vendors for every small feature we wanted. Closed source vendors often surprise you with huge month end bills without any transparency.
We wanted to make a self-hosted & open source version of tools like DataDog, NewRelic for companies that have privacy and security concerns about having customer data going to third party services.
Being open source also gives you complete control of your configuration, sampling, uptimes. You can also build modules over SigNoz to extend business specific capabilities
### Languages supported:
We support [OpenTelemetry](https://opentelemetry.io) as the library which you can use to instrument your applications. So any framework and language supported by OpenTelemetry is also supported by SigNoz. Some of the main supported languages are:
SigNoz supports all major programming languages for monitoring. Any framework and language supported by OpenTelemetry is supported by SigNoz. Find instructions for instrumenting different languages below:
- Java
- Python
- Node.js
- Go
- PHP
- .NET
- Ruby
- Elixir
- Rust
- [Java](https://signoz.io/docs/instrumentation/java/)
- [Python](https://signoz.io/docs/instrumentation/python/)
- [Node.js or Javascript](https://signoz.io/docs/instrumentation/javascript/)
- [Go](https://signoz.io/docs/instrumentation/golang/)
- [PHP](https://signoz.io/docs/instrumentation/php/)
- [.NET](https://signoz.io/docs/instrumentation/dotnet/)
- [Ruby](https://signoz.io/docs/instrumentation/ruby-on-rails/)
- [Elixir](https://signoz.io/docs/instrumentation/elixir/)
- [Rust](https://signoz.io/docs/instrumentation/rust/)
- [Swift](https://signoz.io/docs/instrumentation/swift/)
You can find the complete list of languages here - https://opentelemetry.io/docs/
<br /><br />
## Getting Started
### Deploy using Docker
Please follow the steps listed [here](https://signoz.io/docs/install/docker/) to install using docker
The [troubleshooting instructions](https://signoz.io/docs/install/troubleshooting/) may be helpful if you face any issues.
<p>&nbsp </p>
### Deploy in Kubernetes using Helm
Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_chart) to install using helm charts
You can find our entire documentation [here](https://signoz.io/docs/introduction/).
<br /><br />
@ -144,9 +165,11 @@ Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_cha
### SigNoz vs Prometheus
Prometheus is good if you want to do just metrics. But if you want to have a seamless experience between metrics and traces, then current experience of stitching together Prometheus & Jaeger is not great.
Prometheus is good if you want to do just metrics. But if you want to have a seamless experience between metrics, logs and traces, then current experience of stitching together Prometheus & other tools is not great.
Our goal is to provide an integrated UI between metrics & traces - similar to what SaaS vendors like Datadog provides - and give advanced filtering and aggregation over traces, something which Jaeger currently lack.
SigNoz is a one-stop solution for metrics and other telemetry signals. And because you will use the same standard(OpenTelemetry) to collect all telemetry signals, you can also correlate these signals to troubleshoot quickly.
For example, if you see that there are issues with infrastructure metrics of your k8s cluster at a timestamp, you can jump to other signals like logs and traces to understand the issue quickly.
<p>&nbsp </p>
@ -158,6 +181,7 @@ Moreover, SigNoz has few more advanced features wrt Jaeger:
- Jaegar UI doesnt show any metrics on traces or on filtered traces
- Jaeger cant get aggregates on filtered traces. For example, p99 latency of requests which have tag - customer_type='premium'. This can be done easily on SigNoz
- You can also go from traces to logs easily in SigNoz
<p>&nbsp </p>

View File

@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.56.0
image: signoz/query-service:0.57.0
command:
[
"-config=/root/config/prometheus.yml",
@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.56.0
image: signoz/frontend:0.57.0
deploy:
restart_policy:
condition: on-failure
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.102.12
image: signoz/signoz-otel-collector:0.111.5
command:
[
"--config=/etc/otel-collector-config.yaml",
@ -214,7 +214,6 @@ services:
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
- DOCKER_MULTI_NODE_CLUSTER=false
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
# - "1777:1777" # pprof extension
@ -238,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.102.10
image: signoz/signoz-schema-migrator:0.111.5
deploy:
restart_policy:
condition: on-failure

View File

@ -131,8 +131,7 @@ processors:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
@ -142,7 +141,6 @@ exporters:
# logging: {}
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
use_new_schema: true
extensions:

View File

@ -69,7 +69,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@ -84,7 +84,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.102.12
image: signoz/signoz-otel-collector:0.111.5
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@ -34,7 +34,7 @@ x-db-depend: &db-depend
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator:
otel-collector-migrator-sync:
condition: service_completed_successfully
# clickhouse-2:
# condition: service_healthy
@ -162,7 +162,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.56.0}
image: signoz/query-service:${DOCKER_TAG:-0.57.0}
container_name: signoz-query-service
command:
[
@ -201,7 +201,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
image: signoz/frontend:${DOCKER_TAG:-0.57.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@ -212,11 +212,13 @@ services:
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
container_name: otel-migrator
otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
container_name: otel-migrator-sync
command:
- "sync"
- "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on:
clickhouse:
condition: service_healthy
@ -225,9 +227,25 @@ services:
# clickhouse-3:
# condition: service_healthy
otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
container_name: otel-migrator-async
command:
- "async"
- "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator-sync:
condition: service_completed_successfully
# clickhouse-2:
# condition: service_healthy
# clickhouse-3:
# condition: service_healthy
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.12}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
container_name: signoz-otel-collector
command:
[
@ -244,7 +262,6 @@ services:
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
# - "1777:1777" # pprof extension
@ -262,7 +279,7 @@ services:
depends_on:
clickhouse:
condition: service_healthy
otel-collector-migrator:
otel-collector-migrator-sync:
condition: service_completed_successfully
query-service:
condition: service_healthy

View File

@ -167,7 +167,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.56.0}
image: signoz/query-service:${DOCKER_TAG:-0.57.0}
container_name: signoz-query-service
command:
[
@ -207,7 +207,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
image: signoz/frontend:${DOCKER_TAG:-0.57.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@ -219,7 +219,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@ -233,7 +233,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.12}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
container_name: signoz-otel-collector
command:
[
@ -250,7 +250,6 @@ services:
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
# - "1777:1777" # pprof extension

View File

@ -142,8 +142,7 @@ extensions:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
@ -152,7 +151,6 @@ exporters:
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
use_new_schema: true
# logging: {}

View File

@ -9,7 +9,15 @@ import (
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
if !strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+gateway.AllowedPrefix) {
validPath := false
for _, allowedPrefix := range gateway.AllowedPrefix {
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
validPath = true
break
}
}
if !validPath {
rw.WriteHeader(http.StatusNotFound)
return
}

View File

@ -53,7 +53,11 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
if anomalyQueryExists {
// ensure all queries have metric data source, and there should be only one anomaly query
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
if query.DataSource != v3.DataSourceMetrics {
// What is query.QueryName == query.Expression doing here?
// In the current implementation, the way to recognize if a query is a formula is by
// checking if the expression is the same as the query name. if the expression is different
// then it is a formula. otherwise, it is simple builder query.
if query.DataSource != v3.DataSourceMetrics && query.QueryName == query.Expression {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil)
return
}
@ -100,6 +104,13 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector),
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags),
)
default:
provider = anomaly.NewDailyProvider(
anomaly.WithCache[*anomaly.DailyProvider](aH.opts.Cache),
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
anomaly.WithReader[*anomaly.DailyProvider](aH.opts.DataConnector),
anomaly.WithFeatureLookup[*anomaly.DailyProvider](aH.opts.FeatureFlags),
)
}
anomalies, err := provider.GetAnomalies(r.Context(), &anomaly.GetAnomaliesRequest{Params: queryRangeParams})
if err != nil {

View File

@ -364,6 +364,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)

View File

@ -8,9 +8,9 @@ import (
"strings"
)
const (
RoutePrefix string = "/api/gateway"
AllowedPrefix string = "/v1/workspaces/me"
var (
RoutePrefix string = "/api/gateway"
AllowedPrefix []string = []string{"/v1/workspaces/me", "/v2/profiles/me", "/v2/deployments/me"}
)
type proxy struct {

View File

@ -68,7 +68,7 @@
"css-loader": "5.0.0",
"css-minimizer-webpack-plugin": "5.0.1",
"dayjs": "^1.10.7",
"dompurify": "3.0.0",
"dompurify": "3.1.3",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
@ -239,6 +239,7 @@
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1"
"phin": "^3.7.1",
"body-parser": "1.20.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

1
frontend/public/css/uPlot.min.css vendored Normal file
View File

@ -0,0 +1 @@
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -56,6 +56,7 @@
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_above_below": "above/below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
@ -110,6 +111,8 @@
"choose_alert_type": "Choose a type for the alert",
"metric_based_alert": "Metric based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"anomaly_based_alert": "Anomaly based Alert",
"anomaly_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"log_based_alert": "Log-based Alert",
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
"traces_based_alert": "Trace-based Alert",

View File

@ -43,6 +43,7 @@
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_above_below": "above/below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",

View File

@ -13,9 +13,12 @@
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"alert_form_step1": "Choose a detection method",
"alert_form_step2": "Define the metric",
"alert_form_step3": "Define Alert Conditions",
"alert_form_step4": "Alert Configuration",
"threshold_alert_desc": "An alert is triggered whenever a metric deviates from an expected threshold.",
"anomaly_detection_alert_desc": "An alert is triggered whenever a metric deviates from an expected pattern.",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
@ -35,6 +38,7 @@
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when",
"text_condition1_anomaly": "Send notification when the observed value for",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_1min": "1 min",
@ -56,6 +60,7 @@
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_above_below": "above/below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
@ -109,7 +114,9 @@
"user_tooltip_more_help": "More details on how to create alerts",
"choose_alert_type": "Choose a type for the alert",
"metric_based_alert": "Metric based Alert",
"anomaly_based_alert": "Anomaly based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"anomaly_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
"log_based_alert": "Log-based Alert",
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
"traces_based_alert": "Trace-based Alert",

View File

@ -43,6 +43,7 @@
"option_last": "last",
"option_above": "above",
"option_below": "below",
"option_above_below": "above/below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",

View File

@ -0,0 +1,18 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllOrgPreferences = async (): Promise<
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/org/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllOrgPreferences;

View File

@ -0,0 +1,18 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllUserPreferences = async (): Promise<
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllUserPreferences;

View File

@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getOrgPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
const response = await axios.get(`/org/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getOrgPreference;

View File

@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getUserPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getUserPreference;

View File

@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateOrgPreferenceProps,
UpdateOrgPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateOrgPreference = async (
preferencePayload: UpdateOrgPreferenceProps,
): Promise<
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(`/org/preferences`, {
preference_value: preferencePayload.value,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateOrgPreference;

View File

@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateUserPreferenceProps,
UpdateUserPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateUserPreference = async (
preferencePayload: UpdateUserPreferenceProps,
): Promise<
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(`/user/preferences`, {
preference_value: preferencePayload.value,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateUserPreference;

View File

@ -1,46 +1,3 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AlertDef } from 'types/api/alerts/def';
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
export const chartHelpMessage = (
selectedDashboard: Dashboard | undefined,
graphType: PANEL_TYPES,
): string => `
Hi Team,
I need help in creating this chart. Here are my dashboard details
Name: ${selectedDashboard?.data.title || ''}
Panel type: ${graphType}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardHelpMessage = (
data: DashboardData | undefined,
selectedDashboard: Dashboard | undefined,
): string => `
Hi Team,
I need help with this dashboard. Here are my dashboard details
Name: ${data?.title || ''}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardListMessage = `Hi Team,
I need help with dashboards.
Thanks`;
export const listAlertMessage = `Hi Team,
I need help with managing alerts.
Thanks`;
export const onboardingHelpMessage = (
dataSourceName: string,
moduleId: string,
@ -55,35 +12,3 @@ Module: ${moduleId}
Thanks
`;
export const alertHelpMessage = (
alertDef: AlertDef,
ruleId: number,
): string => `
Hi Team,
I need help in configuring this alert. Here are my alert rule details
Name: ${alertDef?.alert || ''}
Alert Type: ${alertDef?.alertType || ''}
State: ${(alertDef as any)?.state || ''}
Alert Id: ${ruleId}
Thanks`;
export const integrationsListMessage = `Hi Team,
I need help with Integrations.
Thanks`;
export const integrationDetailMessage = (
selectedIntegration: string,
): string => `
Hi Team,
I need help in configuring this integration.
Integration Id: ${selectedIntegration}
Thanks`;

View File

@ -129,6 +129,7 @@ function LogDetail({
return (
<Drawer
width="60%"
maskStyle={{ background: 'none' }}
title={
<>
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />

View File

@ -195,21 +195,20 @@ function ListLogView({
return (
<>
<Container
$isActiveLog={isHighlighted}
$isActiveLog={
isHighlighted ||
activeLog?.id === logData.id ||
activeContextLog?.id === logData.id
}
$isDarkMode={isDarkMode}
$logType={logType}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}
fontSize={fontSize}
>
<div className="log-line">
<LogStateIndicator
type={logType}
isActive={
activeLog?.id === logData.id || activeContextLog?.id === logData.id
}
fontSize={fontSize}
/>
<LogStateIndicator type={logType} fontSize={fontSize} />
<div>
<LogContainer fontSize={fontSize}>
<LogGeneralField

View File

@ -1,8 +1,8 @@
/* eslint-disable no-nested-ternary */
import { Color } from '@signozhq/design-tokens';
import { Card, Typography } from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
import { getActiveLogBackground } from 'utils/logs';
interface LogTextProps {
linesPerRow?: number;
@ -15,6 +15,7 @@ interface LogContainerProps {
export const Container = styled(Card)<{
$isActiveLog: boolean;
$isDarkMode: boolean;
$logType: string;
fontSize: FontSize;
}>`
width: 100% !important;
@ -41,13 +42,8 @@ export const Container = styled(Card)<{
? `padding:0.3rem 0.6rem;`
: ``}
${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
: ''}
}
${({ $isActiveLog, $isDarkMode, $logType }): string =>
getActiveLogBackground($isActiveLog, $isDarkMode, $logType)}
`;
export const Text = styled(Typography.Text)`

View File

@ -41,10 +41,4 @@
background-color: var(--bg-sakura-500);
}
}
&.isActive {
.line {
background-color: var(--bg-robin-400, #7190f9);
}
}
}

View File

@ -17,14 +17,6 @@ describe('LogStateIndicator', () => {
);
});
it('renders correctly when isActive is true', () => {
const { container } = render(
<LogStateIndicator type="INFO" isActive fontSize={FontSize.MEDIUM} />,
);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('isActive')).toBe(true);
});
it('renders correctly with different types', () => {
const { container: containerInfo } = render(
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,

View File

@ -44,22 +44,16 @@ export const LogType = {
function LogStateIndicator({
type,
isActive,
fontSize,
}: {
type: string;
fontSize: FontSize;
isActive?: boolean;
}): JSX.Element {
return (
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
<div className="log-state-indicator">
<div className={cx('line', type, fontSize)}> </div>
</div>
);
}
LogStateIndicator.defaultProps = {
isActive: false,
};
export default LogStateIndicator;

View File

@ -162,20 +162,15 @@ function RawLogView({
$isDarkMode={isDarkMode}
$isReadOnly={isReadOnly}
$isHightlightedLog={isHighlighted}
$isActiveLog={isActiveLog}
$isActiveLog={
activeLog?.id === data.id || activeContextLog?.id === data.id || isActiveLog
}
$logType={logType}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
fontSize={fontSize}
>
<LogStateIndicator
type={logType}
isActive={
activeLog?.id === data.id ||
activeContextLog?.id === data.id ||
isActiveLog
}
fontSize={fontSize}
/>
<LogStateIndicator type={logType} fontSize={fontSize} />
<RawLogContent
$isReadOnly={isReadOnly}

View File

@ -13,6 +13,7 @@ export const RawLogViewContainer = styled(Row)<{
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isHightlightedLog: boolean;
$logType: string;
fontSize: FontSize;
}>`
position: relative;
@ -34,11 +35,12 @@ export const RawLogViewContainer = styled(Row)<{
: `margin: 2px 0;`}
}
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
${({ $isActiveLog, $logType }): string =>
getActiveLogBackground($isActiveLog, true, $logType)}
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
$isActiveLog
? getActiveLogBackground($isActiveLog, $isDarkMode)
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
${({ $isHightlightedLog, $isDarkMode }): string =>

View File

@ -35,8 +35,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
linesPerRow,
fontSize,
appendTo = 'center',
activeContextLog,
activeLog,
isListViewPanel,
} = props;
@ -90,9 +88,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
<div className="table-timestamp">
<LogStateIndicator
type={getLogIndicatorTypeForTable(item)}
isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id
}
fontSize={fontSize}
/>
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
@ -130,16 +125,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
},
...(appendTo === 'end' ? fieldColumns : []),
];
}, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
activeLog?.id,
activeContextLog?.id,
fontSize,
]);
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
return { columns, dataSource: flattenLogData };
};

View File

@ -107,6 +107,7 @@ function DynamicColumnTable({
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
data-testid="additional-filters-button"
/>
</Dropdown>
)}

View File

@ -2,6 +2,7 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { DataSource } from 'types/common/queryBuilder';
export const ALERTS_DATA_SOURCE_MAP: Record<AlertTypes, DataSource> = {
[AlertTypes.ANOMALY_BASED_ALERT]: DataSource.METRICS,
[AlertTypes.METRICS_BASED_ALERT]: DataSource.METRICS,
[AlertTypes.LOGS_BASED_ALERT]: DataSource.LOGS,
[AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES,

View File

@ -22,4 +22,5 @@ export enum FeatureKeys {
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
}

View File

@ -36,4 +36,5 @@ export enum QueryParams {
topic = 'topic',
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType',
}

View File

@ -67,6 +67,10 @@ export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
{
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
];
export const logsQueryFunctionOptions: SelectOption<string, string>[] = [
@ -80,10 +84,15 @@ interface QueryFunctionConfigType {
showInput: boolean;
inputType?: string;
placeholder?: string;
disabled?: boolean;
};
}
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
anomaly: {
showInput: false,
disabled: true,
},
cutOffMin: {
showInput: true,
inputType: 'text',

View File

@ -0,0 +1,180 @@
.anomaly-alert-evaluation-view {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
width: 100%;
height: 100%;
.anomaly-alert-evaluation-view-chart-section {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
&.has-multi-series-data {
width: calc(100% - 240px);
}
.anomaly-alert-evaluation-view-no-data-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.anomaly-alert-evaluation-view-series-selection {
display: flex;
flex-direction: column;
gap: 8px;
width: 240px;
padding: 0px 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
.anomaly-alert-evaluation-view-series-list-search {
margin-bottom: 16px;
}
.anomaly-alert-evaluation-view-series-list-title {
margin-top: 12px;
font-size: 13px !important;
font-weight: 400;
}
.anomaly-alert-evaluation-view-series-list-items {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
overflow-y: auto;
.anomaly-alert-evaluation-view-series-list-item {
display: flex;
flex-direction: row;
gap: 8px;
.anomaly-alert-evaluation-view-series-list-item-color {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-flex;
margin-right: 8px;
vertical-align: middle;
}
cursor: pointer;
}
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
}
.uplot {
.u-title {
text-align: center;
font-size: 18px;
font-weight: 400;
display: flex;
height: 40px;
font-size: 13px;
align-items: center;
}
.u-legend {
display: flex;
margin-top: 16px;
tbody {
width: 100%;
.u-series {
display: inline-flex;
}
}
}
}
}
.uplot-tooltip {
background-color: rgba(0, 0, 0, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #ddd;
font-size: 13px;
line-height: 1.4;
padding: 8px 12px;
pointer-events: none;
position: absolute;
z-index: 100;
max-height: 500px;
width: 280px;
overflow-y: auto;
display: none; /* Hide tooltip by default */
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.uplot-tooltip-title {
font-weight: bold;
margin-bottom: 4px;
}
.uplot-tooltip-series {
display: flex;
gap: 4px;
padding: 4px 0px;
align-items: center;
}
.uplot-tooltip-series-name {
margin-right: 4px;
}
.uplot-tooltip-band {
font-style: italic;
color: #666;
}
.uplot-tooltip-marker {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}

View File

@ -0,0 +1,363 @@
import 'uplot/dist/uPlot.min.css';
import './AnomalyAlertEvaluationView.styles.scss';
import { Checkbox, Typography } from 'antd';
import Search from 'antd/es/input/Search';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useResizeObserver } from 'hooks/useDimensions';
import getAxes from 'lib/uPlotLib/utils/getAxes';
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
import { LineChart } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import uPlot from 'uplot';
import tooltipPlugin from './tooltipPlugin';
function UplotChart({
data,
options,
chartRef,
}: {
data: any;
options: any;
chartRef: any;
}): JSX.Element {
const plotInstance = useRef(null);
useEffect(() => {
if (plotInstance.current) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
plotInstance.current.destroy();
}
if (data && data.length > 0) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line new-cap
plotInstance.current = new uPlot(options, data, chartRef.current);
}
return (): void => {
if (plotInstance.current) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
plotInstance.current.destroy();
}
};
}, [data, options, chartRef]);
return <div ref={chartRef} />;
}
function AnomalyAlertEvaluationView({
data,
yAxisUnit,
}: {
data: any;
yAxisUnit: string;
}): JSX.Element {
const { spline } = uPlot.paths;
// eslint-disable-next-line @typescript-eslint/naming-convention
const _spline = spline ? spline() : undefined;
const chartRef = useRef<HTMLDivElement>(null);
const isDarkMode = useIsDarkMode();
const [seriesData, setSeriesData] = useState<any>({});
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
const [filteredSeriesKeys, setFilteredSeriesKeys] = useState<string[]>([]);
const [allSeries, setAllSeries] = useState<string[]>([]);
const graphRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(graphRef);
useEffect(() => {
const chartData = getUplotChartDataForAnomalyDetection(data, isDarkMode);
setSeriesData(chartData);
setAllSeries(Object.keys(chartData));
setFilteredSeriesKeys(Object.keys(chartData));
}, [data, isDarkMode]);
useEffect(() => {
const seriesKeys = Object.keys(seriesData);
if (seriesKeys.length === 1) {
setSelectedSeries(seriesKeys[0]); // Automatically select if only one series
} else {
setSelectedSeries(null); // Default to "Show All" if multiple series
}
}, [seriesData]);
const handleSeriesChange = (series: string | null): void => {
setSelectedSeries(series);
};
const bandsPlugin = {
hooks: {
draw: [
(u: any): void => {
if (!selectedSeries) return;
const { ctx } = u;
const upperBandIdx = 3;
const lowerBandIdx = 4;
const xData = u.data[0];
const yUpperData = u.data[upperBandIdx];
const yLowerData = u.data[lowerBandIdx];
const strokeStyle =
u.series[1]?.stroke || seriesData[selectedSeries].color;
const fillStyle =
typeof strokeStyle === 'string'
? strokeStyle.replace(')', ', 0.1)')
: 'rgba(255, 255, 255, 0.1)';
ctx.beginPath();
const firstX = u.valToPos(xData[0], 'x', true);
const firstUpperY = u.valToPos(yUpperData[0], 'y', true);
ctx.moveTo(firstX, firstUpperY);
for (let i = 0; i < xData.length; i++) {
const x = u.valToPos(xData[i], 'x', true);
const y = u.valToPos(yUpperData[i], 'y', true);
ctx.lineTo(x, y);
}
for (let i = xData.length - 1; i >= 0; i--) {
const x = u.valToPos(xData[i], 'x', true);
const y = u.valToPos(yLowerData[i], 'y', true);
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
},
],
},
};
const initialData = allSeries.length
? [
seriesData[allSeries[0]].data[0], // Shared X-axis
...allSeries.map((key) => seriesData[key].data[1]), // Map through Y-axis data for all series
]
: [];
const options = {
width: dimensions.width,
height: dimensions.height - 36,
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
focus: {
alpha: 0.3,
},
legend: {
show: true,
live: false,
isolate: true,
},
cursor: {
lock: false,
focus: {
prox: 1e6,
bias: 1,
},
points: {
size: (
u: { series: { [x: string]: { points: { size: number } } } },
seriesIdx: string | number,
): number => u.series[seriesIdx].points.size * 3,
width: (u: any, seriesIdx: any, size: number): number => size / 4,
stroke: (
u: {
series: {
[x: string]: { points: { stroke: (arg0: any, arg1: any) => any } };
};
},
seriesIdx: string | number,
): string => `${u.series[seriesIdx].points.stroke(u, seriesIdx)}90`,
fill: (): string => '#fff',
},
},
series: [
{
label: 'Time',
},
...(selectedSeries
? [
{
label: `Main Series`,
stroke: seriesData[selectedSeries].color,
width: 2,
show: true,
paths: _spline,
spanGaps: true,
},
{
label: `Predicted Value`,
stroke: seriesData[selectedSeries].color,
width: 1,
dash: [2, 2],
show: true,
paths: _spline,
spanGaps: true,
},
{
label: `Upper Band`,
stroke: 'transparent',
show: true,
paths: _spline,
spanGaps: true,
points: {
show: false,
size: 1,
},
},
{
label: `Lower Band`,
stroke: 'transparent',
show: true,
paths: _spline,
spanGaps: true,
points: {
show: false,
size: 1,
},
},
]
: allSeries.map((seriesKey) => ({
label: seriesKey,
stroke: seriesData[seriesKey].color,
width: 2,
show: true,
paths: _spline,
spanGaps: true,
}))),
],
scales: {
x: {
time: true,
spanGaps: true,
},
y: {
...getYAxisScaleForAnomalyDetection({
seriesData,
selectedSeries,
initialData,
yAxisUnit,
}),
},
},
grid: {
show: true,
},
axes: getAxes(isDarkMode, yAxisUnit),
};
const handleSearch = (searchText: string): void => {
if (!searchText || searchText.length === 0) {
setFilteredSeriesKeys(allSeries);
return;
}
const filteredSeries = allSeries.filter((series) =>
series.toLowerCase().includes(searchText.toLowerCase()),
);
setFilteredSeriesKeys(filteredSeries);
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const value = event?.target?.value || '';
handleSearch(value);
}, 300);
return (
<div className="anomaly-alert-evaluation-view">
<div
className={`anomaly-alert-evaluation-view-chart-section ${
allSeries.length > 1 ? 'has-multi-series-data' : ''
}`}
ref={graphRef}
>
{allSeries.length > 0 ? (
<UplotChart
data={selectedSeries ? seriesData[selectedSeries].data : initialData}
options={options}
chartRef={chartRef}
/>
) : (
<div className="anomaly-alert-evaluation-view-no-data-container">
<LineChart size={48} strokeWidth={0.5} />
<Typography>No Data</Typography>
</div>
)}
</div>
{allSeries.length > 1 && (
<div className="anomaly-alert-evaluation-view-series-selection">
{allSeries.length > 1 && (
<div className="anomaly-alert-evaluation-view-series-list">
<Search
className="anomaly-alert-evaluation-view-series-list-search"
placeholder="Search a series"
allowClear
onChange={handleSearchValueChange}
/>
<div className="anomaly-alert-evaluation-view-series-list-items">
{filteredSeriesKeys.length > 0 && (
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
type="checkbox"
name="series"
value="all"
checked={selectedSeries === null}
onChange={(): void => handleSeriesChange(null)}
>
Show All
</Checkbox>
)}
{filteredSeriesKeys.map((seriesKey) => (
<div key={seriesKey}>
<Checkbox
className="anomaly-alert-evaluation-view-series-list-item"
key={seriesKey}
type="checkbox"
name="series"
value={seriesKey}
checked={selectedSeries === seriesKey}
onChange={(): void => handleSeriesChange(seriesKey)}
>
<div
className="anomaly-alert-evaluation-view-series-list-item-color"
style={{ backgroundColor: seriesData[seriesKey].color }}
/>
{seriesKey}
</Checkbox>
</div>
))}
{filteredSeriesKeys.length === 0 && (
<Typography>No series found</Typography>
)}
</div>
</div>
)}
</div>
)}
</div>
);
}
export default AnomalyAlertEvaluationView;

View File

@ -0,0 +1,3 @@
import AnomalyAlertEvaluationView from './AnomalyAlertEvaluationView';
export default AnomalyAlertEvaluationView;

View File

@ -0,0 +1,148 @@
import { themeColors } from 'constants/theme';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
const tooltipPlugin = (
isDarkMode: boolean,
): { hooks: { init: (u: any) => void } } => {
let tooltip: HTMLDivElement;
const tooltipLeftOffset = 10;
const tooltipTopOffset = 10;
let isMouseOverPlot = false;
function formatValue(value: string | number | Date): string | number | Date {
if (typeof value === 'string' && !Number.isNaN(parseFloat(value))) {
return parseFloat(value).toFixed(3);
}
if (typeof value === 'number') {
return value.toFixed(3);
}
if (value instanceof Date) {
return value.toLocaleString();
}
if (value == null) {
return 'N/A';
}
return String(value);
}
function updateTooltip(u: any, left: number, top: number): void {
const idx = u.posToIdx(left);
const xVal = u.data[0][idx];
if (xVal == null) {
tooltip.style.display = 'none';
return;
}
const xDate = new Date(xVal * 1000);
const formattedXDate = formatValue(xDate);
let tooltipContent = `<div class="uplot-tooltip-title">Time: ${formattedXDate}</div>`;
let mainValue;
let upperBand;
let lowerBand;
let color = null;
// Loop through all series (excluding the x-axis series)
for (let i = 1; i < u.series.length; i++) {
const series = u.series[i];
const yVal = u.data[i][idx];
const formattedYVal = formatValue(yVal);
color = generateColor(
series.label,
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
);
// Create the round marker for the series
const marker = `<span class="uplot-tooltip-marker" style="background-color: ${color};"></span>`;
if (series.label.toLowerCase().includes('upper band')) {
upperBand = formattedYVal;
} else if (series.label.toLowerCase().includes('lower band')) {
lowerBand = formattedYVal;
} else if (series.label.toLowerCase().includes('main series')) {
mainValue = formattedYVal;
} else {
tooltipContent += `
<div class="uplot-tooltip-series">
${marker}
<span class="uplot-tooltip-series-name">${series.label}:</span>
<span class="uplot-tooltip-series-value">${formattedYVal}</span>
</div>`;
}
}
// Add main value, upper band, and lower band to the tooltip
if (mainValue !== undefined) {
const marker = `<span class="uplot-tooltip-marker"></span>`;
tooltipContent += `
<div class="uplot-tooltip-series">
${marker}
<span class="uplot-tooltip-series-name">Main Series:</span>
<span class="uplot-tooltip-series-value">${mainValue}</span>
</div>`;
}
if (upperBand !== undefined) {
const marker = `<span class="uplot-tooltip-marker"></span>`;
tooltipContent += `
<div class="uplot-tooltip-series">
${marker}
<span class="uplot-tooltip-series-name">Upper Band:</span>
<span class="uplot-tooltip-series-value">${upperBand}</span>
</div>`;
}
if (lowerBand !== undefined) {
const marker = `<span class="uplot-tooltip-marker"></span>`;
tooltipContent += `
<div class="uplot-tooltip-series">
${marker}
<span class="uplot-tooltip-series-name">Lower Band:</span>
<span class="uplot-tooltip-series-value">${lowerBand}</span>
</div>`;
}
tooltip.innerHTML = tooltipContent;
tooltip.style.display = 'block';
tooltip.style.left = `${left + tooltipLeftOffset}px`;
tooltip.style.top = `${top + tooltipTopOffset}px`;
}
function init(u: any): void {
tooltip = document.createElement('div');
tooltip.className = 'uplot-tooltip';
tooltip.style.display = 'none';
u.over.appendChild(tooltip);
// Add event listeners
u.over.addEventListener('mouseenter', () => {
isMouseOverPlot = true;
});
u.over.addEventListener('mouseleave', () => {
isMouseOverPlot = false;
tooltip.style.display = 'none';
});
u.over.addEventListener('mousemove', (e: MouseEvent) => {
if (isMouseOverPlot) {
const rect = u.over.getBoundingClientRect();
const left = e.clientX - rect.left;
const top = e.clientY - rect.top;
updateTooltip(u, left, top);
}
});
}
return {
hooks: {
init,
},
};
};
export default tooltipPlugin;

View File

@ -211,6 +211,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [licenseData, isFetching]);
useEffect(() => {
// after logging out hide the trial expiry banner
if (!isLoggedIn) {
setShowTrialExpiryBanner(false);
}
}, [isLoggedIn]);
const handleUpgrade = (): void => {
if (role === 'ADMIN') {
history.push(ROUTES.BILLING);

View File

@ -3,25 +3,41 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
import { OptionType } from './types';
export const getOptionList = (t: TFunction): OptionType[] => [
{
title: t('metric_based_alert'),
selection: AlertTypes.METRICS_BASED_ALERT,
description: t('metric_based_alert_desc'),
},
{
title: t('log_based_alert'),
selection: AlertTypes.LOGS_BASED_ALERT,
description: t('log_based_alert_desc'),
},
{
title: t('traces_based_alert'),
selection: AlertTypes.TRACES_BASED_ALERT,
description: t('traces_based_alert_desc'),
},
{
title: t('exceptions_based_alert'),
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
description: t('exceptions_based_alert_desc'),
},
];
export const getOptionList = (
t: TFunction,
isAnomalyDetectionEnabled: boolean,
): OptionType[] => {
const optionList: OptionType[] = [
{
title: t('metric_based_alert'),
selection: AlertTypes.METRICS_BASED_ALERT,
description: t('metric_based_alert_desc'),
},
{
title: t('log_based_alert'),
selection: AlertTypes.LOGS_BASED_ALERT,
description: t('log_based_alert_desc'),
},
{
title: t('traces_based_alert'),
selection: AlertTypes.TRACES_BASED_ALERT,
description: t('traces_based_alert_desc'),
},
{
title: t('exceptions_based_alert'),
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
description: t('exceptions_based_alert_desc'),
},
];
if (isAnomalyDetectionEnabled) {
optionList.unshift({
title: t('anomaly_based_alert'),
selection: AlertTypes.ANOMALY_BASED_ALERT,
description: t('anomaly_based_alert_desc'),
isBeta: true,
});
}
return optionList;
};

View File

@ -1,6 +1,8 @@
import { Row, Typography } from 'antd';
import { Row, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@ -12,11 +14,18 @@ import { OptionType } from './types';
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
const { t } = useTranslation(['alerts']);
const optionList = getOptionList(t);
const isAnomalyDetectionEnabled =
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
function handleRedirection(option: AlertTypes): void {
let url = '';
switch (option) {
case AlertTypes.ANOMALY_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
break;
case AlertTypes.METRICS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
@ -52,6 +61,13 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
<AlertTypeCard
key={option.selection}
title={option.title}
extra={
option.isBeta ? (
<Tag bordered={false} color="geekblue">
Beta
</Tag>
) : undefined
}
onClick={(): void => {
onSelect(option.selection);
}}

View File

@ -4,4 +4,5 @@ export interface OptionType {
title: string;
selection: AlertTypes;
description: string;
isBeta?: boolean;
}

View File

@ -4,12 +4,15 @@ import {
initialQueryPromQLData,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertDef,
defaultAlgorithm,
defaultCompareOp,
defaultEvalWindow,
defaultMatchType,
defaultSeasonality,
} from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
@ -46,6 +49,51 @@ export const alertDefaults: AlertDef = {
},
op: defaultCompareOp,
matchType: defaultMatchType,
algorithm: defaultAlgorithm,
seasonality: defaultSeasonality,
},
labels: {
severity: 'warning',
},
annotations: defaultAnnotations,
evalWindow: defaultEvalWindow,
};
export const anamolyAlertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
version: ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
condition: {
compositeQuery: {
builderQueries: {
A: {
...initialQueryBuilderFormValuesMap.metrics,
functions: [
{
name: 'anomaly',
args: [],
namedArgs: { z_score_threshold: 3 },
},
],
},
},
promQueries: { A: initialQueryPromQLData },
chQueries: {
A: {
name: 'A',
query: ``,
legend: '',
disabled: false,
},
},
queryType: EQueryType.QUERY_BUILDER,
panelType: PANEL_TYPES.TIME_SERIES,
unit: undefined,
},
op: defaultCompareOp,
matchType: defaultMatchType,
algorithm: defaultAlgorithm,
seasonality: defaultSeasonality,
},
labels: {
severity: 'warning',
@ -56,6 +104,7 @@ export const alertDefaults: AlertDef = {
export const logAlertDefaults: AlertDef = {
alertType: AlertTypes.LOGS_BASED_ALERT,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@ -86,6 +135,7 @@ export const logAlertDefaults: AlertDef = {
export const traceAlertDefaults: AlertDef = {
alertType: AlertTypes.TRACES_BASED_ALERT,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@ -116,6 +166,7 @@ export const traceAlertDefaults: AlertDef = {
export const exceptionAlertDefaults: AlertDef = {
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
version: ENTITY_VERSION_V4,
condition: {
compositeQuery: {
builderQueries: {
@ -145,6 +196,7 @@ export const exceptionAlertDefaults: AlertDef = {
};
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
[AlertTypes.ANOMALY_BASED_ALERT]: anamolyAlertDefaults,
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,

View File

@ -2,7 +2,7 @@ import { Form, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import FormAlertRules from 'container/FormAlertRules';
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import history from 'lib/history';
import { useEffect, useState } from 'react';
@ -13,6 +13,7 @@ import { AlertDef } from 'types/api/alerts/def';
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
import {
alertDefaults,
anamolyAlertDefaults,
exceptionAlertDefaults,
logAlertDefaults,
traceAlertDefaults,
@ -24,8 +25,12 @@ function CreateRules(): JSX.Element {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const alertTypeFromURL = queryParams.get(QueryParams.ruleType);
const version = queryParams.get('version');
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
const alertTypeFromParams =
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
? AlertTypes.ANOMALY_BASED_ALERT
: queryParams.get(QueryParams.alertType);
const compositeQuery = useGetCompositeQueryParam();
function getAlertTypeFromDataSource(): AlertTypes | null {
@ -45,6 +50,7 @@ function CreateRules(): JSX.Element {
const onSelectType = (typ: AlertTypes): void => {
setAlertType(typ);
switch (typ) {
case AlertTypes.LOGS_BASED_ALERT:
setInitValues(logAlertDefaults);
@ -55,13 +61,40 @@ function CreateRules(): JSX.Element {
case AlertTypes.EXCEPTIONS_BASED_ALERT:
setInitValues(exceptionAlertDefaults);
break;
case AlertTypes.ANOMALY_BASED_ALERT:
setInitValues({
...anamolyAlertDefaults,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
});
break;
default:
setInitValues({
...alertDefaults,
version: version || ENTITY_VERSION_V4,
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
});
}
queryParams.set(QueryParams.alertType, typ);
queryParams.set(
QueryParams.alertType,
typ === AlertTypes.ANOMALY_BASED_ALERT
? AlertTypes.METRICS_BASED_ALERT
: typ,
);
if (
typ === AlertTypes.ANOMALY_BASED_ALERT ||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
) {
queryParams.set(
QueryParams.ruleType,
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
);
} else {
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
}
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
history.replace(generatedUrl);
};

View File

@ -7,18 +7,16 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
const [formInstance] = Form.useForm();
return (
<div style={{ marginTop: '1rem' }}>
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
</div>
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
);
}

View File

@ -18,7 +18,7 @@
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
padding: 10px 10px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.6);
@ -33,6 +33,7 @@
border: 1px solid var(--bg-slate-400);
background: var(--bg-slate-500);
cursor: pointer;
box-shadow: none;
}
.hidden {

View File

@ -45,7 +45,6 @@ import {
PanelBottomClose,
Plus,
X,
XCircle,
} from 'lucide-react';
import {
CSSProperties,
@ -515,7 +514,11 @@ function ExplorerOptions({
return (
<div className="explorer-options-container">
{isQueryUpdated && !isExplorerOptionHidden && (
{
// if a viewName is selected and the explorer options are not hidden then
// always show the clear option
}
{!isExplorerOptionHidden && viewName && (
<div
className={cx(
isEditDeleteSupported ? '' : 'hide-update',
@ -529,18 +532,25 @@ function ExplorerOptions({
icon={<X size={14} />}
/>
</Tooltip>
<Divider
type="vertical"
className={isEditDeleteSupported ? '' : 'hidden'}
/>
<Tooltip title="Update this view" placement="top">
<Button
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />}
/>
</Tooltip>
{
// only show the update view option when the query is updated
}
{isQueryUpdated && (
<>
<Divider
type="vertical"
className={isEditDeleteSupported ? '' : 'hidden'}
/>
<Tooltip title="Update this view" placement="top">
<Button
className={cx('action-icon', isEditDeleteSupported ? ' ' : 'hidden')}
disabled={isViewUpdating}
onClick={onUpdateQueryHandler}
icon={<Disc3 size={14} />}
/>
</Tooltip>
</>
)}
</div>
)}
{!isExplorerOptionHidden && (
@ -564,10 +574,7 @@ function ExplorerOptions({
}}
dropdownStyle={dropdownStyle}
className="views-dropdown"
allowClear={{
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
}}
onClear={handleClearSelect}
allowClear={false}
ref={ref}
>
{viewsData?.data?.data?.map((view) => {
@ -662,8 +669,8 @@ function ExplorerOptions({
</div>
</div>
)}
<ExplorerOptionsHideArea
viewName={viewName}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
sourcepage={sourcepage}
@ -672,7 +679,6 @@ function ExplorerOptions({
onUpdateQueryHandler={onUpdateQueryHandler}
isEditDeleteSupported={isEditDeleteSupported}
/>
<Modal
className="save-view-modal"
title={<span className="title">Save this view</span>}
@ -705,7 +711,6 @@ function ExplorerOptions({
/>
</div>
</Modal>
<Modal
footer={null}
onOk={onCancel(false)}

View File

@ -10,6 +10,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { setExplorerToolBarVisibility } from './utils';
interface DroppableAreaProps {
viewName: string;
isQueryUpdated: boolean;
isExplorerOptionHidden?: boolean;
sourcepage: DataSource;
@ -20,6 +21,7 @@ interface DroppableAreaProps {
}
function ExplorerOptionsHideArea({
viewName,
isQueryUpdated,
isExplorerOptionHidden,
sourcepage,
@ -39,7 +41,7 @@ function ExplorerOptionsHideArea({
<div className="explorer-option-droppable-container">
{isExplorerOptionHidden && (
<>
{isQueryUpdated && (
{viewName && (
<div className="explorer-actions-btn">
<Tooltip title="Clear this view">
<Button
@ -49,7 +51,7 @@ function ExplorerOptionsHideArea({
icon={<X size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
{isEditDeleteSupported && (
{isEditDeleteSupported && isQueryUpdated && (
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}

View File

@ -96,7 +96,7 @@ function BasicInfo({
return (
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>
<StepHeading> {t('alert_form_step4')} </StepHeading>
<FormContainer>
<Form.Item
label={t('field_severity')}

View File

@ -0,0 +1,26 @@
.alert-chart-container {
height: 57vh;
width: 100%;
.threshold-alert-uplot-chart-container {
height: calc(100% - 24px);
}
.ant-card-body {
padding: 12px;
}
.anomaly-alert-evaluation-view-loading-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.anomaly-alert-evaluation-view-error-container {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@ -1,8 +1,12 @@
import './ChartPreview.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
@ -14,6 +18,7 @@ import {
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString';
@ -34,6 +39,7 @@ import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { AlertDetectionTypes } from '..';
import { ChartContainer, FailedMessageContainer } from './styles';
import { getThresholdLabel } from './utils';
@ -141,6 +147,7 @@ function ChartPreview({
selectedInterval,
minTime,
maxTime,
alertDef?.ruleType,
],
retry: false,
enabled: canQuery,
@ -163,8 +170,6 @@ function ChartPreview({
queryResponse.data.payload.data.result = sortedSeriesData;
}
const chartData = getUPlotChartData(queryResponse?.data?.payload);
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
@ -202,7 +207,10 @@ function ChartPreview({
id: 'alert_legend_widget',
yAxisUnit,
apiResponse: queryResponse?.data?.payload,
dimensions: containerDimensions,
dimensions: {
height: containerDimensions?.height ? containerDimensions.height - 48 : 0,
width: containerDimensions?.width,
},
minTimeScale,
maxTimeScale,
isDarkMode,
@ -245,36 +253,59 @@ function ChartPreview({
],
);
const chartData = getUPlotChartData(queryResponse?.data?.payload);
const isAnomalyDetectionAlert =
alertDef?.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
const chartDataAvailable =
chartData && !queryResponse.isError && !queryResponse.isLoading;
const isAnomalyDetectionEnabled =
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
return (
<ChartContainer>
{headline}
<div className="alert-chart-container" ref={graphRef}>
<ChartContainer>
{headline}
<div ref={graphRef} style={{ height: '100%' }}>
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '}
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
<div className="threshold-alert-uplot-chart-container">
{queryResponse.isLoading && (
<Spinner size="large" tip="Loading..." height="100%" />
)}
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />
{queryResponse.error.message || t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{chartData && !queryResponse.isError && !queryResponse.isLoading && (
<GridPanelSwitch
options={options}
panelType={graphType}
data={chartData}
name={name || 'Chart Preview'}
panelData={
queryResponse.data?.payload?.data?.newResult?.data?.result || []
}
query={query || initialQueriesMap.metrics}
yAxisUnit={yAxisUnit}
/>
)}
</div>
</ChartContainer>
{chartDataAvailable && !isAnomalyDetectionAlert && (
<GridPanelSwitch
options={options}
panelType={graphType}
data={chartData}
name={name || 'Chart Preview'}
panelData={
queryResponse.data?.payload?.data?.newResult?.data?.result || []
}
query={query || initialQueriesMap.metrics}
yAxisUnit={yAxisUnit}
/>
)}
{chartDataAvailable &&
isAnomalyDetectionAlert &&
isAnomalyDetectionEnabled &&
queryResponse?.data?.payload?.data?.resultType === 'anomaly' && (
<AnomalyAlertEvaluationView
data={queryResponse?.data?.payload}
yAxisUnit={yAxisUnit}
/>
)}
</div>
</ChartContainer>
</div>
);
}

View File

@ -21,6 +21,70 @@
}
}
.steps-container {
width: 80%;
}
.qb-chart-preview-container {
margin-bottom: 1rem;
display: flex;
flex-direction: row;
gap: 1rem;
}
.overview-header {
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
.alert-type-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.alert-type-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.ant-typography {
margin: 0;
}
}
}
.chart-preview-container {
position: relative;
display: flex;
flex-direction: row;
gap: 1rem;
.ant-card {
flex: 1;
}
}
.detection-method-container {
margin: 24px 0;
.ant-tabs-nav {
margin-bottom: 0;
.ant-tabs-tab {
padding: 12px 0;
}
}
.detection-method-description {
padding: 8px 0;
font-size: 12px;
}
}
.info-help-btns {
display: grid;
grid-template-columns: auto auto;

View File

@ -222,7 +222,7 @@ function QuerySection({
};
return (
<>
<StepHeading> {t('alert_form_step1')}</StepHeading>
<StepHeading> {t('alert_form_step2')}</StepHeading>
<FormContainer>
<div>{renderTabs(alertType)}</div>
{renderQuerySection(currentTab)}

View File

@ -0,0 +1,6 @@
.rule-definition {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}

View File

@ -1,3 +1,5 @@
import './RuleOptions.styles.scss';
import {
Checkbox,
Collapse,
@ -18,14 +20,17 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useTranslation } from 'react-i18next';
import {
AlertDef,
defaultAlgorithm,
defaultCompareOp,
defaultEvalWindow,
defaultFrequency,
defaultMatchType,
defaultSeasonality,
} from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import { popupContainer } from 'utils/selectPopupContainer';
import { AlertDetectionTypes } from '.';
import {
FormContainer,
InlineSelect,
@ -43,6 +48,8 @@ function RuleOptions({
const { t } = useTranslation('alerts');
const { currentQuery } = useQueryBuilder();
const { ruleType } = alertDef;
const handleMatchOptChange = (value: string | unknown): void => {
const m = (value as string) || alertDef.condition?.matchType;
setAlertDef({
@ -86,8 +93,19 @@ function RuleOptions({
>
<Select.Option value="1">{t('option_above')}</Select.Option>
<Select.Option value="2">{t('option_below')}</Select.Option>
<Select.Option value="3">{t('option_equal')}</Select.Option>
<Select.Option value="4">{t('option_notequal')}</Select.Option>
{/* hide equal and not eqaul in case of analmoy based alert */}
{ruleType !== 'anomaly_rule' && (
<>
<Select.Option value="3">{t('option_equal')}</Select.Option>
<Select.Option value="4">{t('option_notequal')}</Select.Option>
</>
)}
{ruleType === 'anomaly_rule' && (
<Select.Option value="5">{t('option_above_below')}</Select.Option>
)}
</InlineSelect>
);
@ -101,9 +119,14 @@ function RuleOptions({
>
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
<Select.Option value="2">{t('option_allthetimes')}</Select.Option>
<Select.Option value="3">{t('option_onaverage')}</Select.Option>
<Select.Option value="4">{t('option_intotal')}</Select.Option>
<Select.Option value="5">{t('option_last')}</Select.Option>
{ruleType !== 'anomaly_rule' && (
<>
<Select.Option value="3">{t('option_onaverage')}</Select.Option>
<Select.Option value="4">{t('option_intotal')}</Select.Option>
<Select.Option value="5">{t('option_last')}</Select.Option>
</>
)}
</InlineSelect>
);
@ -115,6 +138,37 @@ function RuleOptions({
});
};
const onChangeAlgorithm = (value: string | unknown): void => {
const alg = (value as string) || alertDef.condition.algorithm;
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
algorithm: alg,
},
});
};
const onChangeSeasonality = (value: string | unknown): void => {
const seasonality = (value as string) || alertDef.condition.seasonality;
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
seasonality,
},
});
};
const onChangeDeviation = (value: number): void => {
const target = value || alertDef.condition.target || 3;
setAlertDef({
...alertDef,
condition: { ...alertDef.condition, target: Number(target) },
});
};
const renderEvalWindows = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
@ -146,6 +200,54 @@ function RuleOptions({
</InlineSelect>
);
const renderAlgorithms = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultAlgorithm}
style={{ minWidth: '120px' }}
value={alertDef.condition.algorithm}
onChange={onChangeAlgorithm}
>
<Select.Option value="standard">Standard</Select.Option>
</InlineSelect>
);
const renderDeviationOpts = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={3}
style={{ minWidth: '120px' }}
value={alertDef.condition.target}
onChange={(value: number | unknown): void => {
if (typeof value === 'number') {
onChangeDeviation(value);
}
}}
>
<Select.Option value={1}>1</Select.Option>
<Select.Option value={2}>2</Select.Option>
<Select.Option value={3}>3</Select.Option>
<Select.Option value={4}>4</Select.Option>
<Select.Option value={5}>5</Select.Option>
<Select.Option value={6}>6</Select.Option>
<Select.Option value={7}>7</Select.Option>
</InlineSelect>
);
const renderSeasonality = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
defaultValue={defaultSeasonality}
style={{ minWidth: '120px' }}
value={alertDef.condition.seasonality}
onChange={onChangeSeasonality}
>
<Select.Option value="hourly">Hourly</Select.Option>
<Select.Option value="daily">Daily</Select.Option>
<Select.Option value="weekly">Weekly</Select.Option>
</InlineSelect>
);
const renderThresholdRuleOpts = (): JSX.Element => (
<Form.Item>
<Typography.Text>
@ -216,6 +318,32 @@ function RuleOptions({
});
};
const renderAnomalyRuleOpts = (): JSX.Element => (
<Form.Item>
<Typography.Text className="rule-definition">
{t('text_condition1_anomaly')}
<InlineSelect
getPopupContainer={popupContainer}
allowClear
showSearch
options={queryOptions}
placeholder={t('selected_query_placeholder')}
value={alertDef.condition.selectedQueryName}
onChange={onChangeSelectedQueryName}
/>
{t('text_condition3')} {renderEvalWindows()}
<Typography.Text>is</Typography.Text>
{renderDeviationOpts()}
<Typography.Text>deviations</Typography.Text>
{renderCompareOps()}
<Typography.Text>the predicted data</Typography.Text>
{renderMatchOpts()}
using the {renderAlgorithms()} algorithm with {renderSeasonality()}{' '}
seasonality
</Typography.Text>
</Form.Item>
);
const renderFrequency = (): JSX.Element => (
<InlineSelect
getPopupContainer={popupContainer}
@ -245,36 +373,46 @@ function RuleOptions({
return (
<>
<StepHeading>{t('alert_form_step2')}</StepHeading>
<StepHeading>{t('alert_form_step3')}</StepHeading>
<FormContainer>
{queryCategory === EQueryType.PROM
? renderPromRuleOptions()
: renderThresholdRuleOpts()}
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
{queryCategory !== EQueryType.PROM &&
ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
<>{renderAnomalyRuleOpts()}</>
)}
{queryCategory !== EQueryType.PROM &&
ruleType === AlertDetectionTypes.THRESHOLD_ALERT &&
renderThresholdRuleOpts()}
<Space direction="vertical" size="large">
<Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'target']}>
<InputNumber
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
{queryCategory !== EQueryType.PROM &&
ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
<Space direction="horizontal" align="center">
<Form.Item noStyle name={['condition', 'target']}>
<InputNumber
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={onChange}
type="number"
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
<Form.Item noStyle>
<Select
getPopupContainer={popupContainer}
allowClear
showSearch
options={categorySelectOptions}
placeholder={t('field_unit')}
value={alertDef.condition.targetUnit}
onChange={onChangeAlertUnit}
/>
</Form.Item>
</Space>
)}
<Form.Item noStyle>
<Select
getPopupContainer={popupContainer}
allowClear
showSearch
options={categorySelectOptions}
placeholder={t('field_unit')}
value={alertDef.condition.targetUnit}
onChange={onChangeAlertUnit}
/>
</Form.Item>
</Space>
<Collapse>
<Collapse.Panel header={t('More options')} key="1">
<Space direction="vertical" size="large">

View File

@ -3,7 +3,6 @@ import './FormAlertRules.styles.scss';
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import {
Button,
Col,
FormInstance,
Modal,
SelectProps,
@ -13,8 +12,6 @@ import {
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { alertHelpMessage } from 'components/LaunchChatSupport/util';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
@ -26,17 +23,23 @@ import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import useFeatureFlag, {
MESSAGE,
useIsFeatureDisabled,
} from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEqual } from 'lodash-es';
import { BellDot, ExternalLink } from 'lucide-react';
import Tabs2 from 'periscope/components/Tabs2';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
@ -44,7 +47,11 @@ import {
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
Query,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
@ -56,13 +63,16 @@ import {
ActionButton,
ButtonContainer,
MainFormContainer,
PanelContainer,
StepContainer,
StyledLeftContainer,
StepHeading,
} from './styles';
import UserGuide from './UserGuide';
import { getSelectedQueryOptions } from './utils';
export enum AlertDetectionTypes {
THRESHOLD_ALERT = 'threshold_rule',
ANOMALY_DETECTION_ALERT = 'anomaly_rule',
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function FormAlertRules({
alertType,
@ -79,6 +89,8 @@ function FormAlertRules({
>((state) => state.globalTime);
const urlQuery = useUrlQuery();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
// In case of alert the panel types should always be "Graph" only
const panelType = PANEL_TYPES.TIME_SERIES;
@ -86,6 +98,7 @@ function FormAlertRules({
const {
currentQuery,
stagedQuery,
handleSetQueryData,
handleRunQuery,
handleSetConfig,
initialDataSource,
@ -108,6 +121,10 @@ function FormAlertRules({
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
useEffect(() => {
if (!isEqual(currentQuery.unit, yAxisUnit)) {
setYAxisUnit(currentQuery.unit || '');
@ -138,6 +155,89 @@ function FormAlertRules({
useShareBuilderUrl(sq);
const handleDetectionMethodChange = (value: string): void => {
setAlertDef((def) => ({
...def,
ruleType: value,
}));
logEvent(`Alert: Detection method changed`, {
detectionMethod: value,
});
setDetectionMethod(value);
};
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
const anomalyFunction = {
name: 'anomaly',
args: [],
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
};
const functions = data.functions || [];
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
// Add anomaly if not already present
if (!functions.some((func) => func.name === 'anomaly')) {
functions.push(anomalyFunction);
} else {
const anomalyFuncIndex = functions.findIndex(
(func) => func.name === 'anomaly',
);
if (anomalyFuncIndex !== -1) {
const anomalyFunc = {
...functions[anomalyFuncIndex],
namedArgs: { z_score_threshold: alertDef.condition.target || 3 },
};
functions.splice(anomalyFuncIndex, 1);
functions.push(anomalyFunc);
}
}
} else {
// Remove anomaly if present
const index = functions.findIndex((func) => func.name === 'anomaly');
if (index !== -1) {
functions.splice(index, 1);
}
}
return functions;
};
useEffect(() => {
const ruleType =
detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
? AlertDetectionTypes.ANOMALY_DETECTION_ALERT
: AlertDetectionTypes.THRESHOLD_ALERT;
queryParams.set(QueryParams.ruleType, ruleType);
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
history.replace(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [detectionMethod]);
const updateFunctionsBasedOnAlertType = (): void => {
for (let index = 0; index < currentQuery.builder.queryData.length; index++) {
const queryData = currentQuery.builder.queryData[index];
const updatedFunctions = updateFunctions(queryData);
queryData.functions = updatedFunctions;
handleSetQueryData(index, queryData);
}
};
useEffect(() => {
updateFunctionsBasedOnAlertType();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
detectionMethod,
alertDef.condition.target,
currentQuery.builder.queryData.length,
]);
useEffect(() => {
const broadcastToSpecificChannels =
(initialValue &&
@ -145,10 +245,22 @@ function FormAlertRules({
initialValue.preferredChannels.length > 0) ||
isNewRule;
let ruleType = AlertDetectionTypes.THRESHOLD_ALERT;
if (initialValue.ruleType) {
ruleType = initialValue.ruleType as AlertDetectionTypes;
} else if (alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
ruleType = AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
}
setAlertDef({
...initialValue,
broadcastToAll: !broadcastToSpecificChannels,
ruleType,
});
setDetectionMethod(ruleType);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValue, isNewRule]);
useEffect(() => {
@ -269,7 +381,11 @@ function FormAlertRules({
return false;
}
if (alertDef.condition?.target !== 0 && !alertDef.condition?.target) {
if (
alertDef.ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT &&
alertDef.condition?.target !== 0 &&
!alertDef.condition?.target
) {
notifications.error({
message: 'Error',
description: t('target_missing'),
@ -300,12 +416,15 @@ function FormAlertRules({
const postableAlert: AlertDef = {
...alertDef,
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
alertType,
alertType:
alertType === AlertTypes.ANOMALY_BASED_ALERT
? AlertTypes.METRICS_BASED_ALERT
: alertType,
source: window?.location.toString(),
ruleType:
currentQuery.queryType === EQueryType.PROM
? 'promql_rule'
: 'threshold_rule',
: alertDef.ruleType,
condition: {
...alertDef.condition,
compositeQuery: {
@ -322,6 +441,12 @@ function FormAlertRules({
},
},
};
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
postableAlert.condition.algorithm = alertDef.condition.algorithm;
postableAlert.condition.seasonality = alertDef.condition.seasonality;
}
return postableAlert;
};
@ -418,6 +543,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
alertId: postableAlert?.id,
alertName: postableAlert?.alert,
ruleType: postableAlert?.ruleType,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
@ -502,6 +628,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
status: statusResponse.status,
statusMessage: statusResponse.message,
ruleType: postableAlert.ruleType,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [t, isFormValid, memoizedPreparePostData, notifications]);
@ -585,63 +712,93 @@ function FormAlertRules({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function handleRedirection(option: AlertTypes): void {
let url = '';
switch (option) {
case AlertTypes.METRICS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.LOGS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.TRACES_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.EXCEPTIONS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
default:
break;
}
logEvent('Alert: Check example alert clicked', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
isNewRule: !ruleId || ruleId === 0,
ruleId,
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
}
const tabs = [
{
value: AlertDetectionTypes.THRESHOLD_ALERT,
label: 'Threshold Alert',
},
{
value: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
label: 'Anomaly Detection Alert',
isBeta: true,
},
];
const isAnomalyDetectionEnabled =
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
return (
<>
{Element}
<PanelContainer id="top">
<StyledLeftContainer flex="5 1 600px" md={18}>
<MainFormContainer
initialValues={initialValue}
layout="vertical"
form={formInstance}
className="main-container"
>
<div id="top">
<div className="overview-header">
<div className="alert-type-container">
{isNewRule && (
<Typography.Title level={5} className="alert-type-title">
<BellDot size={14} />
{alertDef.alertType === AlertTypes.ANOMALY_BASED_ALERT &&
'Anomaly Detection Alert'}
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
'Metrics Based Alert'}
{alertDef.alertType === AlertTypes.LOGS_BASED_ALERT &&
'Logs Based Alert'}
{alertDef.alertType === AlertTypes.TRACES_BASED_ALERT &&
'Traces Based Alert'}
{alertDef.alertType === AlertTypes.EXCEPTIONS_BASED_ALERT &&
'Exceptions Based Alert'}
</Typography.Title>
)}
</div>
<Button className="periscope-btn" icon={<ExternalLink size={14} />}>
Alert Setup Guide
</Button>
</div>
<MainFormContainer
initialValues={initialValue}
layout="vertical"
form={formInstance}
className="main-container"
>
<div className="chart-preview-container">
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
renderQBChartPreview()}
{currentQuery.queryType === EQueryType.PROM &&
renderPromAndChQueryChartPreview()}
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
renderPromAndChQueryChartPreview()}
</div>
<StepContainer>
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</StepContainer>
<StepContainer>
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</StepContainer>
<div className="steps-container">
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
isAnomalyDetectionEnabled && (
<div className="detection-method-container">
<StepHeading> {t('alert_form_step1')}</StepHeading>
<Tabs2
key={detectionMethod}
tabs={tabs}
initialSelectedTab={detectionMethod || ''}
onSelectTab={handleDetectionMethodChange}
/>
<div className="detection-method-description">
{detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
? t('anomaly_detection_alert_desc')
: t('threshold_alert_desc')}
</div>
</div>
)}
<QuerySection
queryCategory={currentQuery.queryType}
@ -662,79 +819,49 @@ function FormAlertRules({
/>
{renderBasicInfo()}
<ButtonContainer>
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>
</Tooltip>
</div>
<ButtonContainer>
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
type="default"
onClick={onTestRuleHandler}
>
{' '}
{t('button_testrule')}
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>
<ActionButton
disabled={loading || false}
type="default"
onClick={onCancelHandler}
>
{ruleId === 0 && t('button_cancelchanges')}
{ruleId > 0 && t('button_discard')}
</ActionButton>
</ButtonContainer>
</MainFormContainer>
</StyledLeftContainer>
<Col flex="1 1 300px">
<UserGuide queryType={currentQuery.queryType} />
<div className="info-help-btns">
<Button
style={{ height: 32 }}
onClick={(): void =>
handleRedirection(alertDef?.alertType as AlertTypes)
</Tooltip>
<ActionButton
loading={loading || false}
disabled={
isAlertNameMissing ||
!isChannelConfigurationValid ||
queryStatus === 'error'
}
className="doc-redirection-btn"
type="default"
onClick={onTestRuleHandler}
>
Check an example alert
</Button>
<LaunchChatSupport
attributes={{
alert: alertDef?.alert,
alertType: alertDef?.alertType,
id: ruleId,
ruleType: alertDef?.ruleType,
state: (alertDef as any)?.state,
panelType,
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
}}
className="facing-issue-btn"
eventName="Alert: Facing Issues in alert"
buttonText="Need help with this alert?"
message={alertHelpMessage(alertDef, ruleId)}
onHoverText="Click here to get help with this alert"
/>
</div>
</Col>
</PanelContainer>
{' '}
{t('button_testrule')}
</ActionButton>
<ActionButton
disabled={loading || false}
type="default"
onClick={onCancelHandler}
>
{ruleId === 0 && t('button_cancelchanges')}
{ruleId > 0 && t('button_discard')}
</ActionButton>
</ButtonContainer>
</MainFormContainer>
</div>
</>
);
}

View File

@ -1,13 +1,9 @@
import { Button, Card, Col, Form, Input, Row, Select, Typography } from 'antd';
import { Button, Card, Col, Form, Input, Select, Typography } from 'antd';
import styled from 'styled-components';
const { TextArea } = Input;
const { Item } = Form;
export const PanelContainer = styled(Row)`
flex-wrap: nowrap;
`;
export const StyledLeftContainer = styled(Col)`
&&& {
margin-right: 1rem;

View File

@ -18,6 +18,7 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import useComponentPermission from 'hooks/useComponentPermission';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
@ -72,16 +73,18 @@ function WidgetHeader({
tableProcessedDataRef,
setSearchTerm,
}: IWidgetHeaderProps): JSX.Element | null {
const urlQuery = useUrlQuery();
const onEditHandler = useCallback((): void => {
const widgetId = widget.id;
history.push(
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
widget.panelTypes
}&${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(widget.query),
)}`,
urlQuery.set(QueryParams.widgetId, widgetId);
urlQuery.set(QueryParams.graphType, widget.panelTypes);
urlQuery.set(
QueryParams.compositeQuery,
encodeURIComponent(JSON.stringify(widget.query)),
);
}, [widget.id, widget.panelTypes, widget.query]);
const generatedUrl = `${window.location.pathname}/new?${urlQuery}`;
history.push(generatedUrl);
}, [urlQuery, widget.id, widget.panelTypes, widget.query]);
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');

View File

@ -97,13 +97,19 @@ function GridTableComponent({
const newColumnData = columns.map((e) => ({
...e,
render: (text: string): ReactNode => {
const isNumber = !Number.isNaN(Number(text));
render: (text: string, ...rest: any): ReactNode => {
let textForThreshold = text;
if (columnUnits && columnUnits?.[e.title as string]) {
textForThreshold = rest[0][`${e.title}_without_unit`];
}
const isNumber = !Number.isNaN(Number(textForThreshold));
if (thresholds && isNumber) {
const { hasMultipleMatches, threshold } = findMatchingThreshold(
thresholds,
e.title as string,
Number(text),
Number(textForThreshold),
columnUnits?.[e.title as string],
);
const idx = thresholds.findIndex(

View File

@ -1,5 +1,6 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { ColumnsType, ColumnType } from 'antd/es/table';
import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
@ -30,10 +31,39 @@ function evaluateCondition(
}
}
/**
* Evaluates whether a given value meets a specified threshold condition.
* It first converts the value to the appropriate unit if a threshold unit is provided,
* and then checks the condition using the specified operator.
*
* @param value - The value to be evaluated.
* @param thresholdValue - The threshold value to compare against.
* @param thresholdOperator - The operator used for comparison (e.g., '>', '<', '==').
* @param thresholdUnit - The unit to which the value should be converted.
* @param columnUnit - The current unit of the value.
* @returns A boolean indicating whether the value meets the threshold condition.
*/
function evaluateThresholdWithConvertedValue(
value: number,
thresholdValue: number,
thresholdOperator?: string,
thresholdUnit?: string,
columnUnit?: string,
): boolean {
const convertedValue = convertUnit(value, columnUnit, thresholdUnit);
if (convertedValue) {
return evaluateCondition(thresholdOperator, convertedValue, thresholdValue);
}
return evaluateCondition(thresholdOperator, value, thresholdValue);
}
export function findMatchingThreshold(
thresholds: ThresholdProps[],
label: string,
value: number,
columnUnit?: string,
): {
threshold: ThresholdProps;
hasMultipleMatches: boolean;
@ -45,10 +75,12 @@ export function findMatchingThreshold(
if (
threshold.thresholdValue !== undefined &&
threshold.thresholdTableOptions === label &&
evaluateCondition(
threshold.thresholdOperator,
evaluateThresholdWithConvertedValue(
value,
threshold.thresholdValue,
threshold?.thresholdValue,
threshold.thresholdOperator,
threshold.thresholdUnit,
columnUnit,
)
) {
matchingThresholds.push(threshold);

View File

@ -5,7 +5,6 @@ import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import logEvent from 'api/common/logEvent';
import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/LaunchChatSupport/util';
import {
DynamicColumnsKey,
TableDataSource,
@ -397,15 +396,6 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
dynamicColumns={dynamicColumns}
onChange={handleChange}
pagination={paginationConfig}
facingIssueBtn={{
attributes: {
screen: 'Alert list page',
},
eventName: 'Alert: Facing Issues in alert',
buttonText: 'Facing issues with alerts?',
message: listAlertMessage,
onHoverText: 'Click here to get help with alerts',
}}
/>
</>
);

View File

@ -5,6 +5,17 @@
justify-content: center;
width: 100%;
// overridding the request integration style to fix the spacing for dashboard list
.request-entity-container {
margin-bottom: 16px !important;
margin-top: 0 !important;
}
.integrations-content {
max-width: 100% !important;
width: 100% !important;
}
.dashboards-list-view-content {
width: calc(100% - 30px);
max-width: 836px;

View File

@ -25,8 +25,6 @@ import logEvent from 'api/common/logEvent';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardListMessage } from 'components/LaunchChatSupport/util';
import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes';
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
@ -79,6 +77,7 @@ import { isCloudUser } from 'utils/app';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
import { RequestDashboardBtn } from './RequestDashboardBtn';
import { DeleteButton } from './TableComponents/DeleteButton';
import {
DashboardDynamicColumns,
@ -693,17 +692,14 @@ function DashboardsList(): JSX.Element {
<Typography.Text className="subtitle">
Create and manage dashboards for your workspace.
</Typography.Text>
<LaunchChatSupport
attributes={{
screen: 'Dashboard list page',
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardListMessage}
buttonText="Need help with dashboards?"
onHoverText="Click here to get help with dashboards"
intercomMessageDisabled
/>
</Flex>
{isCloudUser() && (
<div className="integrations-container">
<div className="integrations-content">
<RequestDashboardBtn />
</div>
</div>
)}
</div>
{isDashboardListLoading ||

View File

@ -82,6 +82,12 @@ function ImportJSON({
const dashboardData = JSON.parse(editorValue) as DashboardData;
// Add validation for uuid
if (dashboardData.uuid !== undefined && dashboardData.uuid.trim() === '') {
// silently remove uuid if it is empty
delete dashboardData.uuid;
}
if (dashboardData?.layout) {
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
} else {
@ -123,11 +129,14 @@ function ImportJSON({
});
}
setDashboardCreating(false);
} catch {
} catch (error) {
setDashboardCreating(false);
setIsFeatureAlert(false);
setIsCreateDashboardError(true);
notifications.error({
message: error instanceof Error ? error.message : t('error_loading_json'),
});
}
};

View File

@ -0,0 +1,95 @@
import '../../pages/Integrations/Integrations.styles.scss';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Input, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications';
import { Check } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
export function RequestDashboardBtn(): JSX.Element {
const [
isSubmittingRequestForDashboard,
setIsSubmittingRequestForDashboard,
] = useState(false);
const [requestedDashboardName, setRequestedDashboardName] = useState('');
const { notifications } = useNotifications();
const { t } = useTranslation(['common']);
const handleRequestDashboardSubmit = async (): Promise<void> => {
try {
setIsSubmittingRequestForDashboard(true);
const response = await logEvent('Dashboard Requested', {
screen: 'Dashboard list page',
dashboard: requestedDashboardName,
});
if (response.statusCode === 200) {
notifications.success({
message: 'Dashboard Request Submitted',
});
setIsSubmittingRequestForDashboard(false);
} else {
notifications.error({
message:
response.error ||
t('something_went_wrong', {
ns: 'common',
}),
});
setIsSubmittingRequestForDashboard(false);
}
} catch (error) {
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
setIsSubmittingRequestForDashboard(false);
}
};
return (
<div className="request-entity-container">
<Typography.Text>
Can&apos;t find the dashboard you need? Request a new Dashboard.
</Typography.Text>
<div className="form-section">
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="Enter dashboard name..."
style={{ width: 300, marginBottom: 0 }}
value={requestedDashboardName}
onChange={(e): void => setRequestedDashboardName(e.target.value)}
/>
<Button
className="periscope-btn primary"
icon={
isSubmittingRequestForDashboard ? (
<LoadingOutlined />
) : (
<Check size={12} />
)
}
type="primary"
onClick={handleRequestDashboardSubmit}
disabled={
isSubmittingRequestForDashboard ||
!requestedDashboardName ||
requestedDashboardName?.trim().length === 0
}
>
Submit
</Button>
</Space.Compact>
</div>
</div>
);
}

View File

@ -2,3 +2,25 @@
cursor: pointer;
position: relative;
}
.table-row-backdrop {
&.INFO {
background-color: var(--bg-robin-500) 10;
}
&.WARNING,
&.WARN {
background-color: var(--bg-amber-500) 10;
}
&.ERROR {
background-color: var(--bg-cherry-500) 10;
}
&.TRACE {
background-color: var(--bg-forest-400) 10;
}
&.DEBUG {
background-color: var(--bg-aqua-500) 10;
}
&.FATAL {
background-color: var(--bg-sakura-500) 10;
}
}

View File

@ -1,5 +1,6 @@
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@ -21,6 +22,11 @@ import { TableHeaderCellStyled, TableRowStyled } from './styles';
import TableRow from './TableRow';
import { InfinityTableProps } from './types';
interface CustomTableRowProps {
activeContextLogId: string;
activeLogId: string;
}
// eslint-disable-next-line react/function-component-definition
const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
children,
@ -31,10 +37,17 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
const isDarkMode = useIsDarkMode();
const logType = getLogIndicatorType(props.item);
return (
<TableRowStyled
$isDarkMode={isDarkMode}
$isActiveLog={isHighlighted}
$isActiveLog={
isHighlighted ||
(context as CustomTableRowProps).activeContextLogId === props.item.id ||
(context as CustomTableRowProps).activeLogId === props.item.id
}
$logType={logType}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
@ -66,8 +79,6 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
...tableViewProps,
onClickExpand: onSetActiveLog,
onOpenLogsContext: handleSetActiveContextLog,
activeLog,
activeContextLog,
});
const { draggedColumns, onDragColumns } = useDragColumns<
@ -153,7 +164,14 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
// TODO: fix it in the future
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
TableRow: CustomTableRow,
TableRow: (props): any =>
CustomTableRow({
...props,
context: {
activeContextLogId: activeContextLog?.id,
activeLogId: activeLog?.id,
},
} as any),
}}
itemContent={itemContent}
fixedHeaderContent={tableHeader}

View File

@ -1,5 +1,4 @@
/* eslint-disable no-nested-ternary */
import { Color } from '@signozhq/design-tokens';
import { themeColors } from 'constants/theme';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
@ -37,13 +36,12 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
export const TableRowStyled = styled.tr<{
$isActiveLog: boolean;
$isDarkMode: boolean;
$logType: string;
}>`
td {
${({ $isActiveLog, $isDarkMode }): string =>
${({ $isActiveLog, $isDarkMode, $logType }): string =>
$isActiveLog
? `background-color: ${
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
} !important`
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
: ''};
}

View File

@ -12,8 +12,6 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { dashboardHelpMessage } from 'components/LaunchChatSupport/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
@ -47,7 +45,11 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
import {
Dashboard,
DashboardData,
IDashboardVariable,
} from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@ -63,6 +65,30 @@ interface DashboardDescriptionProps {
handle: FullScreenHandle;
}
function sanitizeDashboardData(
selectedData: DashboardData,
): Omit<DashboardData, 'uuid'> {
if (!selectedData?.variables) {
const { uuid, ...rest } = selectedData;
return rest;
}
const updatedVariables = Object.entries(selectedData.variables).reduce(
(acc, [key, value]) => {
const { selectedValue, ...rest } = value;
acc[key] = rest;
return acc;
},
{} as Record<string, IDashboardVariable>,
);
const { uuid, ...restData } = selectedData;
return {
...restData,
variables: updatedVariables,
};
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { handle } = props;
@ -328,18 +354,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
{isDashboardLocked && <LockKeyhole size={14} />}
</div>
<div className="right-section">
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: updatedTitle,
screen: 'Dashboard Details',
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
buttonText="Need help with this dashboard?"
onHoverText="Click here to get help with dashboard"
intercomMessageDisabled
/>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
<Popover
open={isDashboardSettingsOpen}
@ -407,7 +421,10 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
type="text"
icon={<FileJson size={14} />}
onClick={(): void => {
downloadObjectAsJson(selectedData, selectedData.title);
downloadObjectAsJson(
sanitizeDashboardData(selectedData),
selectedData.title,
);
setIsDashbordSettingsOpen(false);
}}
>
@ -417,7 +434,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
type="text"
icon={<ClipboardCopy size={14} />}
onClick={(): void => {
setCopy(JSON.stringify(selectedData, null, 2));
setCopy(
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
);
setIsDashbordSettingsOpen(false);
}}
>

View File

@ -257,8 +257,7 @@ function VariableItem({
if (variableData.name) {
if (
value === ALL_SELECT_VALUE ||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
(Array.isArray(value) && value.length === 0)
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
) {
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
@ -324,10 +323,6 @@ function VariableItem({
Array.isArray(selectedValueStringified) &&
selectedValueStringified.includes(option.toString())
) {
if (newSelectedValue.length === 0) {
handleChange(ALL_SELECT_VALUE);
return;
}
if (newSelectedValue.length === 1) {
handleChange(newSelectedValue[0].toString());
return;

View File

@ -4,7 +4,6 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
@ -235,21 +234,6 @@ function QuerySection({
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<LaunchChatSupport
attributes={{
uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title,
screen: 'Dashboard widget',
panelType: selectedGraph,
widgetId: query.id,
queryType: currentQuery.queryType,
}}
eventName="Dashboard: Facing Issues in dashboard"
buttonText="Need help with this chart?"
// message={chartHelpMessage(selectedDashboard, graphType)}
onHoverText="Click here to get help with this dashboard widget"
intercomMessageDisabled
/>
<TextToolTip
text="This will temporarily save the current query and graph state. This will persist across tab change"
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"

View File

@ -297,6 +297,16 @@
box-shadow: none;
}
}
.invalid-unit {
color: var(--bg-vanilla-400);
font-family: 'Giest Mono';
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.48px;
}
}
.threshold-card-container:hover {

View File

@ -3,17 +3,18 @@ import './Threshold.styles.scss';
import { Button, Input, InputNumber, Select, Space, Typography } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { unitOptions } from 'container/NewWidget/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Check, Pencil, Trash2, X } from 'lucide-react';
import { useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { useDrag, useDrop, XYCoord } from 'react-dnd';
import {
operatorOptions,
panelTypeVsDragAndDrop,
showAsOptions,
unitOptions,
} from '../constants';
import { convertUnit } from '../dataFormatCategories';
import ColorSelector from './ColorSelector';
import CustomColor from './CustomColor';
import ShowCaseValue from './ShowCaseValue';
@ -40,6 +41,7 @@ function Threshold({
thresholdLabel = '',
tableOptions,
thresholdTableOptions = '',
columnUnits,
}: ThresholdProps): JSX.Element {
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
const [operator, setOperator] = useState<string | number>(
@ -192,6 +194,13 @@ function Threshold({
const allowDragAndDrop = panelTypeVsDragAndDrop[selectedGraph];
const isInvalidUnitComparison = useMemo(
() =>
unit !== 'none' &&
convertUnit(value, unit, columnUnits?.[tableSelectedOption]) === null,
[unit, value, columnUnits, tableSelectedOption],
);
return (
<div
ref={allowDragAndDrop ? ref : null}
@ -303,7 +312,7 @@ function Threshold({
{isEditMode ? (
<Select
defaultValue={unit}
options={unitOptions}
options={unitOptions(columnUnits?.[tableSelectedOption] || '')}
onChange={handleUnitChange}
showSearch
className="unit-selection"
@ -339,6 +348,12 @@ function Threshold({
</>
)}
</div>
{isInvalidUnitComparison && (
<Typography.Text className="invalid-unit">
Threshold unit ({unit}) is not valid in comparison with the column unit (
{columnUnits?.[tableSelectedOption] || 'none'})
</Typography.Text>
)}
{isEditMode && (
<div className="threshold-action-button">
<Button

View File

@ -3,14 +3,12 @@
import './ThresholdSelector.styles.scss';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { Events } from 'constants/events';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Antenna, Plus } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { eventEmitter } from 'utils/getEventEmitter';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import Threshold from './Threshold';
@ -21,22 +19,23 @@ function ThresholdSelector({
setThresholds,
yAxisUnit,
selectedGraph,
columnUnits,
}: ThresholdSelectorProps): JSX.Element {
const [tableOptions, setTableOptions] = useState<
Array<{ value: string; label: string }>
>([]);
useEffect(() => {
eventEmitter.on(
Events.TABLE_COLUMNS_DATA,
(data: { columns: ColumnsType<RowData>; dataSource: RowData[] }) => {
const newTableOptions = data.columns.map((e) => ({
value: e.title as string,
label: e.title as string,
}));
setTableOptions([...newTableOptions]);
},
);
}, []);
const { currentQuery } = useQueryBuilder();
function getAggregateColumnsNamesAndLabels(): string[] {
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
const queries = currentQuery.builder.queryData.map((q) => q.queryName);
const formulas = currentQuery.builder.queryFormulas.map((q) => q.queryName);
return [...queries, ...formulas];
}
if (currentQuery.queryType === EQueryType.CLICKHOUSE) {
return currentQuery.clickhouse_sql.map((q) => q.name);
}
return currentQuery.promql.map((q) => q.name);
}
const aggregationQueries = getAggregateColumnsNamesAndLabels();
const moveThreshold = useCallback(
(dragIndex: number, hoverIndex: number) => {
@ -66,7 +65,7 @@ function ThresholdSelector({
moveThreshold,
keyIndex: thresholds.length,
selectedGraph,
thresholdTableOptions: tableOptions[0]?.value || '',
thresholdTableOptions: aggregationQueries[0] || '',
},
...thresholds,
]);
@ -105,8 +104,12 @@ function ThresholdSelector({
moveThreshold={moveThreshold}
selectedGraph={selectedGraph}
thresholdLabel={threshold.thresholdLabel}
tableOptions={tableOptions}
tableOptions={aggregationQueries.map((query) => ({
value: query,
label: query,
}))}
thresholdTableOptions={threshold.thresholdTableOptions}
columnUnits={columnUnits}
/>
))}
</div>

View File

@ -1,5 +1,6 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Dispatch, ReactNode, SetStateAction } from 'react';
import { ColumnUnit } from 'types/api/dashboard/getAll';
export type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
@ -19,6 +20,7 @@ export type ThresholdProps = {
moveThreshold: (dragIndex: number, hoverIndex: number) => void;
selectedGraph: PANEL_TYPES;
tableOptions?: Array<{ value: string; label: string }>;
columnUnits?: ColumnUnit;
};
export type ShowCaseValueProps = {
@ -36,4 +38,5 @@ export type ThresholdSelectorProps = {
thresholds: ThresholdProps[];
setThresholds: Dispatch<SetStateAction<ThresholdProps[]>>;
selectedGraph: PANEL_TYPES;
columnUnits: ColumnUnit;
};

View File

@ -1,8 +1,5 @@
import { DefaultOptionType } from 'antd/es/select';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
import { getCategorySelectOptionByName } from './alertFomatCategories';
export const operatorOptions: DefaultOptionType[] = [
{ value: '>', label: '>' },
@ -11,11 +8,6 @@ export const operatorOptions: DefaultOptionType[] = [
{ value: '<=', label: '<=' },
];
export const unitOptions = categoryToSupport.map((category) => ({
label: category,
options: getCategorySelectOptionByName(category),
}));
export const showAsOptions: DefaultOptionType[] = [
{ value: 'Text', label: 'Text' },
{ value: 'Background', label: 'Background' },

View File

@ -438,3 +438,168 @@ export const dataTypeCategories: DataTypeCategories = [
export const flattenedCategories = flattenDeep(
dataTypeCategories.map((category) => category.formats),
);
type ConversionFactors = {
[key: string]: {
[key: string]: number | null;
};
};
// Object containing conversion factors for various categories and formats
const conversionFactors: ConversionFactors = {
[CategoryNames.Time]: {
[TimeFormats.Hertz]: 1,
[TimeFormats.Nanoseconds]: 1e-9,
[TimeFormats.Microseconds]: 1e-6,
[TimeFormats.Milliseconds]: 1e-3,
[TimeFormats.Seconds]: 1,
[TimeFormats.Minutes]: 60,
[TimeFormats.Hours]: 3600,
[TimeFormats.Days]: 86400,
[TimeFormats.DurationMs]: 1e-3,
[TimeFormats.DurationS]: 1,
[TimeFormats.DurationHms]: null, // Requires special handling
[TimeFormats.DurationDhms]: null, // Requires special handling
[TimeFormats.Timeticks]: null, // Requires special handling
[TimeFormats.ClockMs]: 1e-3,
[TimeFormats.ClockS]: 1,
},
[CategoryNames.Throughput]: {
[ThroughputFormats.CountsPerSec]: 1,
[ThroughputFormats.OpsPerSec]: 1,
[ThroughputFormats.RequestsPerSec]: 1,
[ThroughputFormats.ReadsPerSec]: 1,
[ThroughputFormats.WritesPerSec]: 1,
[ThroughputFormats.IOOpsPerSec]: 1,
[ThroughputFormats.CountsPerMin]: 1 / 60,
[ThroughputFormats.OpsPerMin]: 1 / 60,
[ThroughputFormats.ReadsPerMin]: 1 / 60,
[ThroughputFormats.WritesPerMin]: 1 / 60,
},
[CategoryNames.Data]: {
[DataFormats.BytesIEC]: 1,
[DataFormats.BytesSI]: 1,
[DataFormats.BitsIEC]: 0.125,
[DataFormats.BitsSI]: 0.125,
[DataFormats.KibiBytes]: 1024,
[DataFormats.KiloBytes]: 1000,
[DataFormats.MebiBytes]: 1048576,
[DataFormats.MegaBytes]: 1000000,
[DataFormats.GibiBytes]: 1073741824,
[DataFormats.GigaBytes]: 1000000000,
[DataFormats.TebiBytes]: 1099511627776,
[DataFormats.TeraBytes]: 1000000000000,
[DataFormats.PebiBytes]: 1125899906842624,
[DataFormats.PetaBytes]: 1000000000000000,
},
[CategoryNames.DataRate]: {
[DataRateFormats.PacketsPerSec]: null, // Cannot convert directly to other data rates
[DataRateFormats.BytesPerSecIEC]: 1,
[DataRateFormats.BytesPerSecSI]: 1,
[DataRateFormats.BitsPerSecIEC]: 0.125,
[DataRateFormats.BitsPerSecSI]: 0.125,
[DataRateFormats.KibiBytesPerSec]: 1024,
[DataRateFormats.KibiBitsPerSec]: 128,
[DataRateFormats.KiloBytesPerSec]: 1000,
[DataRateFormats.KiloBitsPerSec]: 125,
[DataRateFormats.MebiBytesPerSec]: 1048576,
[DataRateFormats.MebiBitsPerSec]: 131072,
[DataRateFormats.MegaBytesPerSec]: 1000000,
[DataRateFormats.MegaBitsPerSec]: 125000,
[DataRateFormats.GibiBytesPerSec]: 1073741824,
[DataRateFormats.GibiBitsPerSec]: 134217728,
[DataRateFormats.GigaBytesPerSec]: 1000000000,
[DataRateFormats.GigaBitsPerSec]: 125000000,
[DataRateFormats.TebiBytesPerSec]: 1099511627776,
[DataRateFormats.TebiBitsPerSec]: 137438953472,
[DataRateFormats.TeraBytesPerSec]: 1000000000000,
[DataRateFormats.TeraBitsPerSec]: 125000000000,
[DataRateFormats.PebiBytesPerSec]: 1125899906842624,
[DataRateFormats.PebiBitsPerSec]: 140737488355328,
[DataRateFormats.PetaBytesPerSec]: 1000000000000000,
[DataRateFormats.PetaBitsPerSec]: 125000000000000,
},
[CategoryNames.Miscellaneous]: {
[MiscellaneousFormats.None]: null,
[MiscellaneousFormats.String]: null,
[MiscellaneousFormats.Short]: null,
[MiscellaneousFormats.Percent]: 1,
[MiscellaneousFormats.PercentUnit]: 100,
[MiscellaneousFormats.Humidity]: 1,
[MiscellaneousFormats.Decibel]: null,
[MiscellaneousFormats.Hexadecimal0x]: null,
[MiscellaneousFormats.Hexadecimal]: null,
[MiscellaneousFormats.ScientificNotation]: null,
[MiscellaneousFormats.LocaleFormat]: null,
[MiscellaneousFormats.Pixels]: null,
},
[CategoryNames.Boolean]: {
[BooleanFormats.TRUE_FALSE]: null, // Not convertible
[BooleanFormats.YES_NO]: null, // Not convertible
[BooleanFormats.ON_OFF]: null, // Not convertible
},
};
// Function to get the conversion factor between two units in a specific category
function getConversionFactor(
fromUnit: string,
toUnit: string,
category: CategoryNames,
): number | null {
// Retrieves the conversion factors for the specified category
const categoryFactors = conversionFactors[category];
if (!categoryFactors) {
return null; // Returns null if the category does not exist
}
const fromFactor = categoryFactors[fromUnit];
const toFactor = categoryFactors[toUnit];
if (
fromFactor === undefined ||
toFactor === undefined ||
fromFactor === null ||
toFactor === null
) {
return null; // Returns null if either unit does not exist or is not convertible
}
return fromFactor / toFactor; // Returns the conversion factor ratio
}
// Function to convert a value from one unit to another
export function convertUnit(
value: number,
fromUnitId?: string,
toUnitId?: string,
): number | null {
let fromUnit: string | undefined;
let toUnit: string | undefined;
// Finds the category that contains the specified units and extracts fromUnit and toUnit using array methods
const category = dataTypeCategories.find((category) =>
category.formats.some((format) => {
if (format.id === fromUnitId) fromUnit = format.id;
if (format.id === toUnitId) toUnit = format.id;
return fromUnit && toUnit; // Break out early if both units are found
}),
);
if (!category || !fromUnit || !toUnit) return null; // Return null if category or units are not found
// Gets the conversion factor for the specified units
const conversionFactor = getConversionFactor(
fromUnit,
toUnit,
category.name as any,
);
if (conversionFactor === null) return null; // Return null if conversion is not possible
return value * conversionFactor;
}
// Function to get the category name for a given unit ID
export const getCategoryName = (unitId: string): CategoryNames | null => {
// Finds the category that contains the specified unit ID
const foundCategory = dataTypeCategories.find((category) =>
category.formats.some((format) => format.id === unitId),
);
return foundCategory ? (foundCategory.name as CategoryNames) : null;
};

View File

@ -311,6 +311,7 @@ function RightContainer({
setThresholds={setThresholds}
yAxisUnit={yAxisUnit}
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
</section>
)}

View File

@ -1,3 +1,4 @@
import { DefaultOptionType } from 'antd/es/select';
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
import {
initialQueryBuilderFormValuesMap,
@ -8,12 +9,19 @@ import {
listViewInitialTraceQuery,
PANEL_TYPES_INITIAL_QUERY,
} from 'container/NewDashboard/ComponentsSlider/constants';
import { cloneDeep, isEqual, set, unset } from 'lodash-es';
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
import { cloneDeep, isEmpty, isEqual, set, unset } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import {
dataTypeCategories,
getCategoryName,
} from './RightContainer/dataFormatCategories';
import { CategoryNames } from './RightContainer/types';
export const getIsQueryModified = (
currentQuery: Query,
stagedQuery: Query | null,
@ -529,3 +537,41 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
EQueryType.PROM,
],
};
/**
* Retrieves a list of category select options based on the provided category name.
* If the category is found, it maps the formats to an array of objects containing
* the label and value for each format.
*/
export const getCategorySelectOptionByName = (
name?: CategoryNames | string,
): DefaultOptionType[] =>
dataTypeCategories
.find((category) => category.name === name)
?.formats.map((format) => ({
label: format.name,
value: format.id,
})) || [];
/**
* Generates unit options based on the provided column unit.
* It first retrieves the category name associated with the column unit.
* If the category is empty, it maps all supported categories to their respective
* select options. If a valid category is found, it filters the supported categories
* to return only the options for the matched category.
*/
export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
const category = getCategoryName(columnUnit);
if (isEmpty(category)) {
return categoryToSupport.map((category) => ({
label: category,
options: getCategorySelectOptionByName(category),
}));
}
return categoryToSupport
.filter((supportedCategory) => supportedCategory === category)
.map((filteredCategory) => ({
label: filteredCategory,
options: getCategorySelectOptionByName(filteredCategory),
}));
};

View File

@ -60,7 +60,7 @@ This is a **sample cURL request** which can be used as a template:
&nbsp;
```bash
curl --location 'https://ingest.{{REGION}}.signoz.cloud:443/logs/json/' \
curl --location 'https://ingest.{{REGION}}.signoz.cloud:443/logs/json' \
--header 'Content-Type: application/json' \
--header 'signoz-access-token: {{SIGNOZ_INGESTION_KEY}}' \
--data '[

View File

@ -13,7 +13,7 @@ import {
IBuilderQuery,
QueryFunctionProps,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
interface FunctionProps {
query: IBuilderQuery;
@ -33,7 +33,7 @@ export default function Function({
handleDeleteFunction,
}: FunctionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { showInput } = queryFunctionsTypesConfig[funcData.name];
const { showInput, disabled } = queryFunctionsTypesConfig[funcData.name];
let functionValue;
@ -57,11 +57,19 @@ export default function Function({
? logsQueryFunctionOptions
: metricQueryFunctionOptions;
const disableRemoveFunction = funcData.name === QueryFunctionsTypes.ANOMALY;
if (funcData.name === QueryFunctionsTypes.ANOMALY) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
return (
<Flex className="query-function">
<Select
className={cx('query-function-name-selector', showInput ? 'showInput' : '')}
value={funcData.name}
disabled={disabled}
style={{ minWidth: '100px' }}
onChange={(value): void => {
handleUpdateFunctionName(funcData, index, value);
@ -91,6 +99,7 @@ export default function Function({
<Button
className="periscope-btn query-function-delete-btn"
disabled={disableRemoveFunction}
onClick={(): void => {
handleDeleteFunction(funcData, index);
}}

View File

@ -13,6 +13,7 @@ import {
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
import Function from './Function';
import { toFloat64 } from './utils';
const defaultMetricFunctionStruct: QueryFunctionProps = {
name: QueryFunctionsTypes.CUTOFF_MIN,
@ -92,6 +93,8 @@ export default function QueryFunctions({
const isDarkMode = useIsDarkMode();
const hasAnomalyFunction = functions.some((func) => func.name === 'anomaly');
const handleAddNewFunction = (): void => {
const defaultFunctionStruct =
query.dataSource === DataSource.LOGS
@ -105,9 +108,22 @@ export default function QueryFunctions({
},
];
setFunctions(updatedFunctionsArr);
const functionsCopy = cloneDeep(updatedFunctionsArr);
onChange(updatedFunctionsArr);
const anomalyFuncIndex = functionsCopy.findIndex(
(func) => func.name === 'anomaly',
);
if (anomalyFuncIndex !== -1) {
const anomalyFunc = functionsCopy[anomalyFuncIndex];
functionsCopy.splice(anomalyFuncIndex, 1);
functionsCopy.push(anomalyFunc);
}
setFunctions(functionsCopy);
onChange(functionsCopy);
};
const handleDeleteFunction = (
@ -143,7 +159,13 @@ export default function QueryFunctions({
const updateFunctions = cloneDeep(functions);
if (updateFunctions && updateFunctions.length > 0 && updateFunctions[index]) {
updateFunctions[index].args = [value];
updateFunctions[index].args = [
// timeShift expects a float64 value, so we convert the string to a number
// For other functions, we keep the value as a string
updateFunctions[index].name === QueryFunctionsTypes.TIME_SHIFT
? toFloat64(value)
: value,
];
setFunctions(updateFunctions);
onChange(updateFunctions);
}
@ -181,7 +203,9 @@ export default function QueryFunctions({
<Tooltip
title={
functions && functions.length >= 3 ? (
'Functions are in early access. You can add a maximum of 3 function as of now.'
`Functions are in early access. You can add a maximum of ${
hasAnomalyFunction ? 2 : 3
} function as of now.`
) : (
<div style={{ textAlign: 'center' }}>
Add new function

View File

@ -0,0 +1,7 @@
export const toFloat64 = (value: string): number => {
const parsed = parseFloat(value);
if (!Number.isFinite(parsed)) {
console.error(`Invalid value for timeshift. value: ${value}`);
}
return parsed;
};

View File

@ -59,9 +59,10 @@ export const useOrderByFilter = ({
];
}, [searchText]);
const selectedValue = useMemo(() => transformToOrderByStringValues(query), [
query,
]);
const selectedValue = useMemo(
() => transformToOrderByStringValues(query, entityVersion),
[query, entityVersion],
);
const generateOptions = useCallback(
(options: IOption[]): IOption[] => {

View File

@ -13,11 +13,14 @@ export const orderByValueDelimiter = '|';
export const transformToOrderByStringValues = (
query: IBuilderQuery,
entityVersion?: string,
): IOption[] => {
const prepareSelectedValue: IOption[] = query.orderBy.map((item) => {
if (item.columnName === SIGNOZ_VALUE) {
return {
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${item.order}`,
label: `${
entityVersion === 'v4' ? query.spaceAggregation : query.aggregateOperator
}(${query.aggregateAttribute.key}) ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
}

View File

@ -1,4 +1,6 @@
/* eslint-disable no-nested-ternary */
import './QueryBuilderSearchV2.styles.scss';
import { Typography } from 'antd';
import {
ArrowDown,

View File

@ -319,7 +319,7 @@ function QueryBuilderSearchV2(
value: '',
}));
setCurrentState(DropdownState.OPERATOR);
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
setSearchValue(`${(parsedValue as BaseAutocompleteData)?.key} `);
}
} else if (currentState === DropdownState.OPERATOR) {
if (isEmpty(value) && currentFilterItem?.key?.key) {
@ -360,7 +360,7 @@ function QueryBuilderSearchV2(
value: '',
}));
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
setSearchValue(`${currentFilterItem?.key?.key} ${value}`);
setSearchValue(`${currentFilterItem?.key?.key} ${value} `);
}
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
const operatorType =
@ -512,11 +512,6 @@ function QueryBuilderSearchV2(
// this useEffect takes care of tokenisation based on the search state
useEffect(() => {
// if we are still fetching the suggestions then return as we won't know the type / data-type etc for the attribute key
if (isFetchingSuggestions) {
return;
}
// if there is no search value reset to the default state
if (!searchValue) {
setCurrentFilterItem(undefined);
@ -766,6 +761,7 @@ function QueryBuilderSearchV2(
suggestionsData?.payload?.attributes,
]);
// keep the query in sync with the selected tags in logs explorer page
useEffect(() => {
const filterTags: IBuilderQuery['filters'] = {
op: 'AND',
@ -788,16 +784,14 @@ function QueryBuilderSearchV2(
if (!isEqual(query.filters, filterTags)) {
onChange(filterTags);
setTags(
filterTags.items.map((tag) => ({
...tag,
op: getOperatorFromValue(tag.op),
})) as ITag[],
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tags]);
// keep the use effects pure!
// if the tags lacks the ID then the above use effect will add it to query
// and then the below use effect will take care of adding it to the tags.
// keep the tags in sycn with current query.
useEffect(() => {
// convert the query and tags to same format before comparison
if (!isEqual(getInitTags(query), tags)) {

View File

@ -98,7 +98,7 @@ function NoFilterTable({
return (
<ResizeTable
columns={columns}
rowKey="startsAt"
rowKey={(record): string => `${record.startsAt}-${record.fingerprint}`}
dataSource={filteredAlerts}
/>
);

View File

@ -1,7 +1,8 @@
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import useUrlQuery from './useUrlQuery';
const useUrlQueryData = <T>(
queryKey: string,
defaultData?: T,
@ -10,7 +11,7 @@ const useUrlQueryData = <T>(
const location = useLocation();
const urlQuery = useUrlQuery();
const query = useMemo(() => urlQuery.get(queryKey), [queryKey, urlQuery]);
const query = useMemo(() => urlQuery.get(queryKey), [urlQuery, queryKey]);
const queryData: T = useMemo(() => (query ? JSON.parse(query) : defaultData), [
query,
@ -21,11 +22,19 @@ const useUrlQueryData = <T>(
(newQueryData: T): void => {
const newQuery = JSON.stringify(newQueryData);
urlQuery.set(queryKey, newQuery);
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
// Create a new URLSearchParams object with the current URL's search params
// This ensures we're working with the most up-to-date URL state
const currentUrlQuery = new URLSearchParams(window.location.search);
// Update or add the specified query parameter with the new serialized data
currentUrlQuery.set(queryKey, newQuery);
// Construct the new URL by combining the current pathname with the updated query string
const generatedUrl = `${location.pathname}?${currentUrlQuery.toString()}`;
history.replace(generatedUrl);
},
[history, location, urlQuery, queryKey],
[history, location.pathname, queryKey],
);
return {

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