Compare commits
8 Commits
anomaly-de
...
anamoly-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a9a2e71be | ||
|
|
57e21c8a49 | ||
|
|
7f44421bac | ||
|
|
b5335a161d | ||
|
|
486798dc12 | ||
|
|
902380ef5d | ||
|
|
755e6de842 | ||
|
|
b9d8556f11 |
83
.github/workflows/docs.yml
vendored
83
.github/workflows/docs.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: "Update PR labels and Block PR until related docs are shipped for the feature"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
types: [opened, edited, labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docs_label_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR Title and Manage Labels
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const prTitle = context.payload.pull_request.title;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Fetch the current PR details to get labels
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const labels = pr.data.labels.map(label => label.name);
|
||||
|
||||
if (prTitle.startsWith('feat:')) {
|
||||
const hasDocsRequired = labels.includes('docs required');
|
||||
const hasDocsShipped = labels.includes('docs shipped');
|
||||
const hasDocsNotRequired = labels.includes('docs not required');
|
||||
|
||||
// If "docs not required" is present, skip the checks
|
||||
if (hasDocsNotRequired && !hasDocsRequired) {
|
||||
console.log("Skipping checks due to 'docs not required' label.");
|
||||
return; // Exit the script early
|
||||
}
|
||||
|
||||
// If "docs shipped" is present, remove "docs required" if it exists
|
||||
if (hasDocsShipped && hasDocsRequired) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
name: 'docs required'
|
||||
});
|
||||
console.log("Removed 'docs required' label.");
|
||||
}
|
||||
|
||||
// Add "docs required" label if neither "docs shipped" nor "docs required" are present
|
||||
if (!hasDocsRequired && !hasDocsShipped) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels: ['docs required']
|
||||
});
|
||||
console.log("Added 'docs required' label.");
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the updated labels after any changes
|
||||
const updatedPr = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
const updatedLabels = updatedPr.data.labels.map(label => label.name);
|
||||
const updatedHasDocsRequired = updatedLabels.includes('docs required');
|
||||
const updatedHasDocsShipped = updatedLabels.includes('docs shipped');
|
||||
|
||||
// Block PR if "docs required" is still present and "docs shipped" is missing
|
||||
if (updatedHasDocsRequired && !updatedHasDocsShipped) {
|
||||
core.setFailed("This PR requires documentation. Please remove the 'docs required' label and add the 'docs shipped' label to proceed.");
|
||||
}
|
||||
1
.github/workflows/staging-deployment.yaml
vendored
1
.github/workflows/staging-deployment.yaml
vendored
@@ -38,7 +38,6 @@ jobs:
|
||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
export OTELCOL_TAG="main"
|
||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||
export KAFKA_SPAN_EVAL="true"
|
||||
docker system prune --force
|
||||
docker pull signoz/signoz-otel-collector:main
|
||||
docker pull signoz/signoz-schema-migrator:main
|
||||
|
||||
13
Makefile
13
Makefile
@@ -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,4 +188,13 @@ check-no-ee-references:
|
||||
fi
|
||||
|
||||
test:
|
||||
go test ./pkg/query-service/...
|
||||
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/...
|
||||
|
||||
@@ -133,7 +133,7 @@ services:
|
||||
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:0.23.7
|
||||
image: signoz/alertmanager:0.23.5
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
command:
|
||||
@@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.56.0
|
||||
image: signoz/query-service:0.55.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.55.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.111.5
|
||||
image: signoz/signoz-otel-collector:0.102.10
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -214,6 +214,7 @@ 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
|
||||
@@ -237,7 +238,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.111.5
|
||||
image: signoz/signoz-schema-migrator:0.102.10
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -131,7 +131,8 @@ processors:
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/signoz_traces
|
||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
@@ -141,6 +142,7 @@ exporters:
|
||||
# logging: {}
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
timeout: 10s
|
||||
use_new_schema: true
|
||||
extensions:
|
||||
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
|
||||
alertmanager:
|
||||
container_name: signoz-alertmanager
|
||||
image: signoz/alertmanager:0.23.7
|
||||
image: signoz/alertmanager:0.23.5
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
depends_on:
|
||||
@@ -69,7 +69,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
|
||||
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.111.5
|
||||
image: signoz/signoz-otel-collector:0.102.10
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
|
||||
@@ -34,7 +34,7 @@ x-db-depend: &db-depend
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
otel-collector-migrator-sync:
|
||||
otel-collector-migrator:
|
||||
condition: service_completed_successfully
|
||||
# clickhouse-2:
|
||||
# condition: service_healthy
|
||||
@@ -147,7 +147,7 @@ services:
|
||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
|
||||
container_name: signoz-alertmanager
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
@@ -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.55.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.55.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -212,13 +212,11 @@ services:
|
||||
volumes:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator-sync:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.5}
|
||||
container_name: otel-migrator-sync
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "sync"
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
- "--up="
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
@@ -227,25 +225,9 @@ 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.111.5}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.10}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
@@ -262,6 +244,7 @@ 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
|
||||
@@ -279,7 +262,7 @@ services:
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
otel-collector-migrator-sync:
|
||||
otel-collector-migrator:
|
||||
condition: service_completed_successfully
|
||||
query-service:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -152,7 +152,7 @@ services:
|
||||
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
|
||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.5}
|
||||
container_name: signoz-alertmanager
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
@@ -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.55.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -191,7 +191,6 @@ services:
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test:
|
||||
@@ -208,7 +207,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.56.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.55.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -220,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.111.5}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.10}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -234,7 +233,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.5}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.10}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
@@ -251,6 +250,7 @@ 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
|
||||
|
||||
@@ -142,7 +142,8 @@ extensions:
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/signoz_traces
|
||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
@@ -151,6 +152,7 @@ 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: {}
|
||||
|
||||
@@ -9,15 +9,7 @@ import (
|
||||
|
||||
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
validPath := false
|
||||
for _, allowedPrefix := range gateway.AllowedPrefix {
|
||||
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
|
||||
validPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validPath {
|
||||
if !strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+gateway.AllowedPrefix) {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,11 +53,7 @@ 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 {
|
||||
// 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 {
|
||||
if query.DataSource != v3.DataSourceMetrics {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("all queries must have metric data source")}, nil)
|
||||
return
|
||||
}
|
||||
@@ -104,13 +100,6 @@ 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 {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
RoutePrefix string = "/api/gateway"
|
||||
AllowedPrefix []string = []string{"/v1/workspaces/me", "/v2/profiles/me", "/v2/deployments/me"}
|
||||
const (
|
||||
RoutePrefix string = "/api/gateway"
|
||||
AllowedPrefix string = "/v1/workspaces/me"
|
||||
)
|
||||
|
||||
type proxy struct {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
@@ -135,13 +134,6 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ProPlan = basemodel.FeatureSet{
|
||||
@@ -257,13 +249,6 @@ var ProPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var EnterprisePlan = basemodel.FeatureSet{
|
||||
@@ -393,11 +378,4 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@dnd-kit/core": "6.1.0",
|
||||
"@dnd-kit/modifiers": "7.0.0",
|
||||
"@dnd-kit/sortable": "8.0.0",
|
||||
"@grafana/data": "^11.2.3",
|
||||
"@grafana/data": "^9.5.2",
|
||||
"@mdx-js/loader": "2.3.0",
|
||||
"@mdx-js/react": "2.3.0",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
@@ -51,7 +51,7 @@
|
||||
"ansi-to-html": "0.7.2",
|
||||
"antd": "5.11.0",
|
||||
"antd-table-saveas-excel": "2.2.1",
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.7.4",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^29.6.4",
|
||||
"babel-loader": "9.1.3",
|
||||
@@ -76,7 +76,7 @@
|
||||
"fontfaceobserver": "2.3.0",
|
||||
"history": "4.10.1",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"http-proxy-middleware": "2.0.7",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
"i18next-http-backend": "^1.3.2",
|
||||
@@ -123,10 +123,10 @@
|
||||
"ts-node": "^10.2.1",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "^4.0.5",
|
||||
"uplot": "1.6.31",
|
||||
"uplot": "1.6.26",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "5.94.0",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||
"xstate": "^4.31.0"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB |
1
frontend/public/css/uPlot.min.css
vendored
1
frontend/public/css/uPlot.min.css
vendored
@@ -1 +0,0 @@
|
||||
.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.
@@ -4,7 +4,6 @@
|
||||
"SERVICE_METRICS": "SigNoz | Service Metrics",
|
||||
"SERVICE_MAP": "SigNoz | Service Map",
|
||||
"GET_STARTED": "SigNoz | Get Started",
|
||||
"ONBOARDING": "SigNoz | Get Started",
|
||||
"GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM",
|
||||
"GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs",
|
||||
"GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import getOrgUser from 'api/user/getOrgUser';
|
||||
import loginApi from 'api/user/login';
|
||||
import { Logout } from 'api/utils';
|
||||
import Spinner from 'components/Spinner';
|
||||
@@ -9,10 +8,8 @@ import ROUTES from 'constants/routes';
|
||||
import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { ReactChild, useEffect, useMemo, useState } from 'react';
|
||||
import { ReactChild, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { matchPath, Redirect, useLocation } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
@@ -20,7 +17,6 @@ import { AppState } from 'store/reducers';
|
||||
import { getInitialUserTokenRefreshToken } from 'store/utils';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
|
||||
import { Organization } from 'types/api/user/getOrganization';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { routePermission } from 'utils/permission';
|
||||
|
||||
@@ -35,19 +31,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
const location = useLocation();
|
||||
const { pathname } = location;
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const {
|
||||
org,
|
||||
orgPreferences,
|
||||
user,
|
||||
role,
|
||||
isUserFetching,
|
||||
isUserFetchingError,
|
||||
isLoggedIn: isLoggedInState,
|
||||
isFetchingOrgPreferences,
|
||||
} = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const mapRoutes = useMemo(
|
||||
() =>
|
||||
new Map(
|
||||
@@ -61,21 +44,18 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const isOnboardingComplete = useMemo(
|
||||
() =>
|
||||
orgPreferences?.find(
|
||||
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
|
||||
)?.value,
|
||||
[orgPreferences],
|
||||
);
|
||||
|
||||
const {
|
||||
data: licensesData,
|
||||
isFetching: isFetchingLicensesData,
|
||||
} = useLicense();
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
const {
|
||||
isUserFetching,
|
||||
isUserFetchingError,
|
||||
isLoggedIn: isLoggedInState,
|
||||
} = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
@@ -86,8 +66,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
|
||||
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
||||
|
||||
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
||||
|
||||
const isLocalStorageLoggedIn =
|
||||
getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true';
|
||||
|
||||
@@ -103,63 +81,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({
|
||||
queryFn: () => {
|
||||
if (orgData && orgData.id !== undefined) {
|
||||
return getOrgUser({
|
||||
orgId: orgData.id,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
queryKey: ['getOrgUser'],
|
||||
enabled: !isEmpty(orgData),
|
||||
});
|
||||
|
||||
const checkFirstTimeUser = (): boolean => {
|
||||
const users = orgUsers?.payload || [];
|
||||
|
||||
const remainingUsers = users.filter(
|
||||
(user) => user.email !== 'admin@signoz.cloud',
|
||||
);
|
||||
|
||||
return remainingUsers.length === 1;
|
||||
};
|
||||
|
||||
// Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
|
||||
const shouldShowOnboarding = (): boolean => {
|
||||
// Only run this effect if the org users and preferences are loaded
|
||||
|
||||
if (!isLoadingOrgUsers && !isFetchingOrgPreferences) {
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
|
||||
// Redirect to get started if it's not the first user or if the onboarding is complete
|
||||
return isFirstUser && !isOnboardingComplete;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleRedirectForOrgOnboarding = (key: string): void => {
|
||||
if (
|
||||
isLoggedInState &&
|
||||
!isFetchingOrgPreferences &&
|
||||
!isLoadingOrgUsers &&
|
||||
!isEmpty(orgUsers?.payload) &&
|
||||
!isNull(orgPreferences)
|
||||
) {
|
||||
if (key === 'ONBOARDING' && isOnboardingComplete) {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
|
||||
const isFirstTimeUser = checkFirstTimeUser();
|
||||
|
||||
if (isFirstTimeUser && !isOnboardingComplete) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserLoginIfTokenPresent = async (
|
||||
key: keyof typeof ROUTES,
|
||||
): Promise<void> => {
|
||||
@@ -181,8 +102,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
response.payload.refreshJwt,
|
||||
);
|
||||
|
||||
handleRedirectForOrgOnboarding(key);
|
||||
|
||||
if (
|
||||
userResponse &&
|
||||
route &&
|
||||
@@ -210,7 +129,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
) {
|
||||
handleUserLoginIfTokenPresent(key);
|
||||
} else {
|
||||
handleRedirectForOrgOnboarding(key);
|
||||
// user does have localstorage values
|
||||
|
||||
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
|
||||
}
|
||||
@@ -241,45 +160,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
}
|
||||
}, [isFetchingLicensesData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||
setOrgData(org[0]);
|
||||
}
|
||||
}, [org]);
|
||||
|
||||
const handleRouting = (): void => {
|
||||
const showOrgOnboarding = shouldShowOnboarding();
|
||||
|
||||
if (showOrgOnboarding && !isOnboardingComplete) {
|
||||
history.push(ROUTES.ONBOARDING);
|
||||
} else {
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { isPrivate } = currentRoute || {
|
||||
isPrivate: false,
|
||||
};
|
||||
|
||||
if (isLoggedInState && role && role !== 'ADMIN') {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
if (!isPrivate) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!isEmpty(user) &&
|
||||
!isFetchingOrgPreferences &&
|
||||
!isEmpty(orgUsers?.payload) &&
|
||||
!isNull(orgPreferences)
|
||||
) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentRoute, user, role, orgUsers, orgPreferences]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
(async (): Promise<void> => {
|
||||
@@ -301,8 +181,9 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
handlePrivateRoutes(key);
|
||||
} else {
|
||||
// no need to fetch the user and make user fetching false
|
||||
|
||||
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
|
||||
handleRouting();
|
||||
history.push(ROUTES.APPLICATION);
|
||||
}
|
||||
dispatch({
|
||||
type: UPDATE_USER_IS_FETCH,
|
||||
@@ -314,7 +195,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
} else if (pathname === ROUTES.HOME_PAGE) {
|
||||
// routing to application page over root page
|
||||
if (isLoggedInState) {
|
||||
handleRouting();
|
||||
history.push(ROUTES.APPLICATION);
|
||||
} else {
|
||||
navigateToLoginIfNotLoggedIn();
|
||||
}
|
||||
@@ -327,20 +208,13 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
history.push(ROUTES.SOMETHING_WENT_WRONG);
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
dispatch,
|
||||
isLoggedInState,
|
||||
currentRoute,
|
||||
licensesData,
|
||||
orgUsers,
|
||||
orgPreferences,
|
||||
]);
|
||||
}, [dispatch, isLoggedInState, currentRoute, licensesData]);
|
||||
|
||||
if (isUserFetchingError) {
|
||||
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
|
||||
}
|
||||
|
||||
if (isUserFetching || isLoading) {
|
||||
if (isUserFetching) {
|
||||
return <Spinner tip="Loading..." />;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ConfigProvider } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
|
||||
import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -25,19 +24,13 @@ import AlertRuleProvider from 'providers/Alert';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_FEATURE_FLAG_RESPONSE,
|
||||
UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||
UPDATE_ORG_PREFERENCES,
|
||||
} from 'types/actions/app';
|
||||
import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app';
|
||||
import AppReducer, { User } from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
|
||||
|
||||
import PrivateRoute from './Private';
|
||||
@@ -72,41 +65,6 @@ function App(): JSX.Element {
|
||||
const isPremiumSupportEnabled =
|
||||
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||
|
||||
const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({
|
||||
queryFn: () => getAllOrgPreferences(),
|
||||
queryKey: ['getOrgPreferences'],
|
||||
enabled: isLoggedInState && role === USER_ROLES.ADMIN,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (orgPreferences && !isLoadingOrgPreferences) {
|
||||
dispatch({
|
||||
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||
payload: {
|
||||
isFetchingOrgPreferences: false,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_ORG_PREFERENCES,
|
||||
payload: {
|
||||
orgPreferences: orgPreferences.payload?.data || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [orgPreferences, dispatch, isLoadingOrgPreferences]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedInState && role !== USER_ROLES.ADMIN) {
|
||||
dispatch({
|
||||
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||
payload: {
|
||||
isFetchingOrgPreferences: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [isLoggedInState, role, dispatch]);
|
||||
|
||||
const featureResponse = useGetFeatureFlag((allFlags) => {
|
||||
dispatch({
|
||||
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
||||
@@ -224,16 +182,6 @@ function App(): JSX.Element {
|
||||
}, [isLoggedInState, isOnBasicPlan, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === ROUTES.ONBOARDING) {
|
||||
window.Intercom('update', {
|
||||
hide_default_launcher: true,
|
||||
});
|
||||
} else {
|
||||
window.Intercom('update', {
|
||||
hide_default_launcher: false,
|
||||
});
|
||||
}
|
||||
|
||||
trackPageView(pathname);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
@@ -256,7 +204,6 @@ function App(): JSX.Element {
|
||||
user,
|
||||
licenseData,
|
||||
isPremiumSupportEnabled,
|
||||
pathname,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -66,10 +66,6 @@ export const Onboarding = Loadable(
|
||||
() => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPage'),
|
||||
);
|
||||
|
||||
export const OrgOnboarding = Loadable(
|
||||
() => import(/* webpackChunkName: "OrgOnboarding" */ 'pages/OrgOnboarding'),
|
||||
);
|
||||
|
||||
export const DashboardPage = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
OldLogsExplorer,
|
||||
Onboarding,
|
||||
OrganizationSettings,
|
||||
OrgOnboarding,
|
||||
PasswordReset,
|
||||
PipelinePage,
|
||||
ServiceMapPage,
|
||||
@@ -69,13 +68,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'GET_STARTED',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ONBOARDING,
|
||||
exact: false,
|
||||
component: OrgOnboarding,
|
||||
isPrivate: true,
|
||||
key: 'ONBOARDING',
|
||||
},
|
||||
{
|
||||
component: LogsIndexToFields,
|
||||
path: ROUTES.LOGS_INDEX_FIELDS,
|
||||
|
||||
@@ -4,7 +4,6 @@ export const apiV2 = '/api/v2/';
|
||||
export const apiV3 = '/api/v3/';
|
||||
export const apiV4 = '/api/v4/';
|
||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||
export const gatewayApiV2 = '/api/gateway/v2/';
|
||||
export const apiAlertManager = '/api/alertmanager/';
|
||||
|
||||
export default apiV1;
|
||||
|
||||
@@ -15,7 +15,6 @@ import apiV1, {
|
||||
apiV3,
|
||||
apiV4,
|
||||
gatewayApiV1,
|
||||
gatewayApiV2,
|
||||
} from './apiV1';
|
||||
import { Logout } from './utils';
|
||||
|
||||
@@ -170,19 +169,6 @@ GatewayApiV1Instance.interceptors.response.use(
|
||||
GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
// gateway Api V2
|
||||
export const GatewayApiV2Instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV2}`,
|
||||
});
|
||||
|
||||
GatewayApiV2Instance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
);
|
||||
|
||||
GatewayApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
AxiosAlertManagerInstance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { GatewayApiV2Instance } from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { UpdateProfileProps } from 'types/api/onboarding/types';
|
||||
|
||||
const updateProfile = async (
|
||||
props: UpdateProfileProps,
|
||||
): Promise<SuccessResponse<UpdateProfileProps> | ErrorResponse> => {
|
||||
const response = await GatewayApiV2Instance.put('/profiles/me', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateProfile;
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
@@ -1,18 +0,0 @@
|
||||
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;
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -1,22 +0,0 @@
|
||||
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;
|
||||
@@ -1,28 +0,0 @@
|
||||
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/${preferencePayload.preferenceID}`,
|
||||
{
|
||||
preference_value: preferencePayload.value,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateOrgPreference;
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
@@ -1,18 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { InviteUsersResponse, UsersProps } from 'types/api/user/inviteUsers';
|
||||
|
||||
const inviteUsers = async (
|
||||
users: UsersProps,
|
||||
): Promise<SuccessResponse<InviteUsersResponse>> => {
|
||||
const response = await axios.post(`/invite/bulk`, users);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default inviteUsers;
|
||||
@@ -1,3 +1,46 @@
|
||||
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,
|
||||
@@ -12,3 +55,35 @@ 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`;
|
||||
|
||||
@@ -129,7 +129,6 @@ function LogDetail({
|
||||
return (
|
||||
<Drawer
|
||||
width="60%"
|
||||
maskStyle={{ background: 'none' }}
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
||||
|
||||
@@ -195,20 +195,21 @@ function ListLogView({
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
$isActiveLog={
|
||||
isHighlighted ||
|
||||
activeLog?.id === logData.id ||
|
||||
activeContextLog?.id === logData.id
|
||||
}
|
||||
$isActiveLog={isHighlighted}
|
||||
$isDarkMode={isDarkMode}
|
||||
$logType={logType}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleDetailedView}
|
||||
fontSize={fontSize}
|
||||
>
|
||||
<div className="log-line">
|
||||
<LogStateIndicator type={logType} fontSize={fontSize} />
|
||||
<LogStateIndicator
|
||||
type={logType}
|
||||
isActive={
|
||||
activeLog?.id === logData.id || activeContextLog?.id === logData.id
|
||||
}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
<div>
|
||||
<LogContainer fontSize={fontSize}>
|
||||
<LogGeneralField
|
||||
|
||||
@@ -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,7 +15,6 @@ interface LogContainerProps {
|
||||
export const Container = styled(Card)<{
|
||||
$isActiveLog: boolean;
|
||||
$isDarkMode: boolean;
|
||||
$logType: string;
|
||||
fontSize: FontSize;
|
||||
}>`
|
||||
width: 100% !important;
|
||||
@@ -42,8 +41,13 @@ export const Container = styled(Card)<{
|
||||
? `padding:0.3rem 0.6rem;`
|
||||
: ``}
|
||||
|
||||
${({ $isActiveLog, $isDarkMode, $logType }): string =>
|
||||
getActiveLogBackground($isActiveLog, $isDarkMode, $logType)}
|
||||
${({ $isActiveLog, $isDarkMode }): string =>
|
||||
$isActiveLog
|
||||
? `background-color: ${
|
||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
||||
} !important`
|
||||
: ''}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Text = styled(Typography.Text)`
|
||||
|
||||
@@ -41,4 +41,10 @@
|
||||
background-color: var(--bg-sakura-500);
|
||||
}
|
||||
}
|
||||
|
||||
&.isActive {
|
||||
.line {
|
||||
background-color: var(--bg-robin-400, #7190f9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@ 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} />,
|
||||
|
||||
@@ -44,16 +44,22 @@ export const LogType = {
|
||||
|
||||
function LogStateIndicator({
|
||||
type,
|
||||
isActive,
|
||||
fontSize,
|
||||
}: {
|
||||
type: string;
|
||||
fontSize: FontSize;
|
||||
isActive?: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="log-state-indicator">
|
||||
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
|
||||
<div className={cx('line', type, fontSize)}> </div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LogStateIndicator.defaultProps = {
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
export default LogStateIndicator;
|
||||
|
||||
@@ -162,15 +162,20 @@ function RawLogView({
|
||||
$isDarkMode={isDarkMode}
|
||||
$isReadOnly={isReadOnly}
|
||||
$isHightlightedLog={isHighlighted}
|
||||
$isActiveLog={
|
||||
activeLog?.id === data.id || activeContextLog?.id === data.id || isActiveLog
|
||||
}
|
||||
$logType={logType}
|
||||
$isActiveLog={isActiveLog}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
fontSize={fontSize}
|
||||
>
|
||||
<LogStateIndicator type={logType} fontSize={fontSize} />
|
||||
<LogStateIndicator
|
||||
type={logType}
|
||||
isActive={
|
||||
activeLog?.id === data.id ||
|
||||
activeContextLog?.id === data.id ||
|
||||
isActiveLog
|
||||
}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
|
||||
<RawLogContent
|
||||
$isReadOnly={isReadOnly}
|
||||
|
||||
@@ -13,7 +13,6 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
$isHightlightedLog: boolean;
|
||||
$logType: string;
|
||||
fontSize: FontSize;
|
||||
}>`
|
||||
position: relative;
|
||||
@@ -35,12 +34,11 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
: `margin: 2px 0;`}
|
||||
}
|
||||
|
||||
${({ $isActiveLog, $logType }): string =>
|
||||
getActiveLogBackground($isActiveLog, true, $logType)}
|
||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||
|
||||
${({ $isReadOnly, $isActiveLog, $isDarkMode, $logType }): string =>
|
||||
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
|
||||
$isActiveLog
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||
|
||||
${({ $isHightlightedLog, $isDarkMode }): string =>
|
||||
|
||||
@@ -35,6 +35,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
linesPerRow,
|
||||
fontSize,
|
||||
appendTo = 'center',
|
||||
activeContextLog,
|
||||
activeLog,
|
||||
isListViewPanel,
|
||||
} = props;
|
||||
|
||||
@@ -88,6 +90,9 @@ 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)}>
|
||||
@@ -125,7 +130,16 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
},
|
||||
...(appendTo === 'end' ? fieldColumns : []),
|
||||
];
|
||||
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
|
||||
}, [
|
||||
fields,
|
||||
isListViewPanel,
|
||||
appendTo,
|
||||
isDarkMode,
|
||||
linesPerRow,
|
||||
activeLog?.id,
|
||||
activeContextLog?.id,
|
||||
fontSize,
|
||||
]);
|
||||
|
||||
return { columns, dataSource: flattenLogData };
|
||||
};
|
||||
|
||||
@@ -107,7 +107,6 @@ function DynamicColumnTable({
|
||||
className="dynamicColumnTable-button filter-btn"
|
||||
size="middle"
|
||||
icon={<SlidersHorizontal size={14} />}
|
||||
data-testid="additional-filters-button"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
@@ -22,5 +22,4 @@ export enum FeatureKeys {
|
||||
GATEWAY = 'GATEWAY',
|
||||
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
||||
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
|
||||
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import { QueryFunctionsTypes } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
|
||||
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
||||
{
|
||||
value: QueryFunctionsTypes.ANOMALY,
|
||||
label: 'Anomaly',
|
||||
},
|
||||
{
|
||||
value: QueryFunctionsTypes.CUTOFF_MIN,
|
||||
label: 'Cut Off Min',
|
||||
|
||||
@@ -8,7 +8,6 @@ const ROUTES = {
|
||||
TRACE_DETAIL: '/trace/:id',
|
||||
TRACES_EXPLORER: '/traces-explorer',
|
||||
GET_STARTED: '/get-started',
|
||||
ONBOARDING: '/onboarding',
|
||||
GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring',
|
||||
GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management',
|
||||
GET_STARTED_INFRASTRUCTURE_MONITORING:
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 240px;
|
||||
padding: 0px 8px;
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
|
||||
.anomaly-alert-evaluation-view-series-list {
|
||||
@@ -41,10 +41,6 @@
|
||||
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;
|
||||
@@ -63,32 +59,8 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,63 +90,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ 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';
|
||||
@@ -13,8 +11,6 @@ import { LineChart } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import tooltipPlugin from './tooltipPlugin';
|
||||
|
||||
function UplotChart({
|
||||
data,
|
||||
options,
|
||||
@@ -67,20 +63,13 @@ function AnomalyAlertEvaluationView({
|
||||
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);
|
||||
const chartData = getUplotChartDataForAnomalyDetection(data);
|
||||
setSeriesData(chartData);
|
||||
|
||||
setAllSeries(Object.keys(chartData));
|
||||
|
||||
setFilteredSeriesKeys(Object.keys(chartData));
|
||||
}, [data, isDarkMode]);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const seriesKeys = Object.keys(seriesData);
|
||||
@@ -141,6 +130,8 @@ function AnomalyAlertEvaluationView({
|
||||
},
|
||||
};
|
||||
|
||||
const allSeries = Object.keys(seriesData);
|
||||
|
||||
const initialData = allSeries.length
|
||||
? [
|
||||
seriesData[allSeries[0]].data[0], // Shared X-axis
|
||||
@@ -151,38 +142,10 @@ function AnomalyAlertEvaluationView({
|
||||
const options = {
|
||||
width: dimensions.width,
|
||||
height: dimensions.height - 36,
|
||||
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
|
||||
plugins: [bandsPlugin],
|
||||
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',
|
||||
@@ -195,7 +158,6 @@ function AnomalyAlertEvaluationView({
|
||||
width: 2,
|
||||
show: true,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
},
|
||||
{
|
||||
label: `Predicted Value`,
|
||||
@@ -204,29 +166,18 @@ function AnomalyAlertEvaluationView({
|
||||
dash: [2, 2],
|
||||
show: true,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
},
|
||||
{
|
||||
label: `Upper Band`,
|
||||
stroke: 'transparent',
|
||||
show: true,
|
||||
show: false,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
show: false,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Lower Band`,
|
||||
stroke: 'transparent',
|
||||
show: true,
|
||||
show: false,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
points: {
|
||||
show: false,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
]
|
||||
: allSeries.map((seriesKey) => ({
|
||||
@@ -235,13 +186,11 @@ function AnomalyAlertEvaluationView({
|
||||
width: 2,
|
||||
show: true,
|
||||
paths: _spline,
|
||||
spanGaps: true,
|
||||
}))),
|
||||
],
|
||||
scales: {
|
||||
x: {
|
||||
time: true,
|
||||
spanGaps: true,
|
||||
},
|
||||
y: {
|
||||
...getYAxisScaleForAnomalyDetection({
|
||||
@@ -255,30 +204,12 @@ function AnomalyAlertEvaluationView({
|
||||
grid: {
|
||||
show: true,
|
||||
},
|
||||
legend: {
|
||||
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
|
||||
@@ -306,51 +237,37 @@ function AnomalyAlertEvaluationView({
|
||||
<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}
|
||||
/>
|
||||
|
||||
<Typography.Title
|
||||
level={5}
|
||||
className="anomaly-alert-evaluation-view-series-list-title"
|
||||
>
|
||||
Select a series
|
||||
</Typography.Title>
|
||||
<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>
|
||||
|
||||
{allSeries.map((seriesKey) => (
|
||||
<Checkbox
|
||||
className="anomaly-alert-evaluation-view-series-list-item"
|
||||
key={seriesKey}
|
||||
type="checkbox"
|
||||
name="series"
|
||||
value="all"
|
||||
checked={selectedSeries === null}
|
||||
onChange={(): void => handleSeriesChange(null)}
|
||||
value={seriesKey}
|
||||
checked={selectedSeries === seriesKey}
|
||||
onChange={(): void => handleSeriesChange(seriesKey)}
|
||||
>
|
||||
Show All
|
||||
{seriesKey}
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
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;
|
||||
@@ -7,8 +7,6 @@
|
||||
width: calc(100% - 64px);
|
||||
z-index: 0;
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
.content-container {
|
||||
position: relative;
|
||||
margin: 0 1rem;
|
||||
|
||||
@@ -191,7 +191,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const pageTitle = t(routeKey);
|
||||
const renderFullScreen =
|
||||
pathname === ROUTES.GET_STARTED ||
|
||||
pathname === ROUTES.ONBOARDING ||
|
||||
pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING ||
|
||||
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
|
||||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
|
||||
@@ -212,13 +211,6 @@ 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);
|
||||
|
||||
@@ -3,41 +3,30 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { OptionType } from './types';
|
||||
|
||||
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;
|
||||
};
|
||||
export const getOptionList = (t: TFunction): OptionType[] => [
|
||||
{
|
||||
title: t('anomaly_based_alert'),
|
||||
selection: AlertTypes.ANOMALY_BASED_ALERT,
|
||||
description: t('anomaly_based_alert_desc'),
|
||||
},
|
||||
{
|
||||
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'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Row, Tag, Typography } from 'antd';
|
||||
import { Row, 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';
|
||||
@@ -14,10 +12,7 @@ import { OptionType } from './types';
|
||||
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
const { t } = useTranslation(['alerts']);
|
||||
|
||||
const isAnomalyDetectionEnabled =
|
||||
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||
|
||||
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
|
||||
const optionList = getOptionList(t);
|
||||
|
||||
function handleRedirection(option: AlertTypes): void {
|
||||
let url = '';
|
||||
@@ -61,13 +56,6 @@ 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);
|
||||
}}
|
||||
|
||||
@@ -4,5 +4,4 @@ export interface OptionType {
|
||||
title: string;
|
||||
selection: AlertTypes;
|
||||
description: string;
|
||||
isBeta?: boolean;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
initialQueryPromQLData,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
AlertDef,
|
||||
@@ -59,53 +58,8 @@ export const alertDefaults: AlertDef = {
|
||||
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,
|
||||
target: 3,
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
||||
export const logAlertDefaults: AlertDef = {
|
||||
alertType: AlertTypes.LOGS_BASED_ALERT,
|
||||
version: ENTITY_VERSION_V4,
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
@@ -136,7 +90,6 @@ export const logAlertDefaults: AlertDef = {
|
||||
|
||||
export const traceAlertDefaults: AlertDef = {
|
||||
alertType: AlertTypes.TRACES_BASED_ALERT,
|
||||
version: ENTITY_VERSION_V4,
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
@@ -167,7 +120,6 @@ export const traceAlertDefaults: AlertDef = {
|
||||
|
||||
export const exceptionAlertDefaults: AlertDef = {
|
||||
alertType: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
version: ENTITY_VERSION_V4,
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
@@ -197,7 +149,7 @@ export const exceptionAlertDefaults: AlertDef = {
|
||||
};
|
||||
|
||||
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]: anamolyAlertDefaults,
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]: alertDefaults,
|
||||
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
|
||||
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
|
||||
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
||||
|
||||
@@ -13,7 +13,6 @@ import { AlertDef } from 'types/api/alerts/def';
|
||||
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||
import {
|
||||
alertDefaults,
|
||||
anamolyAlertDefaults,
|
||||
exceptionAlertDefaults,
|
||||
logAlertDefaults,
|
||||
traceAlertDefaults,
|
||||
@@ -25,12 +24,8 @@ 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 =
|
||||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
? AlertTypes.ANOMALY_BASED_ALERT
|
||||
: queryParams.get(QueryParams.alertType);
|
||||
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
function getAlertTypeFromDataSource(): AlertTypes | null {
|
||||
@@ -63,7 +58,7 @@ function CreateRules(): JSX.Element {
|
||||
break;
|
||||
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||
setInitValues({
|
||||
...anamolyAlertDefaults,
|
||||
...alertDefaults,
|
||||
version: version || ENTITY_VERSION_V4,
|
||||
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
});
|
||||
@@ -83,10 +78,7 @@ function CreateRules(): JSX.Element {
|
||||
: typ,
|
||||
);
|
||||
|
||||
if (
|
||||
typ === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
) {
|
||||
if (typ === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
queryParams.set(
|
||||
QueryParams.ruleType,
|
||||
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: rgba(22, 24, 29, 0.6);
|
||||
@@ -33,7 +33,6 @@
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-slate-500);
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
PanelBottomClose,
|
||||
Plus,
|
||||
X,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
CSSProperties,
|
||||
@@ -514,11 +515,7 @@ function ExplorerOptions({
|
||||
|
||||
return (
|
||||
<div className="explorer-options-container">
|
||||
{
|
||||
// if a viewName is selected and the explorer options are not hidden then
|
||||
// always show the clear option
|
||||
}
|
||||
{!isExplorerOptionHidden && viewName && (
|
||||
{isQueryUpdated && !isExplorerOptionHidden && (
|
||||
<div
|
||||
className={cx(
|
||||
isEditDeleteSupported ? '' : 'hide-update',
|
||||
@@ -532,25 +529,18 @@ function ExplorerOptions({
|
||||
icon={<X 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>
|
||||
</>
|
||||
)}
|
||||
<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 && (
|
||||
@@ -574,7 +564,10 @@ function ExplorerOptions({
|
||||
}}
|
||||
dropdownStyle={dropdownStyle}
|
||||
className="views-dropdown"
|
||||
allowClear={false}
|
||||
allowClear={{
|
||||
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
|
||||
}}
|
||||
onClear={handleClearSelect}
|
||||
ref={ref}
|
||||
>
|
||||
{viewsData?.data?.data?.map((view) => {
|
||||
@@ -669,8 +662,8 @@ function ExplorerOptions({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ExplorerOptionsHideArea
|
||||
viewName={viewName}
|
||||
isExplorerOptionHidden={isExplorerOptionHidden}
|
||||
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
||||
sourcepage={sourcepage}
|
||||
@@ -679,6 +672,7 @@ function ExplorerOptions({
|
||||
onUpdateQueryHandler={onUpdateQueryHandler}
|
||||
isEditDeleteSupported={isEditDeleteSupported}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
className="save-view-modal"
|
||||
title={<span className="title">Save this view</span>}
|
||||
@@ -711,6 +705,7 @@ function ExplorerOptions({
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
footer={null}
|
||||
onOk={onCancel(false)}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { setExplorerToolBarVisibility } from './utils';
|
||||
|
||||
interface DroppableAreaProps {
|
||||
viewName: string;
|
||||
isQueryUpdated: boolean;
|
||||
isExplorerOptionHidden?: boolean;
|
||||
sourcepage: DataSource;
|
||||
@@ -21,7 +20,6 @@ interface DroppableAreaProps {
|
||||
}
|
||||
|
||||
function ExplorerOptionsHideArea({
|
||||
viewName,
|
||||
isQueryUpdated,
|
||||
isExplorerOptionHidden,
|
||||
sourcepage,
|
||||
@@ -41,7 +39,7 @@ function ExplorerOptionsHideArea({
|
||||
<div className="explorer-option-droppable-container">
|
||||
{isExplorerOptionHidden && (
|
||||
<>
|
||||
{viewName && (
|
||||
{isQueryUpdated && (
|
||||
<div className="explorer-actions-btn">
|
||||
<Tooltip title="Clear this view">
|
||||
<Button
|
||||
@@ -51,7 +49,7 @@ function ExplorerOptionsHideArea({
|
||||
icon={<X size={14} color={Color.BG_INK_500} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
{isEditDeleteSupported && isQueryUpdated && (
|
||||
{isEditDeleteSupported && (
|
||||
<Tooltip title="Update this View">
|
||||
<Button
|
||||
onClick={onUpdateQueryHandler}
|
||||
|
||||
@@ -3,7 +3,6 @@ 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';
|
||||
@@ -18,7 +17,6 @@ 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';
|
||||
@@ -261,9 +259,6 @@ function ChartPreview({
|
||||
const chartDataAvailable =
|
||||
chartData && !queryResponse.isError && !queryResponse.isLoading;
|
||||
|
||||
const isAnomalyDetectionEnabled =
|
||||
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||
|
||||
return (
|
||||
<div className="alert-chart-container" ref={graphRef}>
|
||||
<ChartContainer>
|
||||
@@ -296,7 +291,6 @@ function ChartPreview({
|
||||
|
||||
{chartDataAvailable &&
|
||||
isAnomalyDetectionAlert &&
|
||||
isAnomalyDetectionEnabled &&
|
||||
queryResponse?.data?.payload?.data?.resultType === 'anomaly' && (
|
||||
<AnomalyAlertEvaluationView
|
||||
data={queryResponse?.data?.payload}
|
||||
|
||||
@@ -160,15 +160,6 @@ function RuleOptions({
|
||||
});
|
||||
};
|
||||
|
||||
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}
|
||||
@@ -212,28 +203,6 @@ function RuleOptions({
|
||||
</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}
|
||||
@@ -268,6 +237,39 @@ function RuleOptions({
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const renderAnomalyRuleOpts = (
|
||||
onChange: InputNumberProps['onChange'],
|
||||
): 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>
|
||||
<InputNumber
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
<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 renderPromRuleOptions = (): JSX.Element => (
|
||||
<Form.Item>
|
||||
<Typography.Text>
|
||||
@@ -318,32 +320,6 @@ 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}
|
||||
@@ -378,7 +354,7 @@ function RuleOptions({
|
||||
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
|
||||
{queryCategory !== EQueryType.PROM &&
|
||||
ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
|
||||
<>{renderAnomalyRuleOpts()}</>
|
||||
<>{renderAnomalyRuleOpts(onChange)}</>
|
||||
)}
|
||||
|
||||
{queryCategory !== EQueryType.PROM &&
|
||||
@@ -386,31 +362,32 @@ function RuleOptions({
|
||||
renderThresholdRuleOpts()}
|
||||
|
||||
<Space direction="vertical" size="large">
|
||||
{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>
|
||||
{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">
|
||||
|
||||
@@ -23,10 +23,7 @@ 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 useFeatureFlag, {
|
||||
MESSAGE,
|
||||
useIsFeatureDisabled,
|
||||
} from 'hooks/useFeatureFlag';
|
||||
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
@@ -39,7 +36,6 @@ 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 {
|
||||
@@ -73,19 +69,6 @@ export enum AlertDetectionTypes {
|
||||
ANOMALY_DETECTION_ALERT = 'anomaly_rule',
|
||||
}
|
||||
|
||||
const ALERT_SETUP_GUIDE_URLS: Record<AlertTypes, string> = {
|
||||
[AlertTypes.METRICS_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
[AlertTypes.LOGS_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
[AlertTypes.TRACES_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
[AlertTypes.ANOMALY_BASED_ALERT]:
|
||||
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
@@ -102,8 +85,6 @@ 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;
|
||||
@@ -136,7 +117,9 @@ function FormAlertRules({
|
||||
|
||||
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
|
||||
|
||||
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
|
||||
const [detectionMethod, setDetectionMethod] = useState<string>(
|
||||
AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(currentQuery.unit, yAxisUnit)) {
|
||||
@@ -168,24 +151,11 @@ 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 },
|
||||
namedArgs: { z_score_threshold: 9 },
|
||||
};
|
||||
const functions = data.functions || [];
|
||||
|
||||
@@ -193,19 +163,6 @@ function FormAlertRules({
|
||||
// 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
|
||||
@@ -218,20 +175,6 @@ function FormAlertRules({
|
||||
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];
|
||||
@@ -245,11 +188,7 @@ function FormAlertRules({
|
||||
useEffect(() => {
|
||||
updateFunctionsBasedOnAlertType();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
detectionMethod,
|
||||
alertDef.condition.target,
|
||||
currentQuery.builder.queryData.length,
|
||||
]);
|
||||
}, [detectionMethod, alertDef, currentQuery.builder.queryData.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const broadcastToSpecificChannels =
|
||||
@@ -273,8 +212,7 @@ function FormAlertRules({
|
||||
});
|
||||
|
||||
setDetectionMethod(ruleType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialValue, isNewRule]);
|
||||
}, [initialValue, isNewRule, alertTypeFromURL]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set selectedQueryName based on the length of queryOptions
|
||||
@@ -394,11 +332,7 @@ function FormAlertRules({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
alertDef.ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT &&
|
||||
alertDef.condition?.target !== 0 &&
|
||||
!alertDef.condition?.target
|
||||
) {
|
||||
if (alertDef.condition?.target !== 0 && !alertDef.condition?.target) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('target_missing'),
|
||||
@@ -556,7 +490,6 @@ function FormAlertRules({
|
||||
queryType: currentQuery.queryType,
|
||||
alertId: postableAlert?.id,
|
||||
alertName: postableAlert?.alert,
|
||||
ruleType: postableAlert?.ruleType,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
@@ -641,7 +574,6 @@ 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]);
|
||||
@@ -715,29 +647,6 @@ function FormAlertRules({
|
||||
|
||||
const isRuleCreated = !ruleId || ruleId === 0;
|
||||
|
||||
function handleRedirection(option: AlertTypes): void {
|
||||
let url;
|
||||
if (
|
||||
option === AlertTypes.METRICS_BASED_ALERT &&
|
||||
alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
) {
|
||||
url = ALERT_SETUP_GUIDE_URLS[AlertTypes.ANOMALY_BASED_ALERT];
|
||||
} else {
|
||||
url = ALERT_SETUP_GUIDE_URLS[option];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRuleCreated) {
|
||||
logEvent('Alert: Edit page visited', {
|
||||
@@ -756,12 +665,17 @@ function FormAlertRules({
|
||||
{
|
||||
value: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
label: 'Anomaly Detection Alert',
|
||||
isBeta: true,
|
||||
},
|
||||
];
|
||||
|
||||
const isAnomalyDetectionEnabled =
|
||||
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||
const handleDetectionMethodChange = (value: any): void => {
|
||||
setAlertDef((def) => ({
|
||||
...def,
|
||||
ruleType: value,
|
||||
}));
|
||||
|
||||
setDetectionMethod(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -788,11 +702,7 @@ function FormAlertRules({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={(): void => handleRedirection(alertDef.alertType as AlertTypes)}
|
||||
icon={<ExternalLink size={14} />}
|
||||
>
|
||||
<Button className="periscope-btn" icon={<ExternalLink size={14} />}>
|
||||
Alert Setup Guide
|
||||
</Button>
|
||||
</div>
|
||||
@@ -820,25 +730,24 @@ function FormAlertRules({
|
||||
</StepContainer>
|
||||
|
||||
<div className="steps-container">
|
||||
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||
isAnomalyDetectionEnabled && (
|
||||
<div className="detection-method-container">
|
||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT && (
|
||||
<div className="detection-method-container">
|
||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||
|
||||
<Tabs2
|
||||
key={detectionMethod}
|
||||
tabs={tabs}
|
||||
initialSelectedTab={detectionMethod || ''}
|
||||
onSelectTab={handleDetectionMethodChange}
|
||||
/>
|
||||
<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 className="detection-method-description">
|
||||
{detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
? t('anomaly_detection_alert_desc')
|
||||
: t('threshold_alert_desc')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<QuerySection
|
||||
queryCategory={currentQuery.queryType}
|
||||
|
||||
@@ -18,7 +18,6 @@ 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';
|
||||
@@ -73,18 +72,16 @@ function WidgetHeader({
|
||||
tableProcessedDataRef,
|
||||
setSearchTerm,
|
||||
}: IWidgetHeaderProps): JSX.Element | null {
|
||||
const urlQuery = useUrlQuery();
|
||||
const onEditHandler = useCallback((): void => {
|
||||
const widgetId = widget.id;
|
||||
urlQuery.set(QueryParams.widgetId, widgetId);
|
||||
urlQuery.set(QueryParams.graphType, widget.panelTypes);
|
||||
urlQuery.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(widget.query)),
|
||||
history.push(
|
||||
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
|
||||
widget.panelTypes
|
||||
}&${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||
JSON.stringify(widget.query),
|
||||
)}`,
|
||||
);
|
||||
const generatedUrl = `${window.location.pathname}/new?${urlQuery}`;
|
||||
history.push(generatedUrl);
|
||||
}, [urlQuery, widget.id, widget.panelTypes, widget.query]);
|
||||
}, [widget.id, widget.panelTypes, widget.query]);
|
||||
|
||||
const onCreateAlertsHandler = useCreateAlerts(widget, 'dashboardView');
|
||||
|
||||
|
||||
@@ -97,19 +97,13 @@ function GridTableComponent({
|
||||
|
||||
const newColumnData = columns.map((e) => ({
|
||||
...e,
|
||||
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));
|
||||
|
||||
render: (text: string): ReactNode => {
|
||||
const isNumber = !Number.isNaN(Number(text));
|
||||
if (thresholds && isNumber) {
|
||||
const { hasMultipleMatches, threshold } = findMatchingThreshold(
|
||||
thresholds,
|
||||
e.title as string,
|
||||
Number(textForThreshold),
|
||||
columnUnits?.[e.title as string],
|
||||
Number(text),
|
||||
);
|
||||
|
||||
const idx = thresholds.findIndex(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* 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';
|
||||
@@ -31,39 +30,10 @@ 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;
|
||||
@@ -75,12 +45,10 @@ export function findMatchingThreshold(
|
||||
if (
|
||||
threshold.thresholdValue !== undefined &&
|
||||
threshold.thresholdTableOptions === label &&
|
||||
evaluateThresholdWithConvertedValue(
|
||||
value,
|
||||
threshold?.thresholdValue,
|
||||
evaluateCondition(
|
||||
threshold.thresholdOperator,
|
||||
threshold.thresholdUnit,
|
||||
columnUnit,
|
||||
value,
|
||||
threshold.thresholdValue,
|
||||
)
|
||||
) {
|
||||
matchingThresholds.push(threshold);
|
||||
|
||||
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -396,6 +397,15 @@ 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',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,17 +5,6 @@
|
||||
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;
|
||||
|
||||
@@ -25,6 +25,8 @@ 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';
|
||||
@@ -77,7 +79,6 @@ 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,
|
||||
@@ -692,14 +693,17 @@ 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 ||
|
||||
|
||||
@@ -82,12 +82,6 @@ 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 {
|
||||
@@ -129,14 +123,11 @@ function ImportJSON({
|
||||
});
|
||||
}
|
||||
setDashboardCreating(false);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setDashboardCreating(false);
|
||||
setIsFeatureAlert(false);
|
||||
|
||||
setIsCreateDashboardError(true);
|
||||
notifications.error({
|
||||
message: error instanceof Error ? error.message : t('error_loading_json'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
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'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>
|
||||
);
|
||||
}
|
||||
@@ -2,25 +2,3 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -22,11 +21,6 @@ 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,
|
||||
@@ -37,17 +31,10 @@ const CustomTableRow: TableComponents<ILog>['TableRow'] = ({
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const logType = getLogIndicatorType(props.item);
|
||||
|
||||
return (
|
||||
<TableRowStyled
|
||||
$isDarkMode={isDarkMode}
|
||||
$isActiveLog={
|
||||
isHighlighted ||
|
||||
(context as CustomTableRowProps).activeContextLogId === props.item.id ||
|
||||
(context as CustomTableRowProps).activeLogId === props.item.id
|
||||
}
|
||||
$logType={logType}
|
||||
$isActiveLog={isHighlighted}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
>
|
||||
@@ -79,6 +66,8 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
...tableViewProps,
|
||||
onClickExpand: onSetActiveLog,
|
||||
onOpenLogsContext: handleSetActiveContextLog,
|
||||
activeLog,
|
||||
activeContextLog,
|
||||
});
|
||||
|
||||
const { draggedColumns, onDragColumns } = useDragColumns<
|
||||
@@ -164,14 +153,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
// TODO: fix it in the future
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
TableRow: (props): any =>
|
||||
CustomTableRow({
|
||||
...props,
|
||||
context: {
|
||||
activeContextLogId: activeContextLog?.id,
|
||||
activeLogId: activeLog?.id,
|
||||
},
|
||||
} as any),
|
||||
TableRow: CustomTableRow,
|
||||
}}
|
||||
itemContent={itemContent}
|
||||
fixedHeaderContent={tableHeader}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* 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';
|
||||
@@ -36,12 +37,13 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
|
||||
export const TableRowStyled = styled.tr<{
|
||||
$isActiveLog: boolean;
|
||||
$isDarkMode: boolean;
|
||||
$logType: string;
|
||||
}>`
|
||||
td {
|
||||
${({ $isActiveLog, $isDarkMode, $logType }): string =>
|
||||
${({ $isActiveLog, $isDarkMode }): string =>
|
||||
$isActiveLog
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode, $logType)
|
||||
? `background-color: ${
|
||||
$isDarkMode ? Color.BG_SLATE_500 : Color.BG_VANILLA_300
|
||||
} !important`
|
||||
: ''};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ 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';
|
||||
@@ -45,11 +47,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { AppState } from 'store/reducers';
|
||||
import {
|
||||
Dashboard,
|
||||
DashboardData,
|
||||
IDashboardVariable,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
@@ -65,30 +63,6 @@ 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;
|
||||
@@ -354,6 +328,18 @@ 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}
|
||||
@@ -421,10 +407,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
type="text"
|
||||
icon={<FileJson size={14} />}
|
||||
onClick={(): void => {
|
||||
downloadObjectAsJson(
|
||||
sanitizeDashboardData(selectedData),
|
||||
selectedData.title,
|
||||
);
|
||||
downloadObjectAsJson(selectedData, selectedData.title);
|
||||
setIsDashbordSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
@@ -434,9 +417,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
type="text"
|
||||
icon={<ClipboardCopy size={14} />}
|
||||
onClick={(): void => {
|
||||
setCopy(
|
||||
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
|
||||
);
|
||||
setCopy(JSON.stringify(selectedData, null, 2));
|
||||
setIsDashbordSettingsOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -257,7 +257,8 @@ function VariableItem({
|
||||
if (variableData.name) {
|
||||
if (
|
||||
value === ALL_SELECT_VALUE ||
|
||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE))
|
||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
||||
(Array.isArray(value) && value.length === 0)
|
||||
) {
|
||||
onValueUpdate(variableData.name, variableData.id, optionsData, true);
|
||||
} else {
|
||||
@@ -323,6 +324,10 @@ 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;
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -234,6 +235,21 @@ 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"
|
||||
|
||||
@@ -297,16 +297,6 @@
|
||||
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 {
|
||||
|
||||
@@ -3,18 +3,17 @@ 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 { useMemo, useRef, useState } from 'react';
|
||||
import { 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';
|
||||
@@ -41,7 +40,6 @@ function Threshold({
|
||||
thresholdLabel = '',
|
||||
tableOptions,
|
||||
thresholdTableOptions = '',
|
||||
columnUnits,
|
||||
}: ThresholdProps): JSX.Element {
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
|
||||
const [operator, setOperator] = useState<string | number>(
|
||||
@@ -194,13 +192,6 @@ 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}
|
||||
@@ -312,7 +303,7 @@ function Threshold({
|
||||
{isEditMode ? (
|
||||
<Select
|
||||
defaultValue={unit}
|
||||
options={unitOptions(columnUnits?.[tableSelectedOption] || '')}
|
||||
options={unitOptions}
|
||||
onChange={handleUnitChange}
|
||||
showSearch
|
||||
className="unit-selection"
|
||||
@@ -348,12 +339,6 @@ 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
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import './ThresholdSelector.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { Events } from 'constants/events';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { Antenna, Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import Threshold from './Threshold';
|
||||
@@ -19,23 +21,22 @@ function ThresholdSelector({
|
||||
setThresholds,
|
||||
yAxisUnit,
|
||||
selectedGraph,
|
||||
columnUnits,
|
||||
}: ThresholdSelectorProps): JSX.Element {
|
||||
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 [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 moveThreshold = useCallback(
|
||||
(dragIndex: number, hoverIndex: number) => {
|
||||
@@ -65,7 +66,7 @@ function ThresholdSelector({
|
||||
moveThreshold,
|
||||
keyIndex: thresholds.length,
|
||||
selectedGraph,
|
||||
thresholdTableOptions: aggregationQueries[0] || '',
|
||||
thresholdTableOptions: tableOptions[0]?.value || '',
|
||||
},
|
||||
...thresholds,
|
||||
]);
|
||||
@@ -104,12 +105,8 @@ function ThresholdSelector({
|
||||
moveThreshold={moveThreshold}
|
||||
selectedGraph={selectedGraph}
|
||||
thresholdLabel={threshold.thresholdLabel}
|
||||
tableOptions={aggregationQueries.map((query) => ({
|
||||
value: query,
|
||||
label: query,
|
||||
}))}
|
||||
tableOptions={tableOptions}
|
||||
thresholdTableOptions={threshold.thresholdTableOptions}
|
||||
columnUnits={columnUnits}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
import { ColumnUnit } from 'types/api/dashboard/getAll';
|
||||
|
||||
export type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
|
||||
|
||||
@@ -20,7 +19,6 @@ export type ThresholdProps = {
|
||||
moveThreshold: (dragIndex: number, hoverIndex: number) => void;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
tableOptions?: Array<{ value: string; label: string }>;
|
||||
columnUnits?: ColumnUnit;
|
||||
};
|
||||
|
||||
export type ShowCaseValueProps = {
|
||||
@@ -38,5 +36,4 @@ export type ThresholdSelectorProps = {
|
||||
thresholds: ThresholdProps[];
|
||||
setThresholds: Dispatch<SetStateAction<ThresholdProps[]>>;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
columnUnits: ColumnUnit;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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: '>' },
|
||||
@@ -8,6 +11,11 @@ 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' },
|
||||
|
||||
@@ -438,168 +438,3 @@ 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;
|
||||
};
|
||||
|
||||
@@ -311,7 +311,6 @@ function RightContainer({
|
||||
setThresholds={setThresholds}
|
||||
yAxisUnit={yAxisUnit}
|
||||
selectedGraph={selectedGraph}
|
||||
columnUnits={columnUnits}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
@@ -9,19 +8,12 @@ import {
|
||||
listViewInitialTraceQuery,
|
||||
PANEL_TYPES_INITIAL_QUERY,
|
||||
} from 'container/NewDashboard/ComponentsSlider/constants';
|
||||
import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config';
|
||||
import { cloneDeep, isEmpty, isEqual, set, unset } from 'lodash-es';
|
||||
import { cloneDeep, 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,
|
||||
@@ -537,41 +529,3 @@ 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),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ This is a **sample cURL request** which can be used as a template:
|
||||
|
||||
|
||||
```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 '[
|
||||
|
||||
@@ -312,7 +312,7 @@ export default function Onboarding(): JSX.Element {
|
||||
<div
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Skip Button Clicked', {});
|
||||
history.push(ROUTES.APPLICATION);
|
||||
history.push('/');
|
||||
}}
|
||||
className="skip-to-console"
|
||||
>
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import '../OnboardingQuestionaire.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Input, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface SignozDetails {
|
||||
hearAboutSignoz: string | null;
|
||||
interestInSignoz: string | null;
|
||||
otherInterestInSignoz: string | null;
|
||||
otherAboutSignoz: string | null;
|
||||
}
|
||||
|
||||
interface AboutSigNozQuestionsProps {
|
||||
signozDetails: SignozDetails;
|
||||
setSignozDetails: (details: SignozDetails) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const hearAboutSignozOptions: Record<string, string> = {
|
||||
search: 'Google / Search',
|
||||
hackerNews: 'Hacker News',
|
||||
linkedin: 'LinkedIn',
|
||||
twitter: 'Twitter',
|
||||
reddit: 'Reddit',
|
||||
colleaguesFriends: 'Colleagues / Friends',
|
||||
};
|
||||
|
||||
const interestedInOptions: Record<string, string> = {
|
||||
savingCosts: 'Saving costs',
|
||||
otelNativeStack: 'Interested in Otel-native stack',
|
||||
allInOne: 'All in one (Logs, Metrics & Traces)',
|
||||
};
|
||||
|
||||
export function AboutSigNozQuestions({
|
||||
signozDetails,
|
||||
setSignozDetails,
|
||||
onNext,
|
||||
onBack,
|
||||
}: AboutSigNozQuestionsProps): JSX.Element {
|
||||
const [hearAboutSignoz, setHearAboutSignoz] = useState<string | null>(
|
||||
signozDetails?.hearAboutSignoz || null,
|
||||
);
|
||||
const [otherAboutSignoz, setOtherAboutSignoz] = useState<string>(
|
||||
signozDetails?.otherAboutSignoz || '',
|
||||
);
|
||||
const [interestInSignoz, setInterestInSignoz] = useState<string | null>(
|
||||
signozDetails?.interestInSignoz || null,
|
||||
);
|
||||
const [otherInterestInSignoz, setOtherInterestInSignoz] = useState<string>(
|
||||
signozDetails?.otherInterestInSignoz || '',
|
||||
);
|
||||
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
|
||||
|
||||
useEffect((): void => {
|
||||
if (
|
||||
hearAboutSignoz !== null &&
|
||||
(hearAboutSignoz !== 'Others' || otherAboutSignoz !== '') &&
|
||||
interestInSignoz !== null &&
|
||||
(interestInSignoz !== 'Others' || otherInterestInSignoz !== '')
|
||||
) {
|
||||
setIsNextDisabled(false);
|
||||
} else {
|
||||
setIsNextDisabled(true);
|
||||
}
|
||||
}, [
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
]);
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
setSignozDetails({
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
logEvent('User Onboarding: About SigNoz Questions Answered', {
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
onNext();
|
||||
};
|
||||
|
||||
const handleOnBack = (): void => {
|
||||
setSignozDetails({
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
onBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="questions-container">
|
||||
<Typography.Title level={3} className="title">
|
||||
Tell Us About Your Interest in SigNoz
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="sub-title">
|
||||
We'd love to know a little bit about you and your interest in SigNoz
|
||||
</Typography.Paragraph>
|
||||
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form">
|
||||
<div className="form-group">
|
||||
<div className="question">Where did you hear about SigNoz?</div>
|
||||
<div className="two-column-grid">
|
||||
{Object.keys(hearAboutSignozOptions).map((option: string) => (
|
||||
<Button
|
||||
key={option}
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
hearAboutSignoz === option ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setHearAboutSignoz(option)}
|
||||
>
|
||||
{hearAboutSignozOptions[option]}
|
||||
{hearAboutSignoz === option && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{hearAboutSignoz === 'Others' ? (
|
||||
<Input
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
placeholder="How you got to know about us"
|
||||
value={otherAboutSignoz}
|
||||
autoFocus
|
||||
addonAfter={
|
||||
otherAboutSignoz !== '' ? (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onChange={(e): void => setOtherAboutSignoz(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
hearAboutSignoz === 'Others' ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setHearAboutSignoz('Others')}
|
||||
>
|
||||
Others
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="question">What got you interested in SigNoz?</div>
|
||||
<div className="two-column-grid">
|
||||
{Object.keys(interestedInOptions).map((option: string) => (
|
||||
<Button
|
||||
key={option}
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
interestInSignoz === option ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setInterestInSignoz(option)}
|
||||
>
|
||||
{interestedInOptions[option]}
|
||||
{interestInSignoz === option && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{interestInSignoz === 'Others' ? (
|
||||
<Input
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
placeholder="Please specify your interest"
|
||||
value={otherInterestInSignoz}
|
||||
autoFocus
|
||||
addonAfter={
|
||||
otherInterestInSignoz !== '' ? (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onChange={(e): void => setOtherInterestInSignoz(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
interestInSignoz === 'Others' ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setInterestInSignoz('Others')}
|
||||
>
|
||||
Others
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="next-prev-container">
|
||||
<Button type="default" className="next-button" onClick={handleOnBack}>
|
||||
<ArrowLeft size={14} />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
className={`next-button ${isNextDisabled ? 'disabled' : ''}`}
|
||||
onClick={handleOnNext}
|
||||
disabled={isNextDisabled}
|
||||
>
|
||||
Next
|
||||
<ArrowRight size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
.team-member-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.team-member-role-select {
|
||||
width: 20%;
|
||||
|
||||
.ant-select-selector {
|
||||
border: 1px solid #1d212d;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-email-input {
|
||||
width: 80%;
|
||||
background-color: #121317;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
|
||||
.ant-input,
|
||||
.ant-input-group-addon {
|
||||
background-color: #121317 !important;
|
||||
border-right: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.questions-form-container {
|
||||
.error-message-container,
|
||||
.success-message-container,
|
||||
.partially-sent-invites-container {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.error-message,
|
||||
.success-message {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-users-error-message-container,
|
||||
.invite-users-success-message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.success-message {
|
||||
color: var(--bg-success-500, #00b37e);
|
||||
}
|
||||
}
|
||||
|
||||
.partially-sent-invites-container {
|
||||
margin-top: 16px;
|
||||
padding: 8px;
|
||||
border: 1px solid #1d212d;
|
||||
background-color: #121317;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.partially-sent-invites-message {
|
||||
color: var(--bg-warning-500, #fbbd23);
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.team-member-container {
|
||||
.team-member-role-select {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-email-input {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
.ant-input,
|
||||
.ant-input-group-addon {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.questions-form-container {
|
||||
.invite-users-error-message-container,
|
||||
.invite-users-success-message-container {
|
||||
.success-message {
|
||||
color: var(--bg-success-500, #00b37e);
|
||||
}
|
||||
}
|
||||
|
||||
.partially-sent-invites-container {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
.partially-sent-invites-message {
|
||||
color: var(--bg-warning-500, #fbbd23);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
import './InviteTeamMembers.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Input, Select, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import inviteUsers from 'api/user/inviteUsers';
|
||||
import { AxiosError } from 'axios';
|
||||
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
Plus,
|
||||
TriangleAlert,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useMutation } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import {
|
||||
FailedInvite,
|
||||
InviteUsersResponse,
|
||||
SuccessfulInvite,
|
||||
} from 'types/api/user/inviteUsers';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
interface TeamMember {
|
||||
email: string;
|
||||
role: string;
|
||||
name: string;
|
||||
frontendBaseUrl: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface InviteTeamMembersProps {
|
||||
isLoading: boolean;
|
||||
teamMembers: TeamMember[] | null;
|
||||
setTeamMembers: (teamMembers: TeamMember[]) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
function InviteTeamMembers({
|
||||
isLoading,
|
||||
teamMembers,
|
||||
setTeamMembers,
|
||||
onNext,
|
||||
onBack,
|
||||
}: InviteTeamMembersProps): JSX.Element {
|
||||
const [teamMembersToInvite, setTeamMembersToInvite] = useState<
|
||||
TeamMember[] | null
|
||||
>(teamMembers);
|
||||
const [emailValidity, setEmailValidity] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
const [hasInvalidEmails, setHasInvalidEmails] = useState<boolean>(false);
|
||||
|
||||
const [hasErrors, setHasErrors] = useState<boolean>(true);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [inviteUsersErrorResponse, setInviteUsersErrorResponse] = useState<
|
||||
string[] | null
|
||||
>(null);
|
||||
|
||||
const [inviteUsersSuccessResponse, setInviteUsersSuccessResponse] = useState<
|
||||
string[] | null
|
||||
>(null);
|
||||
|
||||
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
|
||||
|
||||
const defaultTeamMember: TeamMember = {
|
||||
email: '',
|
||||
role: 'EDITOR',
|
||||
name: '',
|
||||
frontendBaseUrl: window.location.origin,
|
||||
id: '',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(teamMembers)) {
|
||||
const teamMember = {
|
||||
...defaultTeamMember,
|
||||
id: uuid(),
|
||||
};
|
||||
|
||||
setTeamMembersToInvite([teamMember]);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [teamMembers]);
|
||||
|
||||
const handleAddTeamMember = (): void => {
|
||||
const newTeamMember = {
|
||||
...defaultTeamMember,
|
||||
id: uuid(),
|
||||
};
|
||||
setTeamMembersToInvite((prev) => [...(prev || []), newTeamMember]);
|
||||
};
|
||||
|
||||
const handleRemoveTeamMember = (id: string): void => {
|
||||
setTeamMembersToInvite((prev) => (prev || []).filter((m) => m.id !== id));
|
||||
};
|
||||
|
||||
// Validation function to check all users
|
||||
const validateAllUsers = (): boolean => {
|
||||
let isValid = true;
|
||||
|
||||
const updatedValidity: Record<string, boolean> = {};
|
||||
|
||||
teamMembersToInvite?.forEach((member) => {
|
||||
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(member.email);
|
||||
if (!emailValid || !member.email) {
|
||||
isValid = false;
|
||||
setHasInvalidEmails(true);
|
||||
}
|
||||
updatedValidity[member.id!] = emailValid;
|
||||
});
|
||||
|
||||
setEmailValidity(updatedValidity);
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const parseInviteUsersSuccessResponse = (
|
||||
response: SuccessfulInvite[],
|
||||
): string[] => response.map((invite) => `${invite.email} - Invite Sent`);
|
||||
|
||||
const parseInviteUsersErrorResponse = (response: FailedInvite[]): string[] =>
|
||||
response.map((invite) => `${invite.email} - ${invite.error}`);
|
||||
|
||||
const handleError = (error: AxiosError): void => {
|
||||
const errorMessage = error.response?.data as InviteUsersResponse;
|
||||
|
||||
if (errorMessage?.status === 'failure') {
|
||||
setHasErrors(true);
|
||||
|
||||
const failedInvitesErrorResponse = parseInviteUsersErrorResponse(
|
||||
errorMessage.failed_invites,
|
||||
);
|
||||
|
||||
setInviteUsersErrorResponse(failedInvitesErrorResponse);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInviteUsersSuccess = (
|
||||
response: SuccessResponse<InviteUsersResponse>,
|
||||
): void => {
|
||||
const inviteUsersResponse = response.payload as InviteUsersResponse;
|
||||
|
||||
if (inviteUsersResponse?.status === 'success') {
|
||||
const successfulInvites = parseInviteUsersSuccessResponse(
|
||||
inviteUsersResponse.successful_invites,
|
||||
);
|
||||
|
||||
setDisableNextButton(true);
|
||||
|
||||
setError(null);
|
||||
setHasErrors(false);
|
||||
setInviteUsersErrorResponse(null);
|
||||
|
||||
setInviteUsersSuccessResponse(successfulInvites);
|
||||
|
||||
setTimeout(() => {
|
||||
setDisableNextButton(false);
|
||||
onNext();
|
||||
}, 1000);
|
||||
} else if (inviteUsersResponse?.status === 'partial_success') {
|
||||
const successfulInvites = parseInviteUsersSuccessResponse(
|
||||
inviteUsersResponse.successful_invites,
|
||||
);
|
||||
|
||||
setInviteUsersSuccessResponse(successfulInvites);
|
||||
|
||||
if (inviteUsersResponse.failed_invites.length > 0) {
|
||||
setHasErrors(true);
|
||||
|
||||
setInviteUsersErrorResponse(
|
||||
parseInviteUsersErrorResponse(inviteUsersResponse.failed_invites),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: sendInvites,
|
||||
isLoading: isSendingInvites,
|
||||
data: inviteUsersApiResponseData,
|
||||
} = useMutation(inviteUsers, {
|
||||
onSuccess: (response: SuccessResponse<InviteUsersResponse>): void => {
|
||||
logEvent('User Onboarding: Invite Team Members Sent', {
|
||||
teamMembers: teamMembersToInvite,
|
||||
});
|
||||
|
||||
handleInviteUsersSuccess(response);
|
||||
},
|
||||
onError: (error: AxiosError): void => {
|
||||
logEvent('User Onboarding: Invite Team Members Failed', {
|
||||
teamMembers: teamMembersToInvite,
|
||||
error,
|
||||
});
|
||||
|
||||
handleError(error);
|
||||
},
|
||||
});
|
||||
|
||||
const handleNext = (): void => {
|
||||
if (validateAllUsers()) {
|
||||
setTeamMembers(teamMembersToInvite || []);
|
||||
|
||||
setHasInvalidEmails(false);
|
||||
setError(null);
|
||||
setHasErrors(false);
|
||||
setInviteUsersErrorResponse(null);
|
||||
setInviteUsersSuccessResponse(null);
|
||||
|
||||
sendInvites({
|
||||
users: teamMembersToInvite || [],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedValidateEmail = useCallback(
|
||||
debounce((email: string, memberId: string) => {
|
||||
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
setEmailValidity((prev) => ({ ...prev, [memberId]: isValid }));
|
||||
}, 500),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleEmailChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
member: TeamMember,
|
||||
): void => {
|
||||
const { value } = e.target;
|
||||
const updatedMembers = cloneDeep(teamMembersToInvite || []);
|
||||
|
||||
const memberToUpdate = updatedMembers.find((m) => m.id === member.id);
|
||||
if (memberToUpdate) {
|
||||
memberToUpdate.email = value;
|
||||
setTeamMembersToInvite(updatedMembers);
|
||||
debouncedValidateEmail(value, member.id!);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleChange = (role: string, member: TeamMember): void => {
|
||||
const updatedMembers = cloneDeep(teamMembersToInvite || []);
|
||||
const memberToUpdate = updatedMembers.find((m) => m.id === member.id);
|
||||
if (memberToUpdate) {
|
||||
memberToUpdate.role = role;
|
||||
setTeamMembersToInvite(updatedMembers);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoLater = (): void => {
|
||||
logEvent('User Onboarding: Invite Team Members Skipped', {
|
||||
teamMembers: teamMembersToInvite,
|
||||
apiResponse: inviteUsersApiResponseData,
|
||||
});
|
||||
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="questions-container">
|
||||
<Typography.Title level={3} className="title">
|
||||
Invite your team members
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="sub-title">
|
||||
The more your team uses SigNoz, the stronger your observability. Share
|
||||
dashboards, collaborate on alerts, and troubleshoot faster together.
|
||||
</Typography.Paragraph>
|
||||
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form invite-team-members-form">
|
||||
<div className="form-group">
|
||||
<div className="question-label">
|
||||
Collaborate with your team
|
||||
<div className="question-sub-label">
|
||||
Invite your team to the SigNoz workspace
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="invite-team-members-container">
|
||||
{teamMembersToInvite?.map((member) => (
|
||||
<div className="team-member-container" key={member.id}>
|
||||
<Input
|
||||
placeholder="your-teammate@org.com"
|
||||
value={member.email}
|
||||
type="email"
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
className="team-member-email-input"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
|
||||
handleEmailChange(e, member)
|
||||
}
|
||||
addonAfter={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
emailValidity[member.id!] === undefined ? null : emailValidity[
|
||||
member.id!
|
||||
] ? (
|
||||
<CheckCircle size={14} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={14} color={Color.BG_SIENNA_500} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
defaultValue={member.role}
|
||||
onChange={(value): void => handleRoleChange(value, member)}
|
||||
className="team-member-role-select"
|
||||
>
|
||||
<Select.Option value="VIEWER">Viewer</Select.Option>
|
||||
<Select.Option value="EDITOR">Editor</Select.Option>
|
||||
<Select.Option value="ADMIN">Admin</Select.Option>
|
||||
</Select>
|
||||
|
||||
{teamMembersToInvite?.length > 1 && (
|
||||
<Button
|
||||
type="primary"
|
||||
className="remove-team-member-button"
|
||||
icon={<X size={14} />}
|
||||
onClick={(): void => handleRemoveTeamMember(member.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="invite-team-members-add-another-member-container">
|
||||
<Button
|
||||
type="primary"
|
||||
className="add-another-member-button"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={handleAddTeamMember}
|
||||
>
|
||||
Member
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasInvalidEmails && (
|
||||
<div className="error-message-container">
|
||||
<Typography.Text className="error-message" type="danger">
|
||||
<TriangleAlert size={14} /> Please enter valid emails for all team
|
||||
members
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-message-container">
|
||||
<Typography.Text className="error-message" type="danger">
|
||||
<TriangleAlert size={14} /> {error}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasErrors && (
|
||||
<>
|
||||
{/* show only when invites are sent successfully & partial error is present */}
|
||||
{inviteUsersSuccessResponse && inviteUsersErrorResponse && (
|
||||
<div className="success-message-container invite-users-success-message-container">
|
||||
{inviteUsersSuccessResponse?.map((success, index) => (
|
||||
<Typography.Text
|
||||
className="success-message"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${success}-${index}`}
|
||||
>
|
||||
<CheckCircle size={14} /> {success}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="error-message-container invite-users-error-message-container">
|
||||
{inviteUsersErrorResponse?.map((error, index) => (
|
||||
<Typography.Text
|
||||
className="error-message"
|
||||
type="danger"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${error}-${index}`}
|
||||
>
|
||||
<TriangleAlert size={14} /> {error}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Partially sent invites */}
|
||||
{inviteUsersSuccessResponse && inviteUsersErrorResponse && (
|
||||
<div className="partially-sent-invites-container">
|
||||
<Typography.Text className="partially-sent-invites-message">
|
||||
<TriangleAlert size={14} />
|
||||
Some invites were sent successfully. Please fix the errors above and
|
||||
resend invites.
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="partially-sent-invites-message">
|
||||
You can click on I'll do this later to go to next step.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="next-prev-container">
|
||||
<Button type="default" className="next-button" onClick={onBack}>
|
||||
<ArrowLeft size={14} />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
className="next-button"
|
||||
onClick={handleNext}
|
||||
loading={isSendingInvites || isLoading || disableNextButton}
|
||||
>
|
||||
Send Invites
|
||||
<ArrowRight size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="do-later-container">
|
||||
<Button
|
||||
type="link"
|
||||
className="do-later-button"
|
||||
onClick={handleDoLater}
|
||||
disabled={isSendingInvites || disableNextButton}
|
||||
>
|
||||
{isLoading && <Loader2 className="animate-spin" size={16} />}
|
||||
|
||||
<span>I'll do this later</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteTeamMembers;
|
||||
@@ -1,49 +0,0 @@
|
||||
.footer-main-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
display: inline-flex;
|
||||
height: 36px;
|
||||
padding: 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--Greyscale-Slate-500, #161922);
|
||||
background: var(--Ink-400, #121317);
|
||||
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.footer-container .footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.footer-container .footer-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #c0c1c3;
|
||||
}
|
||||
|
||||
.footer-container .footer-text {
|
||||
color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3));
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.footer-container .footer-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
fill: var(--Greyscale-Slate-200, #2c3140);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import './OnboardingFooter.styles.scss';
|
||||
|
||||
import { Dot } from 'lucide-react';
|
||||
|
||||
export function OnboardingFooter(): JSX.Element {
|
||||
return (
|
||||
<section className="footer-main-container">
|
||||
<div className="footer-container">
|
||||
<a
|
||||
href="https://trust.signoz.io/"
|
||||
target="_blank"
|
||||
className="footer-content"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src="/logos/hippa.svg" alt="HIPPA" className="footer-logo" />
|
||||
<span className="footer-text">HIPPA</span>
|
||||
</a>
|
||||
<Dot size={24} color="#2C3140" />
|
||||
<a
|
||||
href="https://trust.signoz.io/"
|
||||
target="_blank"
|
||||
className="footer-content"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src="/logos/soc2.svg" alt="SOC2" className="footer-logo" />
|
||||
<span className="footer-text">SOC2</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { OnboardingFooter } from './OnboardingFooter';
|
||||
@@ -1,65 +0,0 @@
|
||||
.header-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0px;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header-container .logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-container .logo-container img {
|
||||
height: 17.5px;
|
||||
width: 17.5px;
|
||||
}
|
||||
|
||||
.header-container .logo-text {
|
||||
font-family: 'Work Sans', sans-serif;
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 15.4px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 17.5px;
|
||||
}
|
||||
|
||||
.header-container .get-help-container {
|
||||
display: flex;
|
||||
width: 113px;
|
||||
height: 32px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.header-container .get-help-container img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-container .get-help-text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 10px;
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.header-container .logo-text {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import './OnboardingHeader.styles.scss';
|
||||
|
||||
export function OnboardingHeader(): JSX.Element {
|
||||
return (
|
||||
<div className="header-container">
|
||||
<div className="logo-container">
|
||||
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
|
||||
<span className="logo-text">SigNoz</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { OnboardingHeader } from './OnboardingHeader';
|
||||
@@ -1,597 +0,0 @@
|
||||
.onboarding-questionaire-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-width: 1176px;
|
||||
|
||||
.onboarding-questionaire-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.onboarding-questionaire-content {
|
||||
height: calc(100vh - 56px - 60px);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
.questions-container {
|
||||
color: var(--bg-vanilla-100, #fff);
|
||||
font-family: Inter;
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
border-radius: 8px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
font-size: 24px !important;
|
||||
line-height: 32px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: var(--bg-vanilla-400) !important;
|
||||
font-size: 14px !important;
|
||||
font-style: normal;
|
||||
font-weight: 400 !important;
|
||||
line-height: 24px !important;
|
||||
margin-top: 0px !important;
|
||||
margin-bottom: 24px !important;
|
||||
}
|
||||
|
||||
.questions-form-container {
|
||||
max-width: 600px;
|
||||
width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.questions-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
min-height: 420px;
|
||||
padding: 20px 24px 24px 24px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
|
||||
.ant-form-item-label {
|
||||
label {
|
||||
color: var(--bg-vanilla-100) !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.invite-team-members-form {
|
||||
min-height: calc(420px - 24px);
|
||||
max-height: calc(420px - 24px);
|
||||
|
||||
.invite-team-members-container {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-team-members-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.ant-input-group {
|
||||
width: 100%;
|
||||
|
||||
.ant-input {
|
||||
font-size: 12px;
|
||||
|
||||
height: 32px;
|
||||
background: var(--Ink-300, #16181d);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
font-size: 11px;
|
||||
height: 32px;
|
||||
min-width: 80px;
|
||||
background: var(--Ink-300, #16181d);
|
||||
border: 1px solid var(--Greyscale-Slate-400, #1d212d);
|
||||
border-left: 0px;
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.question-label {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.question-sub-label {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.next-prev-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.ant-btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
width: 100%;
|
||||
|
||||
.ant-slider .ant-slider-mark {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.do-later-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
|
||||
.do-later-button {
|
||||
font-size: 12px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.question {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group,
|
||||
.grid,
|
||||
.tool-grid {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
gap: 10px;
|
||||
align-self: stretch;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-button,
|
||||
.grid-button,
|
||||
.tool-button {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
padding: 12px;
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
transition: background-color 0.3s ease;
|
||||
min-width: 258px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.radio-button.active,
|
||||
.grid-button.active,
|
||||
.tool-button.active,
|
||||
.radio-button:hover,
|
||||
.grid-button:hover,
|
||||
.tool-button:hover {
|
||||
border: 1px solid rgba(78, 116, 248, 0.4);
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
|
||||
.two-column-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr; /* Two equal columns */
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.onboarding-questionaire-button,
|
||||
.add-another-member-button,
|
||||
.remove-team-member-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-400);
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
transition: background-color 0.3s ease;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid rgba(78, 116, 248, 0.4);
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 1px solid rgba(78, 116, 248, 0.4);
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.add-another-member-button,
|
||||
.remove-team-member-button {
|
||||
font-size: 12px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.remove-team-member-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--bg-ink-300);
|
||||
|
||||
border-left: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
|
||||
.onboarding-questionaire-other-input {
|
||||
.ant-input-group {
|
||||
.ant-input {
|
||||
border-top-right-radius: 0px !important;
|
||||
border-bottom-right-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tool-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-100);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
min-width: 258px;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding: 8px 12px 8px 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
align-self: stretch;
|
||||
border: 0px;
|
||||
border-radius: 50px;
|
||||
margin-top: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.next-button.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 18px;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-questionaire-footer {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
padding: 12px 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.invite-team-members-add-another-member-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-questionaire-loading-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
max-width: 600px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.onboarding-questionaire-container {
|
||||
.onboarding-questionaire-content {
|
||||
.questions-container {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--bg-slate-300) !important;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: var(--bg-slate-400) !important;
|
||||
}
|
||||
|
||||
.questions-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
min-height: 420px;
|
||||
padding: 20px 24px 24px 24px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
|
||||
.ant-form-item-label {
|
||||
label {
|
||||
color: var(--bg-slate-300) !important;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.invite-team-members-form {
|
||||
.invite-team-members-container {
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-team-members-container {
|
||||
.ant-input-group {
|
||||
.ant-input {
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
font-size: 11px;
|
||||
height: 32px;
|
||||
min-width: 80px;
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
border-left: 0px;
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.question-label {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.question-sub-label {
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.question {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.radio-button,
|
||||
.grid-button,
|
||||
.tool-button {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
padding: 12px;
|
||||
color: var(--bg-slate-300);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
transition: background-color 0.3s ease;
|
||||
min-width: 258px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.radio-button.active,
|
||||
.grid-button.active,
|
||||
.tool-button.active,
|
||||
.radio-button:hover,
|
||||
.grid-button:hover,
|
||||
.tool-button:hover {
|
||||
border: 1px solid rgba(78, 116, 248, 0.4);
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
|
||||
.onboarding-questionaire-button,
|
||||
.add-another-member-button,
|
||||
.remove-team-member-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-300);
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
transition: background-color 0.3s ease;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid rgba(78, 116, 248, 0.4);
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 1px solid rgba(78, 116, 248, 0.4);
|
||||
background: rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.remove-team-member-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
border-left: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
import { Button, Slider, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowLeft, ArrowRight, Loader2, Minus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface OptimiseSignozDetails {
|
||||
logsPerDay: number;
|
||||
hostsPerDay: number;
|
||||
services: number;
|
||||
}
|
||||
|
||||
// Define exponential range
|
||||
const logsMin = 1; // Set to your minimum value in the exponential range
|
||||
const logsMax = 10000; // Set to your maximum value in the exponential range
|
||||
|
||||
const hostsMin = 1;
|
||||
const hostsMax = 10000;
|
||||
|
||||
const servicesMin = 1;
|
||||
const servicesMax = 5000;
|
||||
|
||||
// Function to convert linear slider value to exponential scale
|
||||
const linearToExponential = (
|
||||
value: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): number => {
|
||||
const expMin = Math.log10(min);
|
||||
const expMax = Math.log10(max);
|
||||
const expValue = 10 ** (expMin + ((expMax - expMin) * value) / 100);
|
||||
return Math.round(expValue);
|
||||
};
|
||||
|
||||
const exponentialToLinear = (
|
||||
expValue: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): number => {
|
||||
const expMin = Math.log10(min);
|
||||
const expMax = Math.log10(max);
|
||||
const linearValue =
|
||||
((Math.log10(expValue) - expMin) / (expMax - expMin)) * 100;
|
||||
return Math.round(linearValue); // Round to get a whole number within the 0-100 range
|
||||
};
|
||||
|
||||
interface OptimiseSignozNeedsProps {
|
||||
optimiseSignozDetails: OptimiseSignozDetails;
|
||||
setOptimiseSignozDetails: (details: OptimiseSignozDetails) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
onWillDoLater: () => void;
|
||||
isUpdatingProfile: boolean;
|
||||
isNextDisabled: boolean;
|
||||
}
|
||||
|
||||
const marks = {
|
||||
0: `${linearToExponential(0, logsMin, logsMax).toLocaleString()} GB`,
|
||||
25: `${linearToExponential(25, logsMin, logsMax).toLocaleString()} GB`,
|
||||
50: `${linearToExponential(50, logsMin, logsMax).toLocaleString()} GB`,
|
||||
75: `${linearToExponential(75, logsMin, logsMax).toLocaleString()} GB`,
|
||||
100: `${linearToExponential(100, logsMin, logsMax).toLocaleString()} GB`,
|
||||
};
|
||||
|
||||
const hostMarks = {
|
||||
0: `${linearToExponential(0, hostsMin, hostsMax).toLocaleString()}`,
|
||||
25: `${linearToExponential(25, hostsMin, hostsMax).toLocaleString()}`,
|
||||
50: `${linearToExponential(50, hostsMin, hostsMax).toLocaleString()}`,
|
||||
75: `${linearToExponential(75, hostsMin, hostsMax).toLocaleString()}`,
|
||||
100: `${linearToExponential(100, hostsMin, hostsMax).toLocaleString()}`,
|
||||
};
|
||||
|
||||
const serviceMarks = {
|
||||
0: `${linearToExponential(0, servicesMin, servicesMax).toLocaleString()}`,
|
||||
25: `${linearToExponential(25, servicesMin, servicesMax).toLocaleString()}`,
|
||||
50: `${linearToExponential(50, servicesMin, servicesMax).toLocaleString()}`,
|
||||
75: `${linearToExponential(75, servicesMin, servicesMax).toLocaleString()}`,
|
||||
100: `${linearToExponential(100, servicesMin, servicesMax).toLocaleString()}`,
|
||||
};
|
||||
|
||||
function OptimiseSignozNeeds({
|
||||
isUpdatingProfile,
|
||||
optimiseSignozDetails,
|
||||
setOptimiseSignozDetails,
|
||||
onNext,
|
||||
onBack,
|
||||
onWillDoLater,
|
||||
isNextDisabled,
|
||||
}: OptimiseSignozNeedsProps): JSX.Element {
|
||||
const [logsPerDay, setLogsPerDay] = useState<number>(
|
||||
optimiseSignozDetails?.logsPerDay || 0,
|
||||
);
|
||||
const [hostsPerDay, setHostsPerDay] = useState<number>(
|
||||
optimiseSignozDetails?.hostsPerDay || 0,
|
||||
);
|
||||
const [services, setServices] = useState<number>(
|
||||
optimiseSignozDetails?.services || 0,
|
||||
);
|
||||
|
||||
// Internal state for the linear slider
|
||||
const [sliderValues, setSliderValues] = useState({
|
||||
logsPerDay: 0,
|
||||
hostsPerDay: 0,
|
||||
services: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSliderValues({
|
||||
logsPerDay: exponentialToLinear(logsPerDay, logsMin, logsMax),
|
||||
hostsPerDay: exponentialToLinear(hostsPerDay, hostsMin, hostsMax),
|
||||
services: exponentialToLinear(services, servicesMin, servicesMax),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setOptimiseSignozDetails({
|
||||
logsPerDay,
|
||||
hostsPerDay,
|
||||
services,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [services, hostsPerDay, logsPerDay]);
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
logEvent('User Onboarding: Optimise SigNoz Needs Answered', {
|
||||
logsPerDay,
|
||||
hostsPerDay,
|
||||
services,
|
||||
});
|
||||
|
||||
onNext();
|
||||
};
|
||||
|
||||
const handleOnBack = (): void => {
|
||||
onBack();
|
||||
};
|
||||
|
||||
const handleWillDoLater = (): void => {
|
||||
setOptimiseSignozDetails({
|
||||
logsPerDay: 0,
|
||||
hostsPerDay: 0,
|
||||
services: 0,
|
||||
});
|
||||
|
||||
onWillDoLater();
|
||||
|
||||
logEvent('User Onboarding: Optimise SigNoz Needs Skipped', {
|
||||
logsPerDay: 0,
|
||||
hostsPerDay: 0,
|
||||
services: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSliderChange = (key: string, value: number): void => {
|
||||
setSliderValues({
|
||||
...sliderValues,
|
||||
[key]: value,
|
||||
});
|
||||
|
||||
switch (key) {
|
||||
case 'logsPerDay':
|
||||
setLogsPerDay(linearToExponential(value, logsMin, logsMax));
|
||||
break;
|
||||
case 'hostsPerDay':
|
||||
setHostsPerDay(linearToExponential(value, hostsMin, hostsMax));
|
||||
break;
|
||||
case 'services':
|
||||
setServices(linearToExponential(value, servicesMin, servicesMax));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate the exponential value based on the current slider position
|
||||
const logsPerDayValue = linearToExponential(
|
||||
sliderValues.logsPerDay,
|
||||
logsMin,
|
||||
logsMax,
|
||||
);
|
||||
const hostsPerDayValue = linearToExponential(
|
||||
sliderValues.hostsPerDay,
|
||||
hostsMin,
|
||||
hostsMax,
|
||||
);
|
||||
const servicesValue = linearToExponential(
|
||||
sliderValues.services,
|
||||
servicesMin,
|
||||
servicesMax,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="questions-container">
|
||||
<Typography.Title level={3} className="title">
|
||||
Optimize SigNoz for Your Needs
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="sub-title">
|
||||
Give us a quick sense of your scale so SigNoz can keep up!
|
||||
</Typography.Paragraph>
|
||||
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form">
|
||||
<Typography.Paragraph className="question">
|
||||
What does your scale approximately look like?
|
||||
</Typography.Paragraph>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="organisationName">
|
||||
Logs / Day
|
||||
</label>
|
||||
<div className="slider-container">
|
||||
<div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={sliderValues.logsPerDay}
|
||||
marks={marks}
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('logsPerDay', value)
|
||||
}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${logsPerDayValue.toLocaleString()} GB`, // Show whole number
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="organisationName">
|
||||
Metrics <Minus size={14} /> Number of Hosts
|
||||
</label>
|
||||
<div className="slider-container">
|
||||
<div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={sliderValues.hostsPerDay}
|
||||
marks={hostMarks}
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('hostsPerDay', value)
|
||||
}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${hostsPerDayValue.toLocaleString()}`, // Show whole number
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="organisationName">
|
||||
Number of services
|
||||
</label>
|
||||
<div className="slider-container">
|
||||
<div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={sliderValues.services}
|
||||
marks={serviceMarks}
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('services', value)
|
||||
}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${servicesValue.toLocaleString()}`, // Show whole number
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="next-prev-container">
|
||||
<Button
|
||||
type="default"
|
||||
className="next-button"
|
||||
onClick={handleOnBack}
|
||||
disabled={isUpdatingProfile}
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
className="next-button"
|
||||
onClick={handleOnNext}
|
||||
disabled={isUpdatingProfile || isNextDisabled}
|
||||
>
|
||||
Next{' '}
|
||||
{isUpdatingProfile ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<ArrowRight size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="do-later-container">
|
||||
<Button type="link" onClick={handleWillDoLater}>
|
||||
I'll do this later
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptimiseSignozNeeds;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user