Compare commits
61 Commits
v0.42.0-qu
...
v0.46.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cbaa17d9f | ||
|
|
30bfad527f | ||
|
|
9f1c45bc32 | ||
|
|
51becf7cfb | ||
|
|
7460e650af | ||
|
|
211fe4fdd5 | ||
|
|
e2992b42c1 | ||
|
|
3957d91a9b | ||
|
|
967aa16f21 | ||
|
|
08b1a87cb5 | ||
|
|
03ddcdd20e | ||
|
|
1aec7f3ca6 | ||
|
|
241edcb88a | ||
|
|
27d12871af | ||
|
|
e78e1d4b63 | ||
|
|
64bf580323 | ||
|
|
152aa4b518 | ||
|
|
b3d5831574 | ||
|
|
b85b9f42ed | ||
|
|
5c1c09c790 | ||
|
|
33960b05fd | ||
|
|
191d9b0648 | ||
|
|
7d81bc3417 | ||
|
|
506916661d | ||
|
|
5326f2d23b | ||
|
|
dfaa344dce | ||
|
|
882b540a0b | ||
|
|
1c4b579c3d | ||
|
|
706f25cc5d | ||
|
|
e6e0a59f5f | ||
|
|
b2c170c752 | ||
|
|
453be9074d | ||
|
|
3272444e13 | ||
|
|
71b3e6d522 | ||
|
|
6cf7cc9f4f | ||
|
|
5ec2f17d09 | ||
|
|
a45fb8ec0c | ||
|
|
bd148bbd5a | ||
|
|
1306e99ca8 | ||
|
|
1a8f063b4b | ||
|
|
c7668b9a78 | ||
|
|
5e3dba2587 | ||
|
|
374f30e0cd | ||
|
|
38d2833931 | ||
|
|
731eacbbca | ||
|
|
a63bb139bf | ||
|
|
a140bef0e6 | ||
|
|
48e5436167 | ||
|
|
0fc664a387 | ||
|
|
5817d50652 | ||
|
|
bb318cf52a | ||
|
|
ec0185da61 | ||
|
|
fc2bdb610f | ||
|
|
a9464de62d | ||
|
|
57bfdedfe1 | ||
|
|
7bdc9c0cb0 | ||
|
|
0d5934d56b | ||
|
|
3a5a61aff9 | ||
|
|
a54b7baa7d | ||
|
|
cd63dd972d | ||
|
|
389058b9b4 |
31
.github/workflows/jest-coverage-changes.yml
vendored
Normal file
31
.github/workflows/jest-coverage-changes.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Jest Coverage - changed files
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: "refs/heads/develop"
|
||||
token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication
|
||||
|
||||
- name: Fetch branch
|
||||
run: git fetch origin ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- run: |
|
||||
git checkout ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd frontend && npm install -g yarn && yarn
|
||||
|
||||
- name: npm run test:changedsince
|
||||
run: cd frontend && npm run i18n:generate-hash && npm run test:changedsince
|
||||
34
.github/workflows/staging-deployment.yaml
vendored
34
.github/workflows/staging-deployment.yaml
vendored
@@ -9,19 +9,29 @@ jobs:
|
||||
name: Deploy latest develop branch to staging
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
steps:
|
||||
- name: Executing remote ssh commands using ssh key
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
env:
|
||||
GITHUB_BRANCH: develop
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
- id: 'auth'
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
with:
|
||||
host: ${{ secrets.HOST_DNS }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
envs: GITHUB_BRANCH,GITHUB_SHA
|
||||
command_timeout: 60m
|
||||
script: |
|
||||
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
|
||||
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: 'sdk'
|
||||
uses: 'google-github-actions/setup-gcloud@v2'
|
||||
|
||||
- name: 'ssh'
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||
run: |
|
||||
read -r -d '' COMMAND <<EOF || true
|
||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
@@ -40,3 +50,5 @@ jobs:
|
||||
make build-ee-query-service-amd64
|
||||
make build-frontend-amd64
|
||||
make run-signoz
|
||||
EOF
|
||||
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||
|
||||
30
.github/workflows/testing-deployment.yaml
vendored
30
.github/workflows/testing-deployment.yaml
vendored
@@ -9,19 +9,29 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: testing
|
||||
if: ${{ github.event.label.name == 'testing-deploy' }}
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
steps:
|
||||
- name: Executing remote ssh commands using ssh key
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
- id: 'auth'
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
with:
|
||||
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
|
||||
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: 'sdk'
|
||||
uses: 'google-github-actions/setup-gcloud@v2'
|
||||
|
||||
- name: 'ssh'
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
with:
|
||||
host: ${{ secrets.HOST_DNS }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
envs: GITHUB_BRANCH,GITHUB_SHA
|
||||
command_timeout: 60m
|
||||
script: |
|
||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||
run: |
|
||||
read -r -d '' COMMAND <<EOF || true
|
||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
@@ -41,3 +51,5 @@ jobs:
|
||||
make build-ee-query-service-amd64
|
||||
make build-frontend-amd64
|
||||
make run-signoz
|
||||
EOF
|
||||
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||
|
||||
@@ -22,7 +22,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
||||
"wget",
|
||||
"--spider",
|
||||
"-q",
|
||||
"localhost:8123/ping"
|
||||
"0.0.0.0:8123/ping"
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
@@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.42.0
|
||||
image: signoz/query-service:0.44.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.42.0
|
||||
image: signoz/frontend:0.44.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.88.17
|
||||
image: signoz/signoz-otel-collector:0.88.21
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.88.17
|
||||
image: signoz/signoz-schema-migrator:0.88.21
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -111,18 +111,18 @@ processors:
|
||||
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
datasource: tcp://clickhouse:9000/signoz_traces
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
clickhousemetricswrite/prometheus:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
# logging: {}
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
timeout: 10s
|
||||
extensions:
|
||||
|
||||
@@ -22,4 +22,4 @@ rule_files:
|
||||
scrape_configs: []
|
||||
|
||||
remote_read:
|
||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
- url: tcp://clickhouse:9000/signoz_metrics
|
||||
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
"wget",
|
||||
"--spider",
|
||||
"-q",
|
||||
"localhost:8123/ping"
|
||||
"0.0.0.0:8123/ping"
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.17}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.21}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.88.17
|
||||
image: signoz/signoz-otel-collector:0.88.21
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
|
||||
@@ -21,7 +21,7 @@ x-clickhouse-defaults: &clickhouse-defaults
|
||||
"wget",
|
||||
"--spider",
|
||||
"-q",
|
||||
"localhost:8123/ping"
|
||||
"0.0.0.0:8123/ping"
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
@@ -164,7 +164,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.42.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.44.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.42.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.44.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.17}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.21}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.17}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.21}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
|
||||
@@ -122,21 +122,20 @@ extensions:
|
||||
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
datasource: tcp://clickhouse:9000/?database=signoz_traces
|
||||
datasource: tcp://clickhouse:9000/signoz_traces
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
resource_to_telemetry_conversion:
|
||||
enabled: true
|
||||
clickhousemetricswrite/prometheus:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
# logging: {}
|
||||
|
||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
dsn: tcp://clickhouse:9000/signoz_logs
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
timeout: 10s
|
||||
# logging: {}
|
||||
|
||||
service:
|
||||
telemetry:
|
||||
|
||||
@@ -22,4 +22,4 @@ rule_files:
|
||||
scrape_configs: []
|
||||
|
||||
remote_read:
|
||||
- url: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
- url: tcp://clickhouse:9000/signoz_metrics
|
||||
|
||||
@@ -152,7 +152,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
|
||||
|
||||
// PAT APIs
|
||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"go.signoz.io/signoz/pkg/query-service/app/metrics"
|
||||
"go.signoz.io/signoz/pkg/query-service/app/parser"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||||
querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) {
|
||||
if !ah.CheckFeature(basemodel.CustomMetricsFunction) {
|
||||
zap.L().Info("CustomMetricsFunction feature is not enabled in this plan")
|
||||
ah.APIHandler.QueryRangeMetricsV2(w, r)
|
||||
return
|
||||
}
|
||||
metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r)
|
||||
|
||||
if apiErrorObj != nil {
|
||||
zap.L().Error("Error in parsing metric query params", zap.Error(apiErrorObj.Err))
|
||||
RespondError(w, apiErrorObj, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// prometheus instant query needs same timestamp
|
||||
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
|
||||
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.PROM {
|
||||
metricsQueryRangeParams.Start = metricsQueryRangeParams.End
|
||||
}
|
||||
|
||||
// round up the end to nearest multiple
|
||||
if metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER {
|
||||
end := (metricsQueryRangeParams.End) / 1000
|
||||
step := metricsQueryRangeParams.Step
|
||||
metricsQueryRangeParams.End = (end / step * step) * 1000
|
||||
}
|
||||
|
||||
type channelResult struct {
|
||||
Series []*basemodel.Series
|
||||
TableName string
|
||||
Err error
|
||||
Name string
|
||||
Query string
|
||||
}
|
||||
|
||||
execClickHouseQueries := func(queries map[string]string) ([]*basemodel.Series, []string, error, map[string]string) {
|
||||
var seriesList []*basemodel.Series
|
||||
var tableName []string
|
||||
ch := make(chan channelResult, len(queries))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for name, query := range queries {
|
||||
wg.Add(1)
|
||||
go func(name, query string) {
|
||||
defer wg.Done()
|
||||
seriesList, tableName, err := ah.opts.DataConnector.GetMetricResultEE(r.Context(), query)
|
||||
for _, series := range seriesList {
|
||||
series.QueryName = name
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query}
|
||||
return
|
||||
}
|
||||
ch <- channelResult{Series: seriesList, TableName: tableName}
|
||||
}(name, query)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
|
||||
var errs []error
|
||||
errQuriesByName := make(map[string]string)
|
||||
// read values from the channel
|
||||
for r := range ch {
|
||||
if r.Err != nil {
|
||||
errs = append(errs, r.Err)
|
||||
errQuriesByName[r.Name] = r.Query
|
||||
continue
|
||||
}
|
||||
seriesList = append(seriesList, r.Series...)
|
||||
tableName = append(tableName, r.TableName)
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
return nil, nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
|
||||
}
|
||||
return seriesList, tableName, nil, nil
|
||||
}
|
||||
|
||||
execPromQueries := func(metricsQueryRangeParams *basemodel.QueryRangeParamsV2) ([]*basemodel.Series, error, map[string]string) {
|
||||
var seriesList []*basemodel.Series
|
||||
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for name, query := range metricsQueryRangeParams.CompositeMetricQuery.PromQueries {
|
||||
if query.Disabled {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(name string, query *basemodel.PromQuery) {
|
||||
var seriesList []*basemodel.Series
|
||||
defer wg.Done()
|
||||
tmpl := template.New("promql-query")
|
||||
tmpl, tmplErr := tmpl.Parse(query.Query)
|
||||
if tmplErr != nil {
|
||||
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
|
||||
return
|
||||
}
|
||||
var queryBuf bytes.Buffer
|
||||
tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables)
|
||||
if tmplErr != nil {
|
||||
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
|
||||
return
|
||||
}
|
||||
query.Query = queryBuf.String()
|
||||
queryModel := basemodel.QueryRangeParams{
|
||||
Start: time.UnixMilli(metricsQueryRangeParams.Start),
|
||||
End: time.UnixMilli(metricsQueryRangeParams.End),
|
||||
Step: time.Duration(metricsQueryRangeParams.Step * int64(time.Second)),
|
||||
Query: query.Query,
|
||||
}
|
||||
promResult, _, err := ah.opts.DataConnector.GetQueryRangeResult(r.Context(), &queryModel)
|
||||
if err != nil {
|
||||
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query}
|
||||
return
|
||||
}
|
||||
matrix, _ := promResult.Matrix()
|
||||
for _, v := range matrix {
|
||||
var s basemodel.Series
|
||||
s.QueryName = name
|
||||
s.Labels = v.Metric.Copy().Map()
|
||||
for _, p := range v.Floats {
|
||||
s.Points = append(s.Points, basemodel.MetricPoint{Timestamp: p.T, Value: p.F})
|
||||
}
|
||||
seriesList = append(seriesList, &s)
|
||||
}
|
||||
ch <- channelResult{Series: seriesList}
|
||||
}(name, query)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
|
||||
var errs []error
|
||||
errQuriesByName := make(map[string]string)
|
||||
// read values from the channel
|
||||
for r := range ch {
|
||||
if r.Err != nil {
|
||||
errs = append(errs, r.Err)
|
||||
errQuriesByName[r.Name] = r.Query
|
||||
continue
|
||||
}
|
||||
seriesList = append(seriesList, r.Series...)
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
|
||||
}
|
||||
return seriesList, nil, nil
|
||||
}
|
||||
|
||||
var seriesList []*basemodel.Series
|
||||
var tableName []string
|
||||
var err error
|
||||
var errQuriesByName map[string]string
|
||||
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
|
||||
case basemodel.QUERY_BUILDER:
|
||||
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME)
|
||||
if runQueries.Err != nil {
|
||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: runQueries.Err}, nil)
|
||||
return
|
||||
}
|
||||
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(runQueries.Queries)
|
||||
|
||||
case basemodel.CLICKHOUSE:
|
||||
queries := make(map[string]string)
|
||||
|
||||
for name, chQuery := range metricsQueryRangeParams.CompositeMetricQuery.ClickHouseQueries {
|
||||
if chQuery.Disabled {
|
||||
continue
|
||||
}
|
||||
tmpl := template.New("clickhouse-query")
|
||||
tmpl, err := tmpl.Parse(chQuery.Query)
|
||||
if err != nil {
|
||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
var query bytes.Buffer
|
||||
|
||||
// replace go template variables
|
||||
querytemplate.AssignReservedVars(metricsQueryRangeParams)
|
||||
|
||||
err = tmpl.Execute(&query, metricsQueryRangeParams.Variables)
|
||||
if err != nil {
|
||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
queries[name] = query.String()
|
||||
}
|
||||
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(queries)
|
||||
case basemodel.PROM:
|
||||
seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams)
|
||||
default:
|
||||
err = fmt.Errorf("invalid query type")
|
||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, errQuriesByName)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
apiErrObj := &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}
|
||||
RespondError(w, apiErrObj, errQuriesByName)
|
||||
return
|
||||
}
|
||||
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
|
||||
len(seriesList) > 1 &&
|
||||
(metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER ||
|
||||
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.CLICKHOUSE) {
|
||||
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: fmt.Errorf("invalid: query resulted in more than one series for value type")}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
type ResponseFormat struct {
|
||||
ResultType string `json:"resultType"`
|
||||
Result []*basemodel.Series `json:"result"`
|
||||
TableName []string `json:"tableName"`
|
||||
}
|
||||
resp := ResponseFormat{ResultType: "matrix", Result: seriesList, TableName: tableName}
|
||||
ah.Respond(w, resp)
|
||||
}
|
||||
@@ -329,7 +329,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
r.Use(loggingMiddleware)
|
||||
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
apiHandler.RegisterMetricsRoutes(r, am)
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
|
||||
@@ -52,14 +52,14 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
Name: basemodel.QueryBuilderPanels,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: 20,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderAlerts,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: 10,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM nginx:1.25.2-alpine
|
||||
FROM nginx:1.26-alpine
|
||||
|
||||
# Add Maintainer Info
|
||||
LABEL maintainer="signoz"
|
||||
|
||||
@@ -4,6 +4,7 @@ const config: Config.InitialOptions = {
|
||||
clearMocks: true,
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'cobertura', 'html', 'json-summary'],
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
|
||||
modulePathIgnorePatterns: ['dist'],
|
||||
moduleNameMapper: {
|
||||
@@ -35,6 +36,14 @@ const config: Config.InitialOptions = {
|
||||
browsers: ['chromium', 'firefox', 'webkit'],
|
||||
},
|
||||
},
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 80,
|
||||
branches: 65,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"playwright:codegen:local": "playwright codegen http://localhost:3301",
|
||||
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
|
||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||
"commitlint": "commitlint --edit $1"
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest --coverage",
|
||||
"test:changedsince": "jest --changedSince=develop --coverage --silent"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.15.0"
|
||||
@@ -44,11 +46,14 @@
|
||||
"@sentry/webpack-plugin": "2.16.0",
|
||||
"@signozhq/design-tokens": "0.0.8",
|
||||
"@uiw/react-md-editor": "3.23.5",
|
||||
"@visx/group": "3.3.0",
|
||||
"@visx/shape": "3.5.0",
|
||||
"@visx/tooltip": "3.3.0",
|
||||
"@xstate/react": "^3.0.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"antd": "5.11.0",
|
||||
"antd-table-saveas-excel": "2.2.1",
|
||||
"axios": "1.6.2",
|
||||
"axios": "1.6.4",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^29.6.4",
|
||||
"babel-loader": "9.1.3",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"button_test_channel": "Test",
|
||||
"button_return": "Back",
|
||||
"field_channel_name": "Name",
|
||||
"field_send_resolved": "Send resolved alerts",
|
||||
"field_channel_type": "Type",
|
||||
"field_webhook_url": "Webhook URL",
|
||||
"field_slack_recipient": "Recipient",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"new_dashboard_title": "Sample Title",
|
||||
"layout_saved_successfully": "Layout saved successfully",
|
||||
"add_panel": "Add Panel",
|
||||
"add_row": "Add Row",
|
||||
"save_layout": "Save Layout",
|
||||
"variable_updated_successfully": "Variable updated successfully",
|
||||
"error_while_updating_variable": "Error while updating variable",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"button_test_channel": "Test",
|
||||
"button_return": "Back",
|
||||
"field_channel_name": "Name",
|
||||
"field_send_resolved": "Send resolved alerts",
|
||||
"field_channel_type": "Type",
|
||||
"field_webhook_url": "Webhook URL",
|
||||
"field_slack_recipient": "Recipient",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"new_dashboard_title": "Sample Title",
|
||||
"layout_saved_successfully": "Layout saved successfully",
|
||||
"add_panel": "Add Panel",
|
||||
"add_row": "Add Row",
|
||||
"save_layout": "Save Layout",
|
||||
"full_view": "Full Screen View",
|
||||
"variable_updated_successfully": "Variable updated successfully",
|
||||
|
||||
@@ -12,7 +12,7 @@ const create = async (
|
||||
name: props.name,
|
||||
email_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
to: props.to,
|
||||
html: props.html,
|
||||
headers: props.headers,
|
||||
|
||||
@@ -12,7 +12,7 @@ const create = async (
|
||||
name: props.name,
|
||||
msteams_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
webhook_url: props.webhook_url,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
|
||||
@@ -12,7 +12,7 @@ const create = async (
|
||||
name: props.name,
|
||||
pagerduty_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
routing_key: props.routing_key,
|
||||
client: props.client,
|
||||
client_url: props.client_url,
|
||||
|
||||
@@ -12,7 +12,7 @@ const create = async (
|
||||
name: props.name,
|
||||
slack_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
api_url: props.api_url,
|
||||
channel: props.channel,
|
||||
title: props.title,
|
||||
|
||||
@@ -30,7 +30,7 @@ const create = async (
|
||||
name: props.name,
|
||||
webhook_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
url: props.api_url,
|
||||
http_config: httpConfig,
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ const editEmail = async (
|
||||
name: props.name,
|
||||
email_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
to: props.to,
|
||||
html: props.html,
|
||||
headers: props.headers,
|
||||
|
||||
@@ -12,7 +12,7 @@ const editMsTeams = async (
|
||||
name: props.name,
|
||||
msteams_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
webhook_url: props.webhook_url,
|
||||
title: props.title,
|
||||
text: props.text,
|
||||
|
||||
@@ -12,7 +12,7 @@ const editOpsgenie = async (
|
||||
name: props.name,
|
||||
opsgenie_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
api_key: props.api_key,
|
||||
description: props.description,
|
||||
priority: props.priority,
|
||||
|
||||
@@ -12,7 +12,7 @@ const editPager = async (
|
||||
name: props.name,
|
||||
pagerduty_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
routing_key: props.routing_key,
|
||||
client: props.client,
|
||||
client_url: props.client_url,
|
||||
|
||||
@@ -12,7 +12,7 @@ const editSlack = async (
|
||||
name: props.name,
|
||||
slack_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
api_url: props.api_url,
|
||||
channel: props.channel,
|
||||
title: props.title,
|
||||
|
||||
@@ -29,7 +29,7 @@ const editWebhook = async (
|
||||
name: props.name,
|
||||
webhook_configs: [
|
||||
{
|
||||
send_resolved: true,
|
||||
send_resolved: props.send_resolved,
|
||||
url: props.api_url,
|
||||
http_config: httpConfig,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ApiV4Instance } from 'api';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { MetricMetaProps } from 'types/api/metrics/getApDex';
|
||||
|
||||
@@ -6,4 +6,6 @@ export const getMetricMeta = (
|
||||
metricName: string,
|
||||
servicename: string,
|
||||
): Promise<AxiosResponse<MetricMetaProps>> =>
|
||||
axios.get(`/metric_meta?metricName=${metricName}&serviceName=${servicename}`);
|
||||
ApiV4Instance.get(
|
||||
`/metric/metric_metadata?metricName=${metricName}&serviceName=${servicename}`,
|
||||
);
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
MetricNameProps,
|
||||
MetricNamesPayloadProps,
|
||||
} from 'types/api/metrics/getMetricName';
|
||||
|
||||
export const getMetricName = async (
|
||||
props: MetricNameProps,
|
||||
): Promise<SuccessResponse<MetricNamesPayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/metrics/autocomplete/list?match=${props || ''}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ApiV3Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
TagKeyProps,
|
||||
@@ -8,15 +9,19 @@ import {
|
||||
TagValueProps,
|
||||
TagValuesPayloadProps,
|
||||
} from 'types/api/metrics/getResourceAttributes';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
|
||||
export const getResourceAttributesTagKeys = async (
|
||||
props: TagKeyProps,
|
||||
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/metrics/autocomplete/tagKey?metricName=${props.metricName}${
|
||||
props.match ? `&match=${props.match}` : ''
|
||||
}`,
|
||||
`/autocomplete/attribute_keys?${createQueryParams({
|
||||
aggregateOperator: MetricAggregateOperator.RATE,
|
||||
searchText: props.match,
|
||||
dataSource: DataSource.METRICS,
|
||||
aggregateAttribute: props.metricName,
|
||||
})}`,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -35,7 +40,13 @@ export const getResourceAttributesTagValues = async (
|
||||
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/metrics/autocomplete/tagValue?metricName=${props.metricName}&tagKey=${props.tagKey}`,
|
||||
`/autocomplete/attribute_values?${createQueryParams({
|
||||
aggregateOperator: MetricAggregateOperator.RATE,
|
||||
dataSource: DataSource.METRICS,
|
||||
aggregateAttribute: props.metricName,
|
||||
attributeKey: props.tagKey,
|
||||
searchText: '',
|
||||
})}`,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -157,8 +157,8 @@ function ListLogView({
|
||||
const timestampValue = useMemo(
|
||||
() =>
|
||||
typeof flattenLogData.timestamp === 'string'
|
||||
? dayjs(flattenLogData.timestamp).format()
|
||||
: dayjs(flattenLogData.timestamp / 1e6).format(),
|
||||
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
|
||||
[flattenLogData.timestamp],
|
||||
);
|
||||
|
||||
|
||||
@@ -90,12 +90,12 @@ function RawLogView({
|
||||
const text = useMemo(
|
||||
() =>
|
||||
typeof data.timestamp === 'string'
|
||||
? `${dayjs(data.timestamp).format()} | ${attributesText} ${severityText} ${
|
||||
data.body
|
||||
}`
|
||||
: `${dayjs(
|
||||
data.timestamp / 1e6,
|
||||
).format()} | ${attributesText} ${severityText} ${data.body}`,
|
||||
? `${dayjs(data.timestamp).format(
|
||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||
)} | ${attributesText} ${severityText} ${data.body}`
|
||||
: `${dayjs(data.timestamp / 1e6).format(
|
||||
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||
)} | ${attributesText} ${severityText} ${data.body}`,
|
||||
[data.timestamp, data.body, severityText, attributesText],
|
||||
);
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const date =
|
||||
typeof field === 'string'
|
||||
? dayjs(field).format()
|
||||
: dayjs(field / 1e6).format();
|
||||
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
return {
|
||||
children: (
|
||||
<div className="table-timestamp">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import './DynamicColumnTable.syles.scss';
|
||||
|
||||
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
||||
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { SlidersHorizontal } from 'lucide-react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
@@ -20,6 +21,7 @@ function DynamicColumnTable({
|
||||
columns,
|
||||
dynamicColumns,
|
||||
onDragColumn,
|
||||
facingIssueBtn,
|
||||
...restProps
|
||||
}: DynamicColumnTableProps): JSX.Element {
|
||||
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
|
||||
@@ -83,6 +85,8 @@ function DynamicColumnTable({
|
||||
|
||||
return (
|
||||
<div className="DynamicColumnTable">
|
||||
<Flex justify="flex-end" align="center" gap={8}>
|
||||
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
|
||||
{dynamicColumns && (
|
||||
<Dropdown
|
||||
getPopupContainer={popupContainer}
|
||||
@@ -96,6 +100,7 @@ function DynamicColumnTable({
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<ResizeTable
|
||||
columns={columnsData}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { TableProps } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
|
||||
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
|
||||
import { TableDataSource } from './contants';
|
||||
|
||||
@@ -12,6 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
|
||||
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
|
||||
dynamicColumns: TableProps<any>['columns'];
|
||||
onDragColumn?: (fromIndex: number, toIndex: number) => void;
|
||||
facingIssueBtn?: FacingIssueBtnProps;
|
||||
}
|
||||
|
||||
export type GetVisibleColumnsFunction = (
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.facing-issue-button {
|
||||
color: var(--bg-amber-500);
|
||||
border-color: var(--bg-amber-500);
|
||||
|
||||
.ant-btn:hover {
|
||||
color: var(--bg-amber-400) !important;
|
||||
border-color: var(--bg-amber-300) !important;
|
||||
}
|
||||
}
|
||||
62
frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx
Normal file
62
frontend/src/components/facingIssueBtn/FacingIssueBtn.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import './FacingIssueBtn.style.scss';
|
||||
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
export interface FacingIssueBtnProps {
|
||||
eventName: string;
|
||||
attributes: Record<string, unknown>;
|
||||
message?: string;
|
||||
buttonText?: string;
|
||||
className?: string;
|
||||
onHoverText?: string;
|
||||
}
|
||||
|
||||
function FacingIssueBtn({
|
||||
attributes,
|
||||
eventName,
|
||||
message = '',
|
||||
buttonText = '',
|
||||
className = '',
|
||||
onHoverText = '',
|
||||
}: FacingIssueBtnProps): JSX.Element | null {
|
||||
const handleFacingIssuesClick = (): void => {
|
||||
logEvent(eventName, attributes);
|
||||
|
||||
if (window.Intercom) {
|
||||
window.Intercom('showNewMessage', defaultTo(message, ''));
|
||||
}
|
||||
};
|
||||
|
||||
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
|
||||
const isCloudUserVal = isCloudUser();
|
||||
|
||||
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
|
||||
<div className="facing-issue-button">
|
||||
<Tooltip title={onHoverText} autoAdjustOverflow>
|
||||
<Button
|
||||
className={cx('periscope-btn', 'facing-issue-button', className)}
|
||||
onClick={handleFacingIssuesClick}
|
||||
icon={<HelpCircle size={14} />}
|
||||
>
|
||||
{buttonText || 'Facing issues?'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
FacingIssueBtn.defaultProps = {
|
||||
message: '',
|
||||
buttonText: '',
|
||||
className: '',
|
||||
onHoverText: '',
|
||||
};
|
||||
|
||||
export default FacingIssueBtn;
|
||||
57
frontend/src/components/facingIssueBtn/util.ts
Normal file
57
frontend/src/components/facingIssueBtn/util.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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 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`;
|
||||
@@ -29,6 +29,7 @@ export const getComponentForPanelType = (
|
||||
[PANEL_TYPES.LIST]:
|
||||
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
|
||||
[PANEL_TYPES.BAR]: Uplot,
|
||||
[PANEL_TYPES.PIE]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -30,4 +30,5 @@ export enum QueryParams {
|
||||
integration = 'integration',
|
||||
pagination = 'pagination',
|
||||
relativeTime = 'relativeTime',
|
||||
alertType = 'alertType',
|
||||
}
|
||||
|
||||
@@ -285,9 +285,15 @@ export enum PANEL_TYPES {
|
||||
LIST = 'list',
|
||||
TRACE = 'trace',
|
||||
BAR = 'bar',
|
||||
PIE = 'pie',
|
||||
EMPTY_WIDGET = 'EMPTY_WIDGET',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export enum PANEL_GROUP_TYPES {
|
||||
ROW = 'row',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export enum ATTRIBUTE_TYPES {
|
||||
SUM = 'Sum',
|
||||
|
||||
@@ -315,7 +315,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
className={cx(
|
||||
'app-layout',
|
||||
isDarkMode ? 'darkMode' : 'lightMode',
|
||||
!collapsed ? 'docked' : '',
|
||||
!collapsed && !renderFullScreen ? 'docked' : '',
|
||||
)}
|
||||
>
|
||||
{isToDisplayLayout && !renderFullScreen && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.billing-container {
|
||||
margin-bottom: 40px;
|
||||
padding-top: 36px;
|
||||
width: 65%;
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ function CreateAlertChannels({
|
||||
EmailChannel
|
||||
>
|
||||
>({
|
||||
send_resolved: true,
|
||||
text: `{{ range .Alerts -}}
|
||||
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||
|
||||
@@ -119,7 +120,7 @@ function CreateAlertChannels({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
channel: selectedConfig?.channel || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
}),
|
||||
@@ -158,7 +159,7 @@ function CreateAlertChannels({
|
||||
let request: WebhookChannel = {
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
};
|
||||
|
||||
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
|
||||
@@ -226,7 +227,7 @@ function CreateAlertChannels({
|
||||
|
||||
return {
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
routing_key: selectedConfig?.routing_key || '',
|
||||
client: selectedConfig?.client || '',
|
||||
client_url: selectedConfig?.client_url || '',
|
||||
@@ -274,7 +275,7 @@ function CreateAlertChannels({
|
||||
() => ({
|
||||
api_key: selectedConfig?.api_key || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
description: selectedConfig?.description || '',
|
||||
message: selectedConfig?.message || '',
|
||||
priority: selectedConfig?.priority || '',
|
||||
@@ -312,7 +313,7 @@ function CreateAlertChannels({
|
||||
const prepareEmailRequest = useCallback(
|
||||
() => ({
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
to: selectedConfig?.to || '',
|
||||
html: selectedConfig?.html || '',
|
||||
headers: selectedConfig?.headers || {},
|
||||
@@ -350,7 +351,7 @@ function CreateAlertChannels({
|
||||
() => ({
|
||||
webhook_url: selectedConfig?.webhook_url || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Form, Row } from 'antd';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import history from 'lib/history';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -18,15 +20,25 @@ import SelectAlertType from './SelectAlertType';
|
||||
|
||||
function CreateRules(): JSX.Element {
|
||||
const [initValues, setInitValues] = useState<AlertDef | null>(null);
|
||||
const [alertType, setAlertType] = useState<AlertTypes>(
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const version = queryParams.get('version');
|
||||
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
function getAlertTypeFromDataSource(): AlertTypes | null {
|
||||
if (!compositeQuery) {
|
||||
return null;
|
||||
}
|
||||
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
|
||||
|
||||
return ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
||||
}
|
||||
|
||||
const [alertType, setAlertType] = useState<AlertTypes>(
|
||||
(alertTypeFromParams as AlertTypes) || getAlertTypeFromDataSource(),
|
||||
);
|
||||
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
@@ -48,21 +60,17 @@ function CreateRules(): JSX.Element {
|
||||
version: version || ENTITY_VERSION_V4,
|
||||
});
|
||||
}
|
||||
queryParams.set(QueryParams.alertType, typ);
|
||||
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!compositeQuery) {
|
||||
return;
|
||||
}
|
||||
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
|
||||
|
||||
const alertType = ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
||||
|
||||
if (alertType) {
|
||||
onSelectType(alertType);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [compositeQuery]);
|
||||
}, [alertType]);
|
||||
|
||||
if (!initValues) {
|
||||
return (
|
||||
|
||||
@@ -72,7 +72,7 @@ function EditAlertChannels({
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
channel: selectedConfig?.channel || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
id,
|
||||
@@ -115,7 +115,7 @@ function EditAlertChannels({
|
||||
return {
|
||||
api_url: selectedConfig?.api_url || '',
|
||||
name: name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
username,
|
||||
password,
|
||||
id,
|
||||
@@ -284,7 +284,7 @@ function EditAlertChannels({
|
||||
() => ({
|
||||
webhook_url: selectedConfig?.webhook_url || '',
|
||||
name: selectedConfig?.name || '',
|
||||
send_resolved: true,
|
||||
send_resolved: selectedConfig?.send_resolved || false,
|
||||
text: selectedConfig?.text || '',
|
||||
title: selectedConfig?.title || '',
|
||||
id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, FormInstance, Input, Select, Typography } from 'antd';
|
||||
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
|
||||
import { Store } from 'antd/lib/form/interface';
|
||||
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -95,6 +95,22 @@ function FormAlertChannels({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('field_send_resolved')}
|
||||
labelAlign="left"
|
||||
name="send_resolved"
|
||||
>
|
||||
<Switch
|
||||
defaultChecked={initialValue?.send_resolved}
|
||||
onChange={(value): void => {
|
||||
setSelectedConfig((state) => ({
|
||||
...state,
|
||||
send_resolved: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
|
||||
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
|
||||
<Select.Option value="slack" key="slack">
|
||||
|
||||
@@ -43,3 +43,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.facing-issue-btn {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
} from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import testAlertApi from 'api/alerts/testAlert';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { alertHelpMessage } from 'components/facingIssueBtn/util';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -138,6 +140,11 @@ function FormAlertRules({
|
||||
|
||||
useEffect(() => {
|
||||
// Set selectedQueryName based on the length of queryOptions
|
||||
const selectedQueryName = alertDef?.condition?.selectedQueryName;
|
||||
if (
|
||||
!selectedQueryName ||
|
||||
!queryOptions.some((option) => option.value === selectedQueryName)
|
||||
) {
|
||||
setAlertDef((def) => ({
|
||||
...def,
|
||||
condition: {
|
||||
@@ -146,7 +153,8 @@ function FormAlertRules({
|
||||
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
|
||||
},
|
||||
}));
|
||||
}, [currentQuery?.queryType, queryOptions]);
|
||||
}
|
||||
}, [alertDef, currentQuery?.queryType, queryOptions]);
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
@@ -482,6 +490,8 @@ function FormAlertRules({
|
||||
alertDef?.broadcastToAll ||
|
||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
||||
|
||||
const isRuleCreated = !ruleId || ruleId === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
@@ -514,6 +524,7 @@ function FormAlertRules({
|
||||
runQuery={handleRunQuery}
|
||||
alertDef={alertDef}
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
key={currentQuery.queryType}
|
||||
/>
|
||||
|
||||
<RuleOptions
|
||||
@@ -563,6 +574,22 @@ function FormAlertRules({
|
||||
</StyledLeftContainer>
|
||||
<Col flex="1 1 300px">
|
||||
<UserGuide queryType={currentQuery.queryType} />
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
alert: alertDef?.alert,
|
||||
alertType: alertDef?.alertType,
|
||||
id: ruleId,
|
||||
ruleType: alertDef?.ruleType,
|
||||
state: (alertDef as any)?.state,
|
||||
panelType,
|
||||
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
|
||||
}}
|
||||
className="facing-issue-btn"
|
||||
eventName="Alert: Facing Issues in alert"
|
||||
buttonText="Need help with this alert?"
|
||||
message={alertHelpMessage(alertDef, ruleId)}
|
||||
onHoverText="Click here to get help with this alert"
|
||||
/>
|
||||
</Col>
|
||||
</PanelContainer>
|
||||
</>
|
||||
|
||||
@@ -26,5 +26,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
|
||||
LIST: false,
|
||||
TRACE: false,
|
||||
BAR: true,
|
||||
PIE: false,
|
||||
EMPTY_WIDGET: false,
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ function WidgetGraphComponent({
|
||||
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
|
||||
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
|
||||
Array(queryResponse.data?.payload?.data?.result?.length || 0).fill(true),
|
||||
);
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -135,7 +135,7 @@ function WidgetGraphComponent({
|
||||
i: uuid,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 3,
|
||||
h: 6,
|
||||
y: 0,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -35,7 +35,11 @@ function GridCardGraph({
|
||||
}: GridCardGraphProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const {
|
||||
toScrollWidgetId,
|
||||
setToScrollWidgetId,
|
||||
variablesToGetUpdated,
|
||||
} = useDashboard();
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
@@ -90,7 +94,11 @@ function GridCardGraph({
|
||||
const isEmptyWidget =
|
||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||
|
||||
const queryEnabledCondition = isVisible && !isEmptyWidget && isQueryEnabled;
|
||||
const queryEnabledCondition =
|
||||
isVisible &&
|
||||
!isEmptyWidget &&
|
||||
isQueryEnabled &&
|
||||
isEmpty(variablesToGetUpdated);
|
||||
|
||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||
if (widget.panelTypes !== PANEL_TYPES.LIST) {
|
||||
@@ -166,7 +174,8 @@ function GridCardGraph({
|
||||
|
||||
const menuList =
|
||||
widget.panelTypes === PANEL_TYPES.TABLE ||
|
||||
widget.panelTypes === PANEL_TYPES.LIST
|
||||
widget.panelTypes === PANEL_TYPES.LIST ||
|
||||
widget.panelTypes === PANEL_TYPES.PIE
|
||||
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
|
||||
: headerMenuList;
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import './GridCardLayout.styles.scss';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Flex, Form, Input, Modal, Tooltip, Typography } from 'antd';
|
||||
import { useForm } from 'antd/es/form/Form';
|
||||
import cx from 'classnames';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
@@ -12,12 +16,21 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { FullscreenIcon } from 'lucide-react';
|
||||
import {
|
||||
FullscreenIcon,
|
||||
GripVertical,
|
||||
MoveDown,
|
||||
MoveUp,
|
||||
Settings,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { ItemCallback, Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -27,6 +40,7 @@ import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { EditMenuAction, ViewMenuAction } from './config';
|
||||
import GridCard from './GridCard';
|
||||
@@ -45,6 +59,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
selectedDashboard,
|
||||
layouts,
|
||||
setLayouts,
|
||||
panelMap,
|
||||
setPanelMap,
|
||||
setSelectedDashboard,
|
||||
isDashboardLocked,
|
||||
} = useDashboard();
|
||||
@@ -65,6 +81,26 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
|
||||
const [dashboardLayout, setDashboardLayout] = useState<Layout[]>([]);
|
||||
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
|
||||
|
||||
const [currentSelectRowId, setCurrentSelectRowId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [currentPanelMap, setCurrentPanelMap] = useState<
|
||||
Record<string, { widgets: Layout[]; collapsed: boolean }>
|
||||
>({});
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPanelMap(panelMap);
|
||||
}, [panelMap]);
|
||||
|
||||
const [form] = useForm<{
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
@@ -87,7 +123,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDashboardLayout(layouts);
|
||||
setDashboardLayout(sortLayout(layouts));
|
||||
}, [layouts]);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
@@ -97,6 +133,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
panelMap: { ...currentPanelMap },
|
||||
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
@@ -106,8 +143,9 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.payload) {
|
||||
if (updatedDashboard.payload.data.layout)
|
||||
setLayouts(updatedDashboard.payload.data.layout);
|
||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
}
|
||||
|
||||
featureResponse.refetch();
|
||||
@@ -130,7 +168,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
dashboardLayout,
|
||||
);
|
||||
if (!isEqual(filterLayout, filterDashboardLayout)) {
|
||||
setDashboardLayout(layout);
|
||||
const updatedLayout = sortLayout(layout);
|
||||
setDashboardLayout(updatedLayout);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,8 +206,297 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardLayout]);
|
||||
|
||||
function handleAddRow(): void {
|
||||
if (!selectedDashboard) return;
|
||||
const id = uuid();
|
||||
|
||||
const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = {
|
||||
widgets: [],
|
||||
collapsed: false,
|
||||
};
|
||||
const currentRowIdx = 0;
|
||||
for (let j = currentRowIdx; j < dashboardLayout.length; j++) {
|
||||
if (!currentPanelMap[dashboardLayout[j].i]) {
|
||||
newRowWidgetMap.widgets.push(dashboardLayout[j]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
layout: [
|
||||
{
|
||||
i: id,
|
||||
w: 12,
|
||||
minW: 12,
|
||||
minH: 1,
|
||||
maxH: 1,
|
||||
x: 0,
|
||||
h: 1,
|
||||
y: 0,
|
||||
},
|
||||
...dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||
],
|
||||
panelMap: { ...currentPanelMap, [id]: newRowWidgetMap },
|
||||
widgets: [
|
||||
...(selectedDashboard.data.widgets || []),
|
||||
{
|
||||
id,
|
||||
title: 'Sample Row',
|
||||
description: '',
|
||||
panelTypes: PANEL_GROUP_TYPES.ROW,
|
||||
},
|
||||
],
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (updatedDashboard.payload) {
|
||||
if (updatedDashboard.payload.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
}
|
||||
|
||||
featureResponse.refetch();
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const handleRowSettingsClick = (id: string): void => {
|
||||
setIsSettingsModalOpen(true);
|
||||
setCurrentSelectRowId(id);
|
||||
};
|
||||
|
||||
const onSettingsModalSubmit = (): void => {
|
||||
const newTitle = form.getFieldValue('title');
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
if (!currentSelectRowId) return;
|
||||
|
||||
const currentWidget = selectedDashboard?.data?.widgets?.find(
|
||||
(e) => e.id === currentSelectRowId,
|
||||
);
|
||||
|
||||
if (!currentWidget) return;
|
||||
|
||||
currentWidget.title = newTitle;
|
||||
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
|
||||
(e) => e.id !== currentSelectRowId,
|
||||
);
|
||||
|
||||
updatedWidgets?.push(currentWidget);
|
||||
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
if (setPanelMap)
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
form.setFieldValue('title', '');
|
||||
setIsSettingsModalOpen(false);
|
||||
setCurrentSelectRowId(null);
|
||||
featureResponse.refetch();
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleRowCollapse = (id: string): void => {
|
||||
if (!selectedDashboard) return;
|
||||
const rowProperties = { ...currentPanelMap[id] };
|
||||
const updatedPanelMap = { ...currentPanelMap };
|
||||
|
||||
let updatedDashboardLayout = [...dashboardLayout];
|
||||
if (rowProperties.collapsed === true) {
|
||||
rowProperties.collapsed = false;
|
||||
const widgetsInsideTheRow = rowProperties.widgets;
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
|
||||
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
|
||||
|
||||
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j].y += maxY;
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
].widgets.map((w) => ({
|
||||
...w,
|
||||
y: w.y + maxY,
|
||||
}));
|
||||
}
|
||||
}
|
||||
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
|
||||
} else {
|
||||
rowProperties.collapsed = true;
|
||||
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
|
||||
|
||||
let widgetsInsideTheRow: Layout[] = [];
|
||||
let isPanelMapUpdated = false;
|
||||
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
|
||||
if (currentPanelMap[dashboardLayout[j].i]) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
widgetsInsideTheRow = [];
|
||||
isPanelMapUpdated = true;
|
||||
break;
|
||||
} else {
|
||||
widgetsInsideTheRow.push(dashboardLayout[j]);
|
||||
}
|
||||
}
|
||||
if (!isPanelMapUpdated) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
}
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout[currentIdx];
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j].y += maxY;
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
].widgets.map((w) => ({
|
||||
...w,
|
||||
y: w.y + maxY,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
updatedDashboardLayout = updatedDashboardLayout.filter(
|
||||
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
|
||||
);
|
||||
}
|
||||
setCurrentPanelMap((prev) => ({
|
||||
...prev,
|
||||
...updatedPanelMap,
|
||||
[id]: {
|
||||
...rowProperties,
|
||||
},
|
||||
}));
|
||||
|
||||
setDashboardLayout(sortLayout(updatedDashboardLayout));
|
||||
};
|
||||
|
||||
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
|
||||
if (currentPanelMap[oldItem.i]) {
|
||||
const differenceY = newItem.y - oldItem.y;
|
||||
const widgetsInsideRow = currentPanelMap[oldItem.i].widgets.map((w) => ({
|
||||
...w,
|
||||
y: w.y + differenceY,
|
||||
}));
|
||||
setCurrentPanelMap((prev) => ({
|
||||
...prev,
|
||||
[oldItem.i]: {
|
||||
...prev[oldItem.i],
|
||||
widgets: widgetsInsideRow,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowDelete = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
if (!currentSelectRowId) return;
|
||||
|
||||
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
|
||||
(e) => e.id !== currentSelectRowId,
|
||||
);
|
||||
|
||||
const updatedLayout =
|
||||
selectedDashboard.data.layout?.filter((e) => e.i !== currentSelectRowId) ||
|
||||
[];
|
||||
|
||||
const updatedPanelMap = { ...currentPanelMap };
|
||||
delete updatedPanelMap[currentSelectRowId];
|
||||
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
layout: updatedLayout,
|
||||
panelMap: updatedPanelMap,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
if (setPanelMap)
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
setIsDeleteModalOpen(false);
|
||||
setCurrentSelectRowId(null);
|
||||
featureResponse.refetch();
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Flex justify="flex-end" gap={8} align="center">
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: data?.title,
|
||||
screen: 'Dashboard Details',
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
buttonText="Need help with this dashboard?"
|
||||
message={dashboardHelpMessage(data, selectedDashboard)}
|
||||
onHoverText="Click here to get help for this dashboard"
|
||||
/>
|
||||
<ButtonContainer>
|
||||
<Tooltip title="Open in Full Screen">
|
||||
<Button
|
||||
@@ -190,12 +518,23 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
{t('dashboard:add_panel')}
|
||||
</Button>
|
||||
)}
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={(): void => handleAddRow()}
|
||||
icon={<PlusOutlined />}
|
||||
data-testid="add-row"
|
||||
>
|
||||
{t('dashboard:add_row')}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonContainer>
|
||||
</Flex>
|
||||
|
||||
<FullScreen handle={handle} className="fullscreen-grid-container">
|
||||
<ReactGridLayout
|
||||
cols={12}
|
||||
rowHeight={100}
|
||||
rowHeight={45}
|
||||
autoSize
|
||||
width={100}
|
||||
useCSSTransforms
|
||||
@@ -204,6 +543,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
isResizable={!isDashboardLocked && addPanelPermission}
|
||||
allowOverlap={false}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
onDragStop={handleDragStop}
|
||||
draggableHandle=".drag-handle"
|
||||
layout={dashboardLayout}
|
||||
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
|
||||
@@ -212,6 +552,58 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
const { i: id } = layout;
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||
|
||||
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
|
||||
const rowWidgetProperties = currentPanelMap[id] || {};
|
||||
return (
|
||||
<CardContainer
|
||||
className="row-card"
|
||||
isDarkMode={isDarkMode}
|
||||
key={id}
|
||||
data-grid={JSON.stringify(currentWidget)}
|
||||
>
|
||||
<div className={cx('row-panel')}>
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<Button
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
icon={
|
||||
rowWidgetProperties.collapsed ? (
|
||||
<MoveDown size={14} />
|
||||
) : (
|
||||
<MoveUp size={14} />
|
||||
)
|
||||
}
|
||||
type="text"
|
||||
onClick={(): void => handleRowCollapse(id)}
|
||||
/>
|
||||
<Typography.Text>{currentWidget.title}</Typography.Text>
|
||||
<Button
|
||||
icon={<Settings size={14} />}
|
||||
type="text"
|
||||
onClick={(): void => handleRowSettingsClick(id)}
|
||||
/>
|
||||
</div>
|
||||
{rowWidgetProperties.collapsed && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<GripVertical size={14} />}
|
||||
className="drag-handle"
|
||||
/>
|
||||
)}
|
||||
{!rowWidgetProperties.collapsed && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Trash2 size={14} />}
|
||||
onClick={(): void => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setCurrentSelectRowId(id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
className={isDashboardLocked ? '' : 'enable-resize'}
|
||||
@@ -224,7 +616,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
|
||||
>
|
||||
<GridCard
|
||||
widget={currentWidget || ({ id, query: {} } as Widgets)}
|
||||
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
version={selectedDashboard?.data?.version}
|
||||
@@ -235,6 +627,46 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
);
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
<Modal
|
||||
open={isSettingsModalOpen}
|
||||
title="Row Options"
|
||||
destroyOnClose
|
||||
footer={null}
|
||||
onCancel={(): void => {
|
||||
setIsSettingsModalOpen(false);
|
||||
setCurrentSelectRowId(null);
|
||||
}}
|
||||
>
|
||||
<Form form={form} onFinish={onSettingsModalSubmit} requiredMark>
|
||||
<Form.Item required name={['title']}>
|
||||
<Input
|
||||
placeholder="Enter row name here..."
|
||||
defaultValue={defaultTo(
|
||||
widgets?.find((widget) => widget.id === currentSelectRowId)
|
||||
?.title as string,
|
||||
'Sample Title',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Apply Changes
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={isDeleteModalOpen}
|
||||
title="Delete Row"
|
||||
destroyOnClose
|
||||
onCancel={(): void => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setCurrentSelectRowId(null);
|
||||
}}
|
||||
onOk={(): void => handleRowDelete()}
|
||||
>
|
||||
<Typography.Text>Are you sure you want to delete this row</Typography.Text>
|
||||
</Modal>
|
||||
</FullScreen>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,6 @@ export const EMPTY_WIDGET_LAYOUT = {
|
||||
i: PANEL_TYPES.EMPTY_WIDGET,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 3,
|
||||
h: 6,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
@@ -29,6 +29,17 @@ interface Props {
|
||||
export const CardContainer = styled.div<Props>`
|
||||
overflow: auto;
|
||||
|
||||
&.row-card {
|
||||
.row-panel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-ink-400);
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.enable-resize {
|
||||
:hover {
|
||||
.react-resizable-handle {
|
||||
|
||||
@@ -42,6 +42,7 @@ const GridPanelSwitch = forwardRef<
|
||||
thresholds,
|
||||
},
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.PIE]: null,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.BAR]: {
|
||||
data,
|
||||
|
||||
@@ -38,6 +38,7 @@ export type PropsTypePropsMap = {
|
||||
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
||||
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
||||
[PANEL_TYPES.TRACE]: null;
|
||||
[PANEL_TYPES.PIE]: null;
|
||||
[PANEL_TYPES.LIST]: null;
|
||||
[PANEL_TYPES.BAR]: UplotProps & {
|
||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Input, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import DropDown from 'components/DropDown/DropDown';
|
||||
import { listAlertMessage } from 'components/facingIssueBtn/util';
|
||||
import {
|
||||
DynamicColumnsKey,
|
||||
TableDataSource,
|
||||
@@ -358,6 +359,15 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
pagination={{
|
||||
defaultCurrent: Number(paginationParam) || 1,
|
||||
}}
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
|
||||
import { ItemType } from 'antd/es/menu/hooks/useItems';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import { AxiosError } from 'axios';
|
||||
import { dashboardListMessage } from 'components/facingIssueBtn/util';
|
||||
import {
|
||||
DynamicColumnsKey,
|
||||
TableDataSource,
|
||||
@@ -385,6 +386,15 @@ function DashboardsList(): JSX.Element {
|
||||
dataSource={data}
|
||||
onChange={handleChange}
|
||||
showSorterTooltip
|
||||
facingIssueBtn={{
|
||||
attributes: {
|
||||
screen: 'Dashboard list page',
|
||||
},
|
||||
eventName: 'Dashboard: Facing Issues in dashboard',
|
||||
buttonText: 'Facing issues with dashboards?',
|
||||
message: dashboardListMessage,
|
||||
onHoverText: 'Click here to get help with dashboards',
|
||||
}}
|
||||
/>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
|
||||
@@ -85,8 +85,8 @@ function LogControls(): JSX.Element | null {
|
||||
logs.map((log) => {
|
||||
const timestamp =
|
||||
typeof log.timestamp === 'string'
|
||||
? dayjs(log.timestamp).format()
|
||||
: dayjs(log.timestamp / 1e6).format();
|
||||
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
|
||||
return FlatLogData({
|
||||
...log,
|
||||
|
||||
@@ -531,8 +531,8 @@ function LogsExplorerViews({
|
||||
logs.map((log) => {
|
||||
const timestamp =
|
||||
typeof log.timestamp === 'string'
|
||||
? dayjs(log.timestamp).format()
|
||||
: dayjs(log.timestamp / 1e6).format();
|
||||
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
||||
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
|
||||
return FlatLogData({
|
||||
timestamp,
|
||||
@@ -608,6 +608,7 @@ function LogsExplorerViews({
|
||||
className="periscope-btn"
|
||||
onClick={handleToggleShowFormatOptions}
|
||||
icon={<Sliders size={14} />}
|
||||
data-testid="periscope-btn"
|
||||
/>
|
||||
|
||||
{showFormatMenuItems && (
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { render, RenderResult } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import i18n from 'ReactI18';
|
||||
import store from 'store';
|
||||
|
||||
import LogsExplorerViews from '..';
|
||||
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
|
||||
|
||||
const logExplorerRoute = '/logs/logs-explorer';
|
||||
|
||||
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
||||
|
||||
const lodsQueryServerRequest = (): void =>
|
||||
server.use(
|
||||
rest.post(queryRangeURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(logsQueryRangeSuccessResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
// mocking the graph components in this test as this should be handled separately
|
||||
jest.mock(
|
||||
'container/TimeSeriesView/TimeSeriesView',
|
||||
() =>
|
||||
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
|
||||
function () {
|
||||
return <div>Time Series Chart</div>;
|
||||
},
|
||||
);
|
||||
jest.mock(
|
||||
'container/LogsExplorerChart',
|
||||
() =>
|
||||
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
|
||||
function () {
|
||||
return <div>Histogram Chart</div>;
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('constants/panelTypes', () => ({
|
||||
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
|
||||
}));
|
||||
|
||||
jest.mock('d3-interpolate', () => ({
|
||||
interpolate: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
|
||||
__esModule: true,
|
||||
useGetExplorerQueryRange: jest.fn(),
|
||||
}));
|
||||
|
||||
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
|
||||
beforeEach(() => {
|
||||
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||
data: { payload: logsQueryRangeSuccessNewFormatResponse },
|
||||
});
|
||||
});
|
||||
|
||||
const renderer = (): RenderResult =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={[logExplorerRoute]}>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<MockQueryClientProvider>
|
||||
<QueryBuilderProvider>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorerViews selectedView={SELECTED_VIEWS.SEARCH} showHistogram />
|
||||
</VirtuosoMockContext.Provider>
|
||||
</QueryBuilderProvider>
|
||||
</MockQueryClientProvider>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
describe('LogsExplorerViews -', () => {
|
||||
it('render correctly with props - list and table', async () => {
|
||||
lodsQueryServerRequest();
|
||||
const { queryByText, queryByTestId } = renderer();
|
||||
|
||||
expect(queryByTestId('periscope-btn')).toBeInTheDocument();
|
||||
await userEvent.click(queryByTestId('periscope-btn') as HTMLElement);
|
||||
|
||||
expect(document.querySelector('.menu-container')).toBeInTheDocument();
|
||||
|
||||
const menuItems = document.querySelectorAll('.menu-items .item');
|
||||
expect(menuItems.length).toBe(3);
|
||||
|
||||
// switch to table view
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||
|
||||
expect(
|
||||
queryByText(
|
||||
'{"container_id":"container_id","container_name":"container_name","driver":"driver","eta":"2m0s","location":"frontend","log_level":"INFO","message":"Dispatch successful","service":"frontend","span_id":"span_id","trace_id":"span_id"}',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('check isLoading state', async () => {
|
||||
lodsQueryServerRequest();
|
||||
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||
data: { payload: logsQueryRangeSuccessNewFormatResponse },
|
||||
isLoading: true,
|
||||
isFetching: false,
|
||||
});
|
||||
const { queryByText, queryByTestId } = renderer();
|
||||
|
||||
// switch to table view
|
||||
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||
expect(
|
||||
queryByText(
|
||||
'Just a bit of patience, just a little bit’s enough ⎯ we’re getting your logs!',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('check error state', async () => {
|
||||
lodsQueryServerRequest();
|
||||
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||
data: { payload: logsQueryRangeSuccessNewFormatResponse },
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isError: true,
|
||||
});
|
||||
const { queryByText, queryByTestId } = renderer();
|
||||
|
||||
expect(
|
||||
queryByText('Something went wrong. Please try again or contact support.'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// switch to table view
|
||||
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||
|
||||
expect(
|
||||
queryByText('Something went wrong. Please try again or contact support.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
51
frontend/src/container/LogsExplorerViews/tests/mock.ts
Normal file
51
frontend/src/container/LogsExplorerViews/tests/mock.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export const logsQueryRangeSuccessNewFormatResponse = {
|
||||
data: {
|
||||
result: [],
|
||||
resultType: '',
|
||||
newResult: {
|
||||
status: 'success',
|
||||
data: {
|
||||
resultType: '',
|
||||
result: [
|
||||
{
|
||||
queryName: 'A',
|
||||
series: null,
|
||||
list: [
|
||||
{
|
||||
timestamp: '2024-02-15T21:20:22Z',
|
||||
data: {
|
||||
attributes_bool: {},
|
||||
attributes_float64: {},
|
||||
attributes_int64: {},
|
||||
attributes_string: {
|
||||
container_id: 'container_id',
|
||||
container_name: 'container_name',
|
||||
driver: 'driver',
|
||||
eta: '2m0s',
|
||||
location: 'frontend',
|
||||
log_level: 'INFO',
|
||||
message: 'Dispatch successful',
|
||||
service: 'frontend',
|
||||
span_id: 'span_id',
|
||||
trace_id: 'span_id',
|
||||
},
|
||||
body:
|
||||
'2024-02-15T21:20:22.035Z\tINFO\tfrontend\tDispatch successful\t{"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}',
|
||||
id: 'id',
|
||||
resources_string: {
|
||||
'container.name': 'container_name',
|
||||
},
|
||||
severity_number: 0,
|
||||
severity_text: '',
|
||||
span_id: '',
|
||||
trace_flags: 0,
|
||||
trace_id: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
|
||||
import { DownloadOptions } from 'container/Download/Download.types';
|
||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||
|
||||
@@ -20,7 +22,7 @@ export enum FORMULA {
|
||||
ERROR_PERCENTAGE = 'A*100/B',
|
||||
DATABASE_CALLS_AVG_DURATION = 'A/B',
|
||||
APDEX_TRACES = '((B + C)/2)/A',
|
||||
APDEX_DELTA_SPAN_METRICS = '(B + C/2)/A',
|
||||
APDEX_DELTA_SPAN_METRICS = '((B + C)/2)/A',
|
||||
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export const getNearestHighestBucketValue = (
|
||||
value: number,
|
||||
buckets: number[],
|
||||
): string => {
|
||||
// sort the buckets
|
||||
buckets.sort((a, b) => a - b);
|
||||
const nearestBucket = buckets.find((bucket) => bucket >= value);
|
||||
return nearestBucket?.toString() || '+Inf';
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export const PANEL_TYPES_INITIAL_QUERY = {
|
||||
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
|
||||
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
|
||||
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
|
||||
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,99 +1,21 @@
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
listViewInitialLogQuery,
|
||||
listViewInitialTraceQuery,
|
||||
PANEL_TYPES_INITIAL_QUERY,
|
||||
} from './constants';
|
||||
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
|
||||
import menuItems from './menuItems';
|
||||
import { Card, Container, Text } from './styles';
|
||||
|
||||
function DashboardGraphSlider(): JSX.Element {
|
||||
const {
|
||||
handleToggleDashboardSlider,
|
||||
layouts,
|
||||
selectedDashboard,
|
||||
} = useDashboard();
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
const { handleToggleDashboardSlider } = useDashboard();
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const onClickHandler = (name: PANEL_TYPES) => (): void => {
|
||||
const id = uuid();
|
||||
|
||||
updateDashboardMutation.mutateAsync(
|
||||
{
|
||||
uuid: selectedDashboard?.uuid || '',
|
||||
data: {
|
||||
title: data?.title || '',
|
||||
variables: data?.variables || {},
|
||||
description: data?.description || '',
|
||||
name: data?.name || '',
|
||||
tags: data?.tags || [],
|
||||
version: data?.version || 'v3',
|
||||
layout: [
|
||||
{
|
||||
i: id,
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 3,
|
||||
y: 0,
|
||||
},
|
||||
...(layouts.filter((layout) => layout.i !== PANEL_TYPES.EMPTY_WIDGET) ||
|
||||
[]),
|
||||
],
|
||||
widgets: [
|
||||
...(data?.widgets || []),
|
||||
{
|
||||
id,
|
||||
title: '',
|
||||
description: '',
|
||||
isStacked: false,
|
||||
nullZeroValues: '',
|
||||
opacity: '',
|
||||
panelTypes: name,
|
||||
query:
|
||||
name === PANEL_TYPES.LIST
|
||||
? listViewInitialLogQuery
|
||||
: PANEL_TYPES_INITIAL_QUERY[name],
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
selectedLogFields: [
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'body',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'timestamp',
|
||||
},
|
||||
],
|
||||
selectedTracesFields: [
|
||||
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
if (data.payload) {
|
||||
handleToggleDashboardSlider(false);
|
||||
const queryParamsLog = {
|
||||
graphType: name,
|
||||
@@ -132,15 +54,6 @@ function DashboardGraphSlider(): JSX.Element {
|
||||
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
notifications.success({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { BarChart3, LineChart, List, SigmaSquare, Table } from 'lucide-react';
|
||||
import {
|
||||
BarChart3,
|
||||
LineChart,
|
||||
List,
|
||||
PieChart,
|
||||
SigmaSquare,
|
||||
Table,
|
||||
} from 'lucide-react';
|
||||
|
||||
const Items: ItemsProps[] = [
|
||||
{
|
||||
@@ -28,6 +35,11 @@ const Items: ItemsProps[] = [
|
||||
icon: <BarChart3 size={32} color={Color.BG_ROBIN_400} />,
|
||||
display: 'Bar',
|
||||
},
|
||||
{
|
||||
name: PANEL_TYPES.PIE,
|
||||
icon: <PieChart size={32} color={Color.BG_ROBIN_400} />,
|
||||
display: 'Pie',
|
||||
},
|
||||
];
|
||||
|
||||
export interface ItemsProps {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Row } from 'antd';
|
||||
import { isNull } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { convertVariablesToDbFormat } from './util';
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
function DashboardVariableSelection(): JSX.Element | null {
|
||||
@@ -11,15 +11,14 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
variablesToGetUpdated,
|
||||
setVariablesToGetUpdated,
|
||||
} = useDashboard();
|
||||
|
||||
const { data } = selectedDashboard || {};
|
||||
|
||||
const { variables } = data || {};
|
||||
|
||||
const [update, setUpdate] = useState<boolean>(false);
|
||||
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
|
||||
|
||||
const [variablesTableData, setVariablesTableData] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -45,8 +44,27 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
}, [variables]);
|
||||
|
||||
const onVarChanged = (name: string): void => {
|
||||
setLastUpdatedVar(name);
|
||||
setUpdate(!update);
|
||||
/**
|
||||
* this function takes care of adding the dependent variables to current update queue and removing
|
||||
* the updated variable name from the queue
|
||||
*/
|
||||
const dependentVariables = variablesTableData
|
||||
?.map((variable: any) => {
|
||||
if (variable.type === 'QUERY') {
|
||||
const re = new RegExp(`\\{\\{\\s*?\\.${name}\\s*?\\}\\}`); // regex for `{{.var}}`
|
||||
const queryValue = variable.queryValue || '';
|
||||
const dependVarReMatch = queryValue.match(re);
|
||||
if (dependVarReMatch !== null && dependVarReMatch.length > 0) {
|
||||
return variable.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((val: string | null) => !isNull(val));
|
||||
setVariablesToGetUpdated((prev) => [
|
||||
...prev.filter((v) => v !== name),
|
||||
...dependentVariables,
|
||||
]);
|
||||
};
|
||||
|
||||
const onValueUpdate = (
|
||||
@@ -54,39 +72,46 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
id: string,
|
||||
value: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): void => {
|
||||
if (id) {
|
||||
const newVariablesArr = variablesTableData.map(
|
||||
(variable: IDashboardVariable) => {
|
||||
const variableCopy = { ...variable };
|
||||
|
||||
if (variableCopy.id === id) {
|
||||
variableCopy.selectedValue = value;
|
||||
variableCopy.allSelected = allSelected;
|
||||
}
|
||||
|
||||
return variableCopy;
|
||||
},
|
||||
);
|
||||
updateLocalStorageDashboardVariables(name, value, allSelected);
|
||||
|
||||
const variables = convertVariablesToDbFormat(newVariablesArr);
|
||||
|
||||
if (selectedDashboard) {
|
||||
setSelectedDashboard({
|
||||
...selectedDashboard,
|
||||
setSelectedDashboard((prev) => {
|
||||
if (prev) {
|
||||
const oldVariables = prev?.data.variables;
|
||||
// this is added to handle case where we have two different
|
||||
// schemas for variable response
|
||||
if (oldVariables[id]) {
|
||||
oldVariables[id] = {
|
||||
...oldVariables[id],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
};
|
||||
}
|
||||
if (oldVariables[name]) {
|
||||
oldVariables[name] = {
|
||||
...oldVariables[name],
|
||||
selectedValue: value,
|
||||
allSelected,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
data: {
|
||||
...selectedDashboard?.data,
|
||||
...prev?.data,
|
||||
variables: {
|
||||
...variables,
|
||||
...oldVariables,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
onVarChanged(name);
|
||||
|
||||
setUpdate(!update);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,13 +132,12 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
<VariableItem
|
||||
key={`${variable.name}${variable.id}}${variable.order}`}
|
||||
existingVariables={variables}
|
||||
lastUpdatedVar={lastUpdatedVar}
|
||||
variableData={{
|
||||
name: variable.name,
|
||||
...variable,
|
||||
change: update,
|
||||
}}
|
||||
onValueUpdate={onValueUpdate}
|
||||
variablesToGetUpdated={variablesToGetUpdated}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('VariableItem', () => {
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
variablesToGetUpdated={[]}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
@@ -68,7 +68,7 @@ describe('VariableItem', () => {
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
variablesToGetUpdated={[]}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
@@ -82,7 +82,7 @@ describe('VariableItem', () => {
|
||||
variableData={mockVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
variablesToGetUpdated={[]}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
@@ -110,7 +110,7 @@ describe('VariableItem', () => {
|
||||
variableData={mockCustomVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
variablesToGetUpdated={[]}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
@@ -131,7 +131,7 @@ describe('VariableItem', () => {
|
||||
variableData={customVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
variablesToGetUpdated={[]}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
@@ -146,7 +146,7 @@ describe('VariableItem', () => {
|
||||
variableData={mockCustomVariableData}
|
||||
existingVariables={{}}
|
||||
onValueUpdate={mockOnValueUpdate}
|
||||
lastUpdatedVar=""
|
||||
variablesToGetUpdated={[]}
|
||||
/>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ interface VariableItemProps {
|
||||
arg1: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
) => void;
|
||||
lastUpdatedVar: string;
|
||||
variablesToGetUpdated: string[];
|
||||
}
|
||||
|
||||
const getSelectValue = (
|
||||
@@ -49,7 +49,7 @@ function VariableItem({
|
||||
variableData,
|
||||
existingVariables,
|
||||
onValueUpdate,
|
||||
lastUpdatedVar,
|
||||
variablesToGetUpdated,
|
||||
}: VariableItemProps): JSX.Element {
|
||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||
[],
|
||||
@@ -108,16 +108,10 @@ function VariableItem({
|
||||
|
||||
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
|
||||
/* eslint-disable no-useless-escape */
|
||||
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
|
||||
// If the variable is dependent on the last updated variable
|
||||
// and contains the last updated variable in its query (of the form `{{.var}}`)
|
||||
// then we need to update the value of the variable
|
||||
const queryValue = variableData.queryValue || '';
|
||||
const dependVarReMatch = queryValue.match(re);
|
||||
if (
|
||||
variableData.type === 'QUERY' &&
|
||||
dependVarReMatch !== null &&
|
||||
dependVarReMatch.length > 0
|
||||
variableData.name &&
|
||||
variablesToGetUpdated.includes(variableData.name)
|
||||
) {
|
||||
let value = variableData.selectedValue;
|
||||
let allSelected = false;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
import { getDefaultWidgetData } from 'container/NewWidget/utils';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
@@ -11,6 +12,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { Atom, Play, Terminal } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
@@ -55,8 +57,11 @@ function QuerySection({
|
||||
|
||||
const getWidget = useCallback(() => {
|
||||
const widgetId = urlQuery.get('widgetId');
|
||||
return widgets?.find((e) => e.id === widgetId);
|
||||
}, [widgets, urlQuery]);
|
||||
return defaultTo(
|
||||
widgets?.find((e) => e.id === widgetId),
|
||||
getDefaultWidgetData(widgetId || '', selectedGraph),
|
||||
);
|
||||
}, [urlQuery, widgets, selectedGraph]);
|
||||
|
||||
const selectedWidget = getWidget() as Widgets;
|
||||
|
||||
|
||||
@@ -67,12 +67,13 @@ function LeftContainer({
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
selectedTime: selectedTime.enum || prev.selectedTime,
|
||||
globalSelectedInterval,
|
||||
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
|
||||
query: stagedQuery,
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stagedQuery, selectedTime]);
|
||||
}, [stagedQuery, selectedTime, globalSelectedInterval]);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
requestData,
|
||||
|
||||
4
frontend/src/container/NewWidget/NewWidget.styles.scss
Normal file
4
frontend/src/container/NewWidget/NewWidget.styles.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.facing-issue-btn-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.VALUE]: true,
|
||||
[PANEL_TYPES.TABLE]: true,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: false,
|
||||
[PANEL_TYPES.BAR]: true,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
@@ -36,6 +37,7 @@ export const panelTypeVsSoftMinMax: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.VALUE]: false,
|
||||
[PANEL_TYPES.TABLE]: false,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: false,
|
||||
[PANEL_TYPES.BAR]: true,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
@@ -45,6 +47,7 @@ export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.TIME_SERIES]: false,
|
||||
[PANEL_TYPES.VALUE]: true,
|
||||
[PANEL_TYPES.TABLE]: true,
|
||||
[PANEL_TYPES.PIE]: false,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.BAR]: false,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
@@ -56,6 +59,7 @@ export const panelTypeVsFillSpan: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.VALUE]: false,
|
||||
[PANEL_TYPES.TABLE]: false,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: false,
|
||||
[PANEL_TYPES.BAR]: false,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
@@ -66,6 +70,7 @@ export const panelTypeVsYAxisUnit: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.VALUE]: true,
|
||||
[PANEL_TYPES.TABLE]: true,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: false,
|
||||
[PANEL_TYPES.BAR]: true,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
@@ -76,6 +81,7 @@ export const panelTypeVsCreateAlert: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.VALUE]: true,
|
||||
[PANEL_TYPES.TABLE]: false,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: false,
|
||||
[PANEL_TYPES.BAR]: true,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
@@ -88,6 +94,7 @@ export const panelTypeVsPanelTimePreferences: {
|
||||
[PANEL_TYPES.VALUE]: true,
|
||||
[PANEL_TYPES.TABLE]: true,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.PIE]: true,
|
||||
[PANEL_TYPES.BAR]: true,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './NewWidget.styles.scss';
|
||||
|
||||
import { LockFilled, WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal, Space, Tooltip, Typography } from 'antd';
|
||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||
import { chartHelpMessage } from 'components/facingIssueBtn/util';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -14,6 +18,7 @@ import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { defaultTo, isUndefined } from 'lodash-es';
|
||||
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
@@ -45,7 +50,11 @@ import {
|
||||
RightContainerWrapper,
|
||||
} from './styles';
|
||||
import { NewWidgetProps } from './types';
|
||||
import { getIsQueryModified, handleQueryChange } from './utils';
|
||||
import {
|
||||
getDefaultWidgetData,
|
||||
getIsQueryModified,
|
||||
handleQueryChange,
|
||||
} from './utils';
|
||||
|
||||
function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
const {
|
||||
@@ -80,10 +89,26 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
const { dashboardId } = useParams<DashboardWidgetPageParams>();
|
||||
|
||||
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const widgetId = query.get('widgetId');
|
||||
const selectedWidget = widgets?.find((e) => e.id === widgetId);
|
||||
const isWidgetNotPresent = isUndefined(selectedWidget);
|
||||
if (isWidgetNotPresent) {
|
||||
setIsNewDashboard(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const getWidget = useCallback(() => {
|
||||
const widgetId = query.get('widgetId');
|
||||
return widgets?.find((e) => e.id === widgetId);
|
||||
}, [query, widgets]);
|
||||
const selectedWidget = widgets?.find((e) => e.id === widgetId);
|
||||
return defaultTo(
|
||||
selectedWidget,
|
||||
getDefaultWidgetData(widgetId || '', selectedGraph),
|
||||
) as Widgets;
|
||||
}, [query, selectedGraph, widgets]);
|
||||
|
||||
const [selectedWidget, setSelectedWidget] = useState(getWidget());
|
||||
|
||||
@@ -227,6 +252,20 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
const widgetId = query.get('widgetId');
|
||||
let updatedLayout = selectedDashboard.data.layout || [];
|
||||
if (isNewDashboard) {
|
||||
updatedLayout = [
|
||||
{
|
||||
i: widgetId || '',
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 6,
|
||||
y: 0,
|
||||
},
|
||||
...updatedLayout,
|
||||
];
|
||||
}
|
||||
const dashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
uuid: selectedDashboard.uuid,
|
||||
@@ -254,6 +293,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
},
|
||||
...afterWidgets,
|
||||
],
|
||||
layout: [...updatedLayout],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -274,6 +314,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
});
|
||||
}, [
|
||||
selectedDashboard,
|
||||
query,
|
||||
isNewDashboard,
|
||||
preWidgets,
|
||||
selectedWidget,
|
||||
selectedTime.enum,
|
||||
@@ -363,6 +405,21 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="facing-issue-btn-container">
|
||||
<FacingIssueBtn
|
||||
attributes={{
|
||||
uuid: selectedDashboard?.uuid,
|
||||
title: selectedDashboard?.data.title,
|
||||
panelType: graphType,
|
||||
widgetId: query.get('widgetId'),
|
||||
queryType: currentQuery.queryType,
|
||||
screen: 'Dashboard list page',
|
||||
}}
|
||||
eventName="Dashboard: Facing Issues in dashboard"
|
||||
buttonText="Need help with this chart?"
|
||||
message={chartHelpMessage(selectedDashboard, graphType)}
|
||||
onHoverText="Click here to get help in creating chart"
|
||||
/>
|
||||
<ButtonContainer>
|
||||
{isSaveDisabled && (
|
||||
<Tooltip title={MESSAGE.PANEL}>
|
||||
@@ -390,6 +447,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
)}
|
||||
<Button onClick={onClickDiscardHandler}>Discard Changes</Button>
|
||||
</ButtonContainer>
|
||||
</div>
|
||||
|
||||
<PanelContainer>
|
||||
<LeftContainerWrapper flex={5}>
|
||||
|
||||
@@ -3,7 +3,13 @@ import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import {
|
||||
listViewInitialLogQuery,
|
||||
listViewInitialTraceQuery,
|
||||
PANEL_TYPES_INITIAL_QUERY,
|
||||
} from 'container/NewDashboard/ComponentsSlider/constants';
|
||||
import { isEqual, set, unset } from 'lodash-es';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -25,6 +31,7 @@ export type PartialPanelTypes = {
|
||||
[PANEL_TYPES.TABLE]: 'table';
|
||||
[PANEL_TYPES.TIME_SERIES]: 'graph';
|
||||
[PANEL_TYPES.VALUE]: 'value';
|
||||
[PANEL_TYPES.PIE]: 'pie';
|
||||
};
|
||||
|
||||
export const panelTypeDataSourceFormValuesMap: Record<
|
||||
@@ -163,6 +170,50 @@ export const panelTypeDataSourceFormValuesMap: Record<
|
||||
},
|
||||
},
|
||||
},
|
||||
[PANEL_TYPES.PIE]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'filters',
|
||||
'aggregateOperator',
|
||||
'aggregateAttribute',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.METRICS]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'filters',
|
||||
'aggregateOperator',
|
||||
'aggregateAttribute',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
'functions',
|
||||
'spaceAggregation',
|
||||
],
|
||||
},
|
||||
},
|
||||
[DataSource.TRACES]: {
|
||||
builder: {
|
||||
queryData: [
|
||||
'filters',
|
||||
'aggregateOperator',
|
||||
'aggregateAttribute',
|
||||
'groupBy',
|
||||
'limit',
|
||||
'having',
|
||||
'orderBy',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
[PANEL_TYPES.LIST]: {
|
||||
[DataSource.LOGS]: {
|
||||
builder: {
|
||||
@@ -257,3 +308,38 @@ export function handleQueryChange(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const getDefaultWidgetData = (
|
||||
id: string,
|
||||
name: PANEL_TYPES,
|
||||
): Widgets => ({
|
||||
id,
|
||||
title: '',
|
||||
description: '',
|
||||
isStacked: false,
|
||||
nullZeroValues: '',
|
||||
opacity: '',
|
||||
panelTypes: name,
|
||||
query:
|
||||
name === PANEL_TYPES.LIST
|
||||
? listViewInitialLogQuery
|
||||
: PANEL_TYPES_INITIAL_QUERY[name],
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
selectedLogFields: [
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'body',
|
||||
},
|
||||
{
|
||||
dataType: 'string',
|
||||
type: '',
|
||||
name: 'timestamp',
|
||||
},
|
||||
],
|
||||
selectedTracesFields: [
|
||||
...listViewInitialTraceQuery.builder.queryData[0].selectColumns,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -159,7 +159,7 @@ export default function EnvironmentDetails(): JSX.Element {
|
||||
|
||||
<div className="request-entity-container">
|
||||
<Typography.Text>
|
||||
Cannot find what you’re looking for? Request a data source
|
||||
Cannot find what you’re looking for? Request an environment
|
||||
</Typography.Text>
|
||||
|
||||
<div className="form-section">
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
LeftCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Space, Steps, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
||||
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
||||
@@ -17,6 +18,7 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useOnboardingContext } from '../../context/OnboardingContext';
|
||||
@@ -379,6 +381,31 @@ export default function ModuleStepsContainer({
|
||||
history.push('/');
|
||||
};
|
||||
|
||||
const handleFacingIssuesClick = (): void => {
|
||||
logEvent('Onboarding V2: Facing Issues Sending Data to SigNoz', {
|
||||
dataSource: selectedDataSource?.id,
|
||||
framework: selectedFramework,
|
||||
environment: selectedEnvironment,
|
||||
module: activeStep?.module?.id,
|
||||
step: activeStep?.step?.id,
|
||||
});
|
||||
|
||||
const message = `Hi Team,
|
||||
|
||||
I am facing issues sending data to SigNoz. Here are my application details
|
||||
|
||||
Data Source: ${selectedDataSource?.name}
|
||||
Framework:
|
||||
Environment:
|
||||
Module: ${activeStep?.module?.id}
|
||||
|
||||
Thanks
|
||||
`;
|
||||
if (window.Intercom) {
|
||||
window.Intercom('showNewMessage', message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="onboarding-module-steps">
|
||||
<div className="steps-container">
|
||||
@@ -455,6 +482,15 @@ export default function ModuleStepsContainer({
|
||||
<Button onClick={handleNext} type="primary" icon={<ArrowRightOutlined />}>
|
||||
{current < lastStepIndex ? 'Continue to next step' : 'Done'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={handleFacingIssuesClick}
|
||||
danger
|
||||
icon={<HelpCircle size={14} />}
|
||||
>
|
||||
Facing issues sending data to SigNoz?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
.piechart-no-data {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.piechart-container {
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.piechart-tooltip {
|
||||
|
||||
.piechart-indicator {
|
||||
width: 15px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tooltip-value {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.piechart-legend {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.piechart-legend-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.piechart-legend-label {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
218
frontend/src/container/PanelWrapper/PiePanelWrapper.tsx
Normal file
218
frontend/src/container/PanelWrapper/PiePanelWrapper.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import './PiePanelWrapper.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Group } from '@visx/group';
|
||||
import { Pie } from '@visx/shape';
|
||||
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { PanelWrapperProps, TooltipData } from './panelWrapper.types';
|
||||
import { getLabel, lightenColor, tooltipStyles } from './utils';
|
||||
|
||||
// refernce: https://www.youtube.com/watch?v=bL3P9CqQkKw
|
||||
function PiePanelWrapper({
|
||||
queryResponse,
|
||||
widget,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const [active, setActive] = useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
color: string;
|
||||
} | null>(null);
|
||||
|
||||
const {
|
||||
tooltipOpen,
|
||||
tooltipLeft,
|
||||
tooltipTop,
|
||||
tooltipData,
|
||||
hideTooltip,
|
||||
showTooltip,
|
||||
} = useTooltip<TooltipData>();
|
||||
|
||||
const { containerRef, TooltipInPortal } = useTooltipInPortal({
|
||||
scroll: true,
|
||||
detectBounds: true,
|
||||
});
|
||||
|
||||
const panelData =
|
||||
queryResponse.data?.payload?.data.newResult.data.result || [];
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const pieChartData: {
|
||||
label: string;
|
||||
value: string;
|
||||
color: string;
|
||||
}[] = [].concat(
|
||||
...(panelData
|
||||
.map((d) =>
|
||||
d.series?.map((s) => ({
|
||||
label:
|
||||
d.series?.length === 1
|
||||
? getLabel(Object.values(s.labels)[0], widget.query, d.queryName)
|
||||
: getLabel(Object.values(s.labels)[0], {} as Query, d.queryName, true),
|
||||
value: s.values[0].value,
|
||||
color: generateColor(
|
||||
d.series?.length === 1
|
||||
? getLabel(Object.values(s.labels)[0], widget.query, d.queryName)
|
||||
: getLabel(Object.values(s.labels)[0], {} as Query, d.queryName, true),
|
||||
themeColors.chartcolors,
|
||||
),
|
||||
})),
|
||||
)
|
||||
.filter((d) => d !== undefined) as never[]),
|
||||
);
|
||||
|
||||
let size = 0;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
if (chartRef.current) {
|
||||
const { offsetWidth, offsetHeight } = chartRef.current;
|
||||
size = Math.min(offsetWidth, offsetHeight);
|
||||
width = offsetWidth;
|
||||
height = offsetHeight;
|
||||
}
|
||||
const half = size / 2;
|
||||
|
||||
const getFillColor = (color: string): string => {
|
||||
if (active === null) {
|
||||
return color;
|
||||
}
|
||||
const lightenedColor = lightenColor(color, 0.4); // Adjust the opacity value (0.7 in this case)
|
||||
return active.color === color ? color : lightenedColor;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!pieChartData.length && <div className="piechart-no-data">No data</div>}
|
||||
{pieChartData.length > 0 && (
|
||||
<>
|
||||
<div className="piechart-container" ref={chartRef}>
|
||||
<svg width={width} height={height} ref={containerRef}>
|
||||
<Group top={height / 2} left={width / 2}>
|
||||
<Pie
|
||||
data={pieChartData}
|
||||
pieValue={(data: {
|
||||
label: string;
|
||||
value: string;
|
||||
color: string;
|
||||
}): number => parseFloat(data.value)}
|
||||
outerRadius={({ data }): number => {
|
||||
if (!active) return half - 3;
|
||||
return data.label === active.label ? half : half - 3;
|
||||
}}
|
||||
padAngle={0.02}
|
||||
cornerRadius={3}
|
||||
width={size}
|
||||
height={size}
|
||||
>
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
(pie) =>
|
||||
pie.arcs.map((arc, index) => {
|
||||
const { label } = arc.data;
|
||||
const [centroidX, centroidY] = pie.path.centroid(arc);
|
||||
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.6;
|
||||
const arcPath = pie.path(arc);
|
||||
const arcFill = arc.data.color;
|
||||
return (
|
||||
<g
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`arc-${label}-${index}`}
|
||||
onMouseEnter={(): void => {
|
||||
showTooltip({
|
||||
tooltipData: {
|
||||
label,
|
||||
value: arc.data.value,
|
||||
color: arc.data.color,
|
||||
key: label,
|
||||
},
|
||||
tooltipTop: centroidY + height / 2,
|
||||
tooltipLeft: centroidX + width / 2,
|
||||
});
|
||||
setActive(arc.data);
|
||||
}}
|
||||
onMouseLeave={(): void => {
|
||||
hideTooltip();
|
||||
setActive(null);
|
||||
}}
|
||||
>
|
||||
<path d={arcPath || ''} fill={getFillColor(arcFill)} />
|
||||
{hasSpaceForLabel && (
|
||||
<text
|
||||
x={centroidX}
|
||||
y={centroidY}
|
||||
dy=".33em"
|
||||
fill="#000"
|
||||
fontSize={10}
|
||||
textAnchor="middle"
|
||||
pointerEvents="none"
|
||||
>
|
||||
{arc.data.label}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Pie>
|
||||
</Group>
|
||||
</svg>
|
||||
{tooltipOpen && tooltipData && (
|
||||
<TooltipInPortal
|
||||
top={tooltipTop}
|
||||
left={tooltipLeft}
|
||||
style={{
|
||||
...tooltipStyles,
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400,
|
||||
}}
|
||||
className="piechart-tooltip"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: tooltipData.color,
|
||||
}}
|
||||
className="piechart-indicator"
|
||||
/>
|
||||
{tooltipData.key}
|
||||
<div className="tooltip-value">{tooltipData.value}</div>
|
||||
</TooltipInPortal>
|
||||
)}
|
||||
</div>
|
||||
<div className="piechart-legend">
|
||||
{pieChartData.length > 0 &&
|
||||
pieChartData.map((data) => (
|
||||
<div
|
||||
key={data.label}
|
||||
className="piechart-legend-item"
|
||||
onMouseEnter={(): void => {
|
||||
setActive(data);
|
||||
}}
|
||||
onMouseLeave={(): void => {
|
||||
setActive(null);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: getFillColor(data.color),
|
||||
}}
|
||||
className="piechart-legend-label"
|
||||
/>
|
||||
{data.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PiePanelWrapper;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import ListPanelWrapper from './ListPanelWrapper';
|
||||
import PiePanelWrapper from './PiePanelWrapper';
|
||||
import TablePanelWrapper from './TablePanelWrapper';
|
||||
import UplotPanelWrapper from './UplotPanelWrapper';
|
||||
import ValuePanelWrapper from './ValuePanelWrapper';
|
||||
@@ -12,5 +13,6 @@ export const PanelTypeVsPanelWrapper = {
|
||||
[PANEL_TYPES.VALUE]: ValuePanelWrapper,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
[PANEL_TYPES.PIE]: PiePanelWrapper,
|
||||
[PANEL_TYPES.BAR]: UplotPanelWrapper,
|
||||
};
|
||||
|
||||
@@ -22,3 +22,10 @@ export type PanelWrapperProps = {
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
selectedGraph?: PANEL_TYPES;
|
||||
};
|
||||
|
||||
export type TooltipData = {
|
||||
label: string;
|
||||
key: string;
|
||||
value: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
73
frontend/src/container/PanelWrapper/utils.ts
Normal file
73
frontend/src/container/PanelWrapper/utils.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { defaultStyles } from '@visx/tooltip';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const tooltipStyles = {
|
||||
...defaultStyles,
|
||||
minWidth: 60,
|
||||
backgroundColor: 'rgba(0,0,0,0.9)',
|
||||
color: 'white',
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '5px 10px',
|
||||
};
|
||||
|
||||
export const getLabel = (
|
||||
label: string,
|
||||
query: Query,
|
||||
queryName: string,
|
||||
isQueryContentMultipleResult = false, // If there are more than one aggregation return by the query, this should be set to true. Default is false.
|
||||
): string => {
|
||||
let finalQuery;
|
||||
if (!isQueryContentMultipleResult) {
|
||||
finalQuery = query.builder.queryData.find((q) => q.queryName === queryName);
|
||||
if (!finalQuery) {
|
||||
// If the query is not found in queryData, then check in queryFormulas
|
||||
finalQuery = query.builder.queryFormulas.find(
|
||||
(q) => q.queryName === queryName,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (finalQuery) {
|
||||
if (finalQuery.legend !== '') {
|
||||
return finalQuery.legend;
|
||||
}
|
||||
if (label !== undefined) {
|
||||
return label;
|
||||
}
|
||||
return queryName;
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
// Function to convert a hex color to RGB format
|
||||
const hexToRgb = (
|
||||
color: string,
|
||||
): { r: number; g: number; b: number } | null => {
|
||||
const hex = color.replace(
|
||||
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
|
||||
(m, r, g, b) => r + r + g + g + b + b,
|
||||
);
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export const lightenColor = (color: string, opacity: number): string => {
|
||||
// Convert the hex color to RGB format
|
||||
const rgbColor = hexToRgb(color);
|
||||
if (!rgbColor) return color; // Return the original color if unable to parse
|
||||
|
||||
// Extract the RGB components
|
||||
const { r, g, b } = rgbColor;
|
||||
|
||||
// Create a new RGBA color string with the specified opacity
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import i18n from 'ReactI18';
|
||||
import store from 'store';
|
||||
|
||||
import ChangeHistory from '../index';
|
||||
import { pipelineData, pipelineDataHistory } from './testUtils';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('ChangeHistory test', () => {
|
||||
it('should render changeHistory correctly', () => {
|
||||
const { getAllByText, getByText } = render(
|
||||
<MemoryRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ChangeHistory pipelineData={pipelineData} />
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// change History table headers
|
||||
[
|
||||
'Version',
|
||||
'Deployment Stage',
|
||||
'Last Deploy Message',
|
||||
'Last Deployed Time',
|
||||
'Edited by',
|
||||
].forEach((text) => expect(getByText(text)).toBeInTheDocument());
|
||||
|
||||
// table content
|
||||
expect(getAllByText('test-user').length).toBe(2);
|
||||
expect(getAllByText('Deployment was successful').length).toBe(2);
|
||||
});
|
||||
|
||||
it('test deployment stage and icon based on history data', () => {
|
||||
const { getByText, container } = render(
|
||||
<MemoryRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ChangeHistory
|
||||
pipelineData={{
|
||||
...pipelineData,
|
||||
history: pipelineDataHistory,
|
||||
}}
|
||||
/>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// assertion for different deployment stages
|
||||
expect(container.querySelector('[data-icon="loading"]')).toBeInTheDocument();
|
||||
expect(getByText('In Progress')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-icon="exclamation-circle"]'),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Dirty')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-icon="close-circle"]'),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Failed')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-icon="minus-circle"]'),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Unknown')).toBeInTheDocument();
|
||||
|
||||
expect(container.querySelectorAll('.ant-table-row').length).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { Pipeline } from 'types/api/pipeline/def';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const pipelineData: Pipeline = {
|
||||
id: 'test-id-1',
|
||||
version: 24,
|
||||
elementType: 'log_pipelines',
|
||||
active: false,
|
||||
is_valid: false,
|
||||
disabled: false,
|
||||
deployStatus: 'DEPLOYED',
|
||||
deployResult: 'Deployment was successful',
|
||||
lastHash: 'log_pipelines:24',
|
||||
lastConf: 'oiwernveroi',
|
||||
createdBy: 'test-created-by',
|
||||
pipelines: [
|
||||
{
|
||||
id: 'test-id-2',
|
||||
orderId: 1,
|
||||
name: 'hotrod logs parser',
|
||||
alias: 'hotrodlogsparser',
|
||||
description: 'Trying to test Logs Pipeline feature',
|
||||
enabled: true,
|
||||
filter: {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: 'container_name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
id: 'sampleid',
|
||||
value: 'hotrod',
|
||||
op: '=',
|
||||
},
|
||||
],
|
||||
},
|
||||
config: [
|
||||
{
|
||||
type: 'regex_parser',
|
||||
id: 'parsetext(regex)',
|
||||
output: 'parseattribsjson',
|
||||
on_error: 'send',
|
||||
orderId: 1,
|
||||
enabled: true,
|
||||
name: 'parse text (regex)',
|
||||
parse_to: 'attributes',
|
||||
regex:
|
||||
'.+\\t+(?P<log_level>.+)\\t+(?P<location>.+)\\t+(?P<message>.+)\\t+(?P<attribs_json>.+)',
|
||||
parse_from: 'body',
|
||||
},
|
||||
{
|
||||
type: 'json_parser',
|
||||
id: 'parseattribsjson',
|
||||
output: 'removetempattribs_json',
|
||||
orderId: 2,
|
||||
enabled: true,
|
||||
name: 'parse attribs json',
|
||||
parse_to: 'attributes',
|
||||
parse_from: 'attributes.attribs_json',
|
||||
},
|
||||
{
|
||||
type: 'remove',
|
||||
id: 'removetempattribs_json',
|
||||
output: 'c2062723-895e-4614-ba38-29c5d5ee5927',
|
||||
orderId: 3,
|
||||
enabled: true,
|
||||
name: 'remove temp attribs_json',
|
||||
field: 'attributes.attribs_json',
|
||||
},
|
||||
{
|
||||
type: 'add',
|
||||
id: 'c2062723-895e-4614-ba38-29c5d5ee5927',
|
||||
orderId: 4,
|
||||
enabled: true,
|
||||
name: 'test add ',
|
||||
field: 'resource["container.name"]',
|
||||
value: 'hotrod',
|
||||
},
|
||||
],
|
||||
createdBy: 'test@email',
|
||||
createdAt: '2024-01-02T13:56:02.858300964Z',
|
||||
},
|
||||
{
|
||||
id: 'tes-id-1',
|
||||
orderId: 2,
|
||||
name: 'Logs Parser - test - Customer Service',
|
||||
alias: 'LogsParser-test-CustomerService',
|
||||
description: 'Trying to test Logs Pipeline feature',
|
||||
enabled: true,
|
||||
filter: {
|
||||
op: 'AND',
|
||||
items: [
|
||||
{
|
||||
key: {
|
||||
key: 'service',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
id: 'sample-test-1',
|
||||
value: 'customer',
|
||||
op: '=',
|
||||
},
|
||||
],
|
||||
},
|
||||
config: [
|
||||
{
|
||||
type: 'grok_parser',
|
||||
id: 'Testtest',
|
||||
on_error: 'send',
|
||||
orderId: 1,
|
||||
enabled: true,
|
||||
name: 'Test test',
|
||||
parse_to: 'attributes',
|
||||
pattern:
|
||||
'^%{DATE:date}Z INFO customer/database.go:73 Loading customer {"service": "customer", "component": "mysql", "trace_id": "test-id", "span_id": "1427a3fcad8b1514", "customer_id": "567"}',
|
||||
parse_from: 'body',
|
||||
},
|
||||
],
|
||||
createdBy: 'test@email',
|
||||
createdAt: '2024-01-02T13:56:02.863764227Z',
|
||||
},
|
||||
],
|
||||
history: [
|
||||
{
|
||||
id: 'test-id-4',
|
||||
version: 24,
|
||||
elementType: 'log_pipelines',
|
||||
active: false,
|
||||
isValid: false,
|
||||
disabled: false,
|
||||
deployStatus: 'DEPLOYED',
|
||||
deployResult: 'Deployment was successful',
|
||||
lastHash: 'log_pipelines:24',
|
||||
lastConf: 'eovineroiv',
|
||||
createdBy: 'test-created-by',
|
||||
createdByName: 'test-user',
|
||||
createdAt: '2024-01-02T13:56:02Z',
|
||||
},
|
||||
{
|
||||
id: 'test-4',
|
||||
version: 23,
|
||||
elementType: 'log_pipelines',
|
||||
active: false,
|
||||
isValid: false,
|
||||
disabled: false,
|
||||
deployStatus: 'DEPLOYED',
|
||||
deployResult: 'Deployment was successful',
|
||||
lastHash: 'log_pipelines:23',
|
||||
lastConf: 'eivrounreovi',
|
||||
createdBy: 'test-created-by',
|
||||
createdByName: 'test-user',
|
||||
createdAt: '2023-12-29T12:59:20Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const pipelineDataHistory: Pipeline['history'] = [
|
||||
{
|
||||
id: 'test-id-4',
|
||||
version: 24,
|
||||
elementType: 'log_pipelines',
|
||||
active: false,
|
||||
isValid: false,
|
||||
disabled: false,
|
||||
deployStatus: 'DEPLOYED',
|
||||
deployResult: 'Deployment was successful',
|
||||
lastHash: 'log_pipelines:24',
|
||||
lastConf: 'eovineroiv',
|
||||
createdBy: 'test-created-by',
|
||||
createdByName: 'test-user',
|
||||
createdAt: '2024-01-02T13:56:02Z',
|
||||
},
|
||||
{
|
||||
id: 'test-4',
|
||||
version: 23,
|
||||
elementType: 'log_pipelines',
|
||||
active: false,
|
||||
isValid: false,
|
||||
disabled: false,
|
||||
deployStatus: 'IN_PROGRESS',
|
||||
deployResult: 'Deployment is in progress',
|
||||
lastHash: 'log_pipelines:23',
|
||||
lastConf: 'eivrounreovi',
|
||||
createdBy: 'test-created-by',
|
||||
createdByName: 'test-user',
|
||||
createdAt: '2023-12-29T12:59:20Z',
|
||||
},
|
||||
{
|
||||
id: 'test-4-1',
|
||||
version: 25,
|
||||
elementType: 'log_pipelines',
|
||||
active: false,
|
||||
isValid: false,
|
||||
disabled: false,
|
||||
deployStatus: 'DIRTY',
|
||||
deployResult: 'Deployment is dirty',
|
||||
lastHash: 'log_pipelines:23',
|
||||
lastConf: 'eivrounreovi',
|
||||
createdBy: 'test-created-by',
|
||||
createdByName: 'test-user',
|
||||
createdAt: '2023-12-29T12:59:20Z',
|
||||
},
|
||||
{
|
||||
id: 'test-4-2',
|
||||
version: 26,
|
||||
elementType: 'log_pipelines',
|
||||
active: false,
|
||||
isValid: false,
|
||||
disabled: false,
|
||||
deployStatus: 'FAILED',
|
||||
deployResult: 'Deployment failed',
|
||||
lastHash: 'log_pipelines:23',
|
||||
lastConf: 'eivrounreovi',
|
||||
createdBy: 'test-created-by',
|
||||
createdByName: 'test-user',
|
||||
createdAt: '2023-12-29T12:59:20Z',
|
||||
},
|
||||
{
|
||||
id: 'test-4-3',
|
||||
version: 27,
|
||||
elementType: 'log_pipelines',
|
||||
active: false,
|
||||
isValid: false,
|
||||
disabled: false,
|
||||
deployStatus: 'UNKNOWN',
|
||||
deployResult: '',
|
||||
lastHash: 'log_pipelines:23',
|
||||
lastConf: 'eivrounreovi',
|
||||
createdBy: 'test-created-by',
|
||||
createdByName: 'test-user',
|
||||
createdAt: '2023-12-29T12:59:20Z',
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -8,8 +9,17 @@ import store from 'store';
|
||||
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
|
||||
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
||||
|
||||
const trackEventVar = jest.fn();
|
||||
jest.mock('hooks/analytics/useAnalytics', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
trackEvent: trackEventVar,
|
||||
trackPageView: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
it('should render CreatePipelineButton section', () => {
|
||||
it('should render CreatePipelineButton section', async () => {
|
||||
const { asFragment } = render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
@@ -26,4 +36,58 @@ describe('PipelinePage container test', () => {
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('CreatePipelineButton - edit mode & tracking', async () => {
|
||||
const { getByText } = render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<CreatePipelineButton
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="viewing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={pipelineApiResponseMockData}
|
||||
/>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// enter_edit_mode click and track event data
|
||||
const editButton = getByText('enter_edit_mode');
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
|
||||
expect(trackEventVar).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
|
||||
source: 'signoz-ui',
|
||||
});
|
||||
});
|
||||
|
||||
it('CreatePipelineButton - add new mode & tracking', async () => {
|
||||
const { getByText } = render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<CreatePipelineButton
|
||||
setActionType={jest.fn()}
|
||||
isActionMode="viewing-mode"
|
||||
setActionMode={jest.fn()}
|
||||
pipelineData={{ ...pipelineApiResponseMockData, pipelines: [] }}
|
||||
/>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
// new_pipeline click and track event data
|
||||
const editButton = getByText('new_pipeline');
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
|
||||
expect(trackEventVar).toBeCalledWith(
|
||||
'Logs: Pipelines: Clicked Add New Pipeline',
|
||||
{
|
||||
source: 'signoz-ui',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -20,4 +21,43 @@ describe('PipelinePage container test', () => {
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle search', async () => {
|
||||
const setPipelineValue = jest.fn();
|
||||
const { getByPlaceholderText, container } = render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<PipelinesSearchSection setPipelineSearchValue={setPipelineValue} />
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const searchInput = getByPlaceholderText('search_pipeline_placeholder');
|
||||
|
||||
// Type into the search input
|
||||
userEvent.type(searchInput, 'sample_pipeline');
|
||||
|
||||
jest.advanceTimersByTime(299);
|
||||
expect(setPipelineValue).not.toHaveBeenCalled();
|
||||
|
||||
// Wait for the debounce delay to pass
|
||||
await waitFor(() => {
|
||||
// Expect the callback to be called after debounce delay
|
||||
expect(setPipelineValue).toHaveBeenCalledWith('sample_pipeline');
|
||||
});
|
||||
|
||||
// clear button
|
||||
fireEvent.click(
|
||||
container.querySelector(
|
||||
'span[class*="ant-input-clear-icon"]',
|
||||
) as HTMLElement,
|
||||
);
|
||||
|
||||
// Wait for the debounce delay to pass
|
||||
await waitFor(() => {
|
||||
expect(setPipelineValue).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,8 +105,8 @@ export default function QBEntityOptions({
|
||||
onQueryFunctionsUpdates && (
|
||||
<QueryFunctions
|
||||
query={query}
|
||||
queryFunctions={query.functions}
|
||||
key={query.functions.toString()}
|
||||
queryFunctions={query.functions || []}
|
||||
key={query.functions?.toString()}
|
||||
onChange={onQueryFunctionsUpdates}
|
||||
maxFunctions={isLogsDataSource ? 1 : 3}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
|
||||
import LeftToolbarActions from '../LeftToolbarActions';
|
||||
import RightToolbarActions from '../RightToolbarActions';
|
||||
|
||||
describe('ToolbarActions', () => {
|
||||
it('LeftToolbarActions - renders correctly with default props', async () => {
|
||||
const handleChangeSelectedView = jest.fn();
|
||||
const handleToggleShowHistogram = jest.fn();
|
||||
const { queryByTestId } = render(
|
||||
<LeftToolbarActions
|
||||
items={{
|
||||
search: {
|
||||
name: 'search',
|
||||
label: 'Search',
|
||||
disabled: false,
|
||||
show: true,
|
||||
},
|
||||
queryBuilder: {
|
||||
name: 'query-builder',
|
||||
label: 'Query Builder',
|
||||
disabled: false,
|
||||
show: true,
|
||||
},
|
||||
clickhouse: {
|
||||
name: 'clickhouse',
|
||||
label: 'Clickhouse',
|
||||
disabled: false,
|
||||
},
|
||||
}}
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
onToggleHistrogramVisibility={handleToggleShowHistogram}
|
||||
showHistogram
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId('search-view')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('query-builder-view')).toBeInTheDocument();
|
||||
|
||||
// clickhouse should not be present as its show: false
|
||||
expect(queryByTestId('clickhouse-view')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByTestId('search-view'));
|
||||
expect(handleChangeSelectedView).toBeCalled();
|
||||
|
||||
await userEvent.click(screen.getByTestId('query-builder-view'));
|
||||
expect(handleChangeSelectedView).toBeCalled();
|
||||
});
|
||||
|
||||
it('renders - clickhouse view and test histogram toggle', async () => {
|
||||
const handleChangeSelectedView = jest.fn();
|
||||
const handleToggleShowHistogram = jest.fn();
|
||||
const { queryByTestId, getByRole } = render(
|
||||
<LeftToolbarActions
|
||||
items={{
|
||||
search: {
|
||||
name: 'search',
|
||||
label: 'Search',
|
||||
disabled: false,
|
||||
show: false,
|
||||
},
|
||||
queryBuilder: {
|
||||
name: 'query-builder',
|
||||
label: 'Query Builder',
|
||||
disabled: false,
|
||||
show: true,
|
||||
},
|
||||
clickhouse: {
|
||||
name: 'clickhouse',
|
||||
label: 'Clickhouse',
|
||||
disabled: false,
|
||||
show: true,
|
||||
},
|
||||
}}
|
||||
selectedView={SELECTED_VIEWS.QUERY_BUILDER}
|
||||
onChangeSelectedView={handleChangeSelectedView}
|
||||
onToggleHistrogramVisibility={handleToggleShowHistogram}
|
||||
showHistogram
|
||||
/>,
|
||||
);
|
||||
|
||||
const clickHouseView = queryByTestId('clickhouse-view');
|
||||
expect(clickHouseView).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(clickHouseView as HTMLElement);
|
||||
expect(handleChangeSelectedView).toBeCalled();
|
||||
|
||||
await userEvent.click(getByRole('switch'));
|
||||
expect(handleToggleShowHistogram).toBeCalled();
|
||||
});
|
||||
|
||||
it('RightToolbarActions - render correctly with props', async () => {
|
||||
const onStageRunQuery = jest.fn();
|
||||
const { queryByText } = render(
|
||||
<RightToolbarActions onStageRunQuery={onStageRunQuery} />,
|
||||
);
|
||||
|
||||
const stageNRunBtn = queryByText('Stage & Run Query');
|
||||
expect(stageNRunBtn).toBeInTheDocument();
|
||||
await userEvent.click(stageNRunBtn as HTMLElement);
|
||||
expect(onStageRunQuery).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -61,7 +61,6 @@
|
||||
line-height: 18px;
|
||||
|
||||
background: transparent;
|
||||
border-left: 2px solid transparent;
|
||||
|
||||
transition: 0.2s all linear;
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ function SideNav({
|
||||
}, [isCloudUserVal, isEnterprise, isFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCloudUserVal) {
|
||||
if (!(isCloudUserVal || isEECloudUser())) {
|
||||
let updatedMenuItems = [...menuItems];
|
||||
updatedMenuItems = updatedMenuItems.filter(
|
||||
(item) => item.key !== ROUTES.INTEGRATIONS,
|
||||
|
||||
@@ -13,11 +13,18 @@ function Events({
|
||||
return <Typography>No events data in selected span</Typography>;
|
||||
}
|
||||
|
||||
const sortedTraceEvents = events.sort((a, b) => {
|
||||
// Handle undefined names by treating them as empty strings
|
||||
const nameA = a.name || '';
|
||||
const nameB = b.name || '';
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
return (
|
||||
<ErrorTag
|
||||
onToggleHandler={onToggleHandler}
|
||||
setText={setText}
|
||||
event={events}
|
||||
event={sortedTraceEvents}
|
||||
firstSpanStartTime={firstSpanStartTime}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -41,8 +41,9 @@ function Tags({
|
||||
setSearchText(value);
|
||||
};
|
||||
|
||||
const filteredTags = tags.filter((tag) => tag.key.includes(searchText));
|
||||
|
||||
const filteredTags = tags
|
||||
.filter((tag) => tag.key.includes(searchText))
|
||||
.sort((a, b) => a.key.localeCompare(b.key));
|
||||
if (tags.length === 0) {
|
||||
return <Typography>No tags in selected span</Typography>;
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export const Container = styled.div`
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.3rem;
|
||||
margin: 8px 0;
|
||||
`;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user