mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-09-20 07:23:14 +08:00
commit
a476c68f7e
3
.github/workflows/staging-deployment.yaml
vendored
3
.github/workflows/staging-deployment.yaml
vendored
@ -30,6 +30,7 @@ jobs:
|
|||||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||||
|
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||||
run: |
|
run: |
|
||||||
read -r -d '' COMMAND <<EOF || true
|
read -r -d '' COMMAND <<EOF || true
|
||||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||||
@ -51,4 +52,4 @@ jobs:
|
|||||||
make build-frontend-amd64
|
make build-frontend-amd64
|
||||||
make run-testing
|
make run-testing
|
||||||
EOF
|
EOF
|
||||||
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||||
|
3
.github/workflows/testing-deployment.yaml
vendored
3
.github/workflows/testing-deployment.yaml
vendored
@ -30,6 +30,7 @@ jobs:
|
|||||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||||
|
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||||
run: |
|
run: |
|
||||||
read -r -d '' COMMAND <<EOF || true
|
read -r -d '' COMMAND <<EOF || true
|
||||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||||
@ -52,4 +53,4 @@ jobs:
|
|||||||
make build-frontend-amd64
|
make build-frontend-amd64
|
||||||
make run-testing
|
make run-testing
|
||||||
EOF
|
EOF
|
||||||
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||||
|
@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.50.0
|
image: signoz/query-service:0.51.0
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
@ -199,7 +199,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.102.2
|
image: signoz/signoz-otel-collector:0.102.3
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@ -237,7 +237,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.102.2
|
image: signoz/signoz-schema-migrator:0.102.3
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
@ -66,7 +66,7 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--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`
|
# 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:
|
otel-collector:
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.102.2
|
image: signoz/signoz-otel-collector:0.102.3
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
@ -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`
|
# 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:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.50.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.51.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@ -204,7 +204,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.50.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.51.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -216,7 +216,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@ -230,7 +230,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.3}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
@ -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`
|
# 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:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.50.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.51.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@ -203,7 +203,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.50.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.51.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -215,7 +215,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@ -229,7 +229,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.3}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
|
||||||
@ -29,6 +31,10 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
// Get the dashboard UUID from the request
|
// Get the dashboard UUID from the request
|
||||||
uuid := mux.Vars(r)["uuid"]
|
uuid := mux.Vars(r)["uuid"]
|
||||||
|
if strings.HasPrefix(uuid,"integration") {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: errors.New("dashboards created by integrations cannot be unlocked")}, "You are not authorized to lock/unlock this dashboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
|
dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())
|
||||||
|
@ -4,11 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
|
||||||
_ "net/http/pprof" // http profiler
|
_ "net/http/pprof" // http profiler
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
|
||||||
"go.signoz.io/signoz/ee/query-service/interfaces"
|
"go.signoz.io/signoz/ee/query-service/interfaces"
|
||||||
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
|
||||||
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
licensepkg "go.signoz.io/signoz/ee/query-service/license"
|
||||||
@ -41,6 +42,7 @@ import (
|
|||||||
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
"go.signoz.io/signoz/pkg/query-service/app/opamp"
|
||||||
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/app/preferences"
|
||||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||||
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
||||||
@ -110,6 +112,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
|
|
||||||
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
|
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||||
|
|
||||||
|
if err := preferences.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -118,33 +124,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
|
|
||||||
localDB.SetMaxOpenConns(10)
|
localDB.SetMaxOpenConns(10)
|
||||||
|
|
||||||
gatewayFeature := basemodel.Feature{
|
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||||
Name: "GATEWAY",
|
|
||||||
Active: false,
|
|
||||||
Usage: 0,
|
|
||||||
UsageLimit: -1,
|
|
||||||
Route: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
//Activate this feature if the url is not empty
|
|
||||||
var gatewayProxy *httputil.ReverseProxy
|
|
||||||
if serverOptions.GatewayUrl == "" {
|
|
||||||
gatewayFeature.Active = false
|
|
||||||
gatewayProxy, err = gateway.NewNoopProxy()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
zap.L().Info("Enabling gateway feature flag ...")
|
|
||||||
gatewayFeature.Active = true
|
|
||||||
gatewayProxy, err = gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initiate license manager
|
// initiate license manager
|
||||||
lm, err := licensepkg.StartManager("sqlite", localDB, gatewayFeature)
|
lm, err := licensepkg.StartManager("sqlite", localDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -340,7 +326,17 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
|
|
||||||
// add auth middleware
|
// add auth middleware
|
||||||
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
|
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
|
||||||
return auth.GetUserFromRequest(r, apiHandler)
|
user, err := auth.GetUserFromRequest(r, apiHandler)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.User.OrgId == "" {
|
||||||
|
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
||||||
|
|
||||||
|
@ -20,11 +20,14 @@ import (
|
|||||||
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
|
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
|
||||||
// get auth domain from email domain
|
// get auth domain from email domain
|
||||||
domain, apierr := m.GetDomainByEmail(ctx, email)
|
domain, apierr := m.GetDomainByEmail(ctx, email)
|
||||||
|
|
||||||
if apierr != nil {
|
if apierr != nil {
|
||||||
zap.L().Error("failed to get domain from email", zap.Error(apierr))
|
zap.L().Error("failed to get domain from email", zap.Error(apierr))
|
||||||
return nil, model.InternalErrorStr("failed to get domain from email")
|
return nil, model.InternalErrorStr("failed to get domain from email")
|
||||||
}
|
}
|
||||||
|
if domain == nil {
|
||||||
|
zap.L().Error("email domain does not match any authenticated domain", zap.String("email", email))
|
||||||
|
return nil, model.InternalErrorStr("email domain does not match any authenticated domain")
|
||||||
|
}
|
||||||
|
|
||||||
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
|
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -5,5 +5,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewNoopProxy() (*httputil.ReverseProxy, error) {
|
func NewNoopProxy() (*httputil.ReverseProxy, error) {
|
||||||
return nil, nil
|
return &httputil.ReverseProxy{}, nil
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ const Enterprise = "ENTERPRISE_PLAN"
|
|||||||
const DisableUpsell = "DISABLE_UPSELL"
|
const DisableUpsell = "DISABLE_UPSELL"
|
||||||
const Onboarding = "ONBOARDING"
|
const Onboarding = "ONBOARDING"
|
||||||
const ChatSupport = "CHAT_SUPPORT"
|
const ChatSupport = "CHAT_SUPPORT"
|
||||||
|
const Gateway = "GATEWAY"
|
||||||
|
|
||||||
var BasicPlan = basemodel.FeatureSet{
|
var BasicPlan = basemodel.FeatureSet{
|
||||||
basemodel.Feature{
|
basemodel.Feature{
|
||||||
@ -111,6 +112,13 @@ var BasicPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: Gateway,
|
||||||
|
Active: false,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var ProPlan = basemodel.FeatureSet{
|
var ProPlan = basemodel.FeatureSet{
|
||||||
@ -205,6 +213,13 @@ var ProPlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: Gateway,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var EnterprisePlan = basemodel.FeatureSet{
|
var EnterprisePlan = basemodel.FeatureSet{
|
||||||
@ -313,4 +328,11 @@ var EnterprisePlan = basemodel.FeatureSet{
|
|||||||
UsageLimit: -1,
|
UsageLimit: -1,
|
||||||
Route: "",
|
Route: "",
|
||||||
},
|
},
|
||||||
|
basemodel.Feature{
|
||||||
|
Name: Gateway,
|
||||||
|
Active: true,
|
||||||
|
Usage: 0,
|
||||||
|
UsageLimit: -1,
|
||||||
|
Route: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import NotFound from 'components/NotFound';
|
import NotFound from 'components/NotFound';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
@ -48,7 +49,7 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const { trackPageView, trackEvent } = useAnalytics();
|
const { trackPageView } = useAnalytics();
|
||||||
|
|
||||||
const { hostname, pathname } = window.location;
|
const { hostname, pathname } = window.location;
|
||||||
|
|
||||||
@ -199,7 +200,7 @@ function App(): JSX.Element {
|
|||||||
LOCALSTORAGE.THEME_ANALYTICS_V1,
|
LOCALSTORAGE.THEME_ANALYTICS_V1,
|
||||||
);
|
);
|
||||||
if (!isThemeAnalyticsSent) {
|
if (!isThemeAnalyticsSent) {
|
||||||
trackEvent('Theme Analytics', {
|
logEvent('Theme Analytics', {
|
||||||
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
|
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
|
||||||
user: pick(user, ['email', 'userId', 'name']),
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
org,
|
org,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import axios from 'api';
|
import { ApiBaseInstance as axios } from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
@ -21,6 +21,7 @@ const logEvent = async (
|
|||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -96,6 +96,10 @@ const interceptorRejected = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const interceptorRejectedBase = async (
|
||||||
|
value: AxiosResponse<any>,
|
||||||
|
): Promise<AxiosResponse<any>> => Promise.reject(value);
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||||
});
|
});
|
||||||
@ -140,6 +144,18 @@ ApiV4Instance.interceptors.response.use(
|
|||||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// axios Base
|
||||||
|
export const ApiBaseInstance = axios.create({
|
||||||
|
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
ApiBaseInstance.interceptors.response.use(
|
||||||
|
interceptorsResponse,
|
||||||
|
interceptorRejectedBase,
|
||||||
|
);
|
||||||
|
ApiBaseInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||||
|
//
|
||||||
|
|
||||||
// gateway Api V1
|
// gateway Api V1
|
||||||
export const GatewayApiV1Instance = axios.create({
|
export const GatewayApiV1Instance = axios.create({
|
||||||
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,
|
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,
|
||||||
|
@ -5,7 +5,6 @@ import { Button } from 'antd';
|
|||||||
import { Tag } from 'antd/lib';
|
import { Tag } from 'antd/lib';
|
||||||
import Input from 'components/Input';
|
import Input from 'components/Input';
|
||||||
import { Check, X } from 'lucide-react';
|
import { Check, X } from 'lucide-react';
|
||||||
import { TweenOneGroup } from 'rc-tween-one';
|
|
||||||
import React, { Dispatch, SetStateAction, useState } from 'react';
|
import React, { Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
|
||||||
function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
|
function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
|
||||||
@ -46,41 +45,19 @@ function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
|
|||||||
func(value);
|
func(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const forMap = (tag: string): React.ReactElement => (
|
|
||||||
<span key={tag} style={{ display: 'inline-block' }}>
|
|
||||||
<Tag
|
|
||||||
closable
|
|
||||||
onClose={(e): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleClose(tag);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
const tagChild = tags.map(forMap);
|
|
||||||
|
|
||||||
const renderTagsAnimated = (): React.ReactElement => (
|
|
||||||
<TweenOneGroup
|
|
||||||
appear={false}
|
|
||||||
className="tags"
|
|
||||||
enter={{ scale: 0.8, opacity: 0, type: 'from', duration: 100 }}
|
|
||||||
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
|
|
||||||
onEnd={(e): void => {
|
|
||||||
if (e.type === 'appear' || e.type === 'enter') {
|
|
||||||
(e.target as any).style = 'display: inline-block';
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tagChild}
|
|
||||||
</TweenOneGroup>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tags-container">
|
<div className="tags-container">
|
||||||
{renderTagsAnimated()}
|
{tags.map<React.ReactNode>((tag) => (
|
||||||
|
<Tag
|
||||||
|
key={tag}
|
||||||
|
closable
|
||||||
|
style={{ userSelect: 'none' }}
|
||||||
|
onClose={(): void => handleClose(tag)}
|
||||||
|
>
|
||||||
|
<span>{tag}</span>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
|
||||||
{inputVisible && (
|
{inputVisible && (
|
||||||
<div className="add-tag-container">
|
<div className="add-tag-container">
|
||||||
<Input
|
<Input
|
||||||
|
@ -49,7 +49,10 @@ function ValueGraph({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
|
||||||
<ExclamationCircleFilled className="value-graph-icon" />
|
<ExclamationCircleFilled
|
||||||
|
className="value-graph-icon"
|
||||||
|
data-testid="conflicting-thresholds"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -16,6 +16,7 @@ export interface FacingIssueBtnProps {
|
|||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onHoverText?: string;
|
onHoverText?: string;
|
||||||
|
intercomMessageDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FacingIssueBtn({
|
function FacingIssueBtn({
|
||||||
@ -25,11 +26,12 @@ function FacingIssueBtn({
|
|||||||
buttonText = '',
|
buttonText = '',
|
||||||
className = '',
|
className = '',
|
||||||
onHoverText = '',
|
onHoverText = '',
|
||||||
|
intercomMessageDisabled = false,
|
||||||
}: FacingIssueBtnProps): JSX.Element | null {
|
}: FacingIssueBtnProps): JSX.Element | null {
|
||||||
const handleFacingIssuesClick = (): void => {
|
const handleFacingIssuesClick = (): void => {
|
||||||
logEvent(eventName, attributes);
|
logEvent(eventName, attributes);
|
||||||
|
|
||||||
if (window.Intercom) {
|
if (window.Intercom && !intercomMessageDisabled) {
|
||||||
window.Intercom('showNewMessage', defaultTo(message, ''));
|
window.Intercom('showNewMessage', defaultTo(message, ''));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -62,6 +64,7 @@ FacingIssueBtn.defaultProps = {
|
|||||||
buttonText: '',
|
buttonText: '',
|
||||||
className: '',
|
className: '',
|
||||||
onHoverText: '',
|
onHoverText: '',
|
||||||
|
intercomMessageDisabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FacingIssueBtn;
|
export default FacingIssueBtn;
|
||||||
|
@ -423,9 +423,9 @@ function AllErrors(): JSX.Element {
|
|||||||
)?.tagValue;
|
)?.tagValue;
|
||||||
|
|
||||||
logEvent('Exception: List page visited', {
|
logEvent('Exception: List page visited', {
|
||||||
numberOfExceptions: errorCountResponse.data?.payload,
|
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||||
selectedEnvironments,
|
selectedEnvironments,
|
||||||
resourceAttributeUsed: !!queries.length,
|
resourceAttributeUsed: !!queries?.length,
|
||||||
});
|
});
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,10 @@ import { ColumnsType } from 'antd/es/table';
|
|||||||
import updateCreditCardApi from 'api/billing/checkout';
|
import updateCreditCardApi from 'api/billing/checkout';
|
||||||
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||||
import manageCreditCardApi from 'api/billing/manage';
|
import manageCreditCardApi from 'api/billing/manage';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
import useAxiosError from 'hooks/useAxiosError';
|
||||||
import useLicense from 'hooks/useLicense';
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@ -137,8 +137,6 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
Partial<UsageResponsePayloadProps>
|
Partial<UsageResponsePayloadProps>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const { isFetching, data: licensesData, error: licenseError } = useLicense();
|
const { isFetching, data: licensesData, error: licenseError } = useLicense();
|
||||||
|
|
||||||
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
@ -316,7 +314,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
const handleBilling = useCallback(async () => {
|
const handleBilling = useCallback(async () => {
|
||||||
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
||||||
trackEvent('Billing : Upgrade Plan', {
|
logEvent('Billing : Upgrade Plan', {
|
||||||
user: pick(user, ['email', 'userId', 'name']),
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
org,
|
org,
|
||||||
});
|
});
|
||||||
@ -327,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
cancelURL: window.location.href,
|
cancelURL: window.location.href,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
trackEvent('Billing : Manage Billing', {
|
logEvent('Billing : Manage Billing', {
|
||||||
user: pick(user, ['email', 'userId', 'name']),
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
org,
|
org,
|
||||||
});
|
});
|
||||||
|
@ -449,8 +449,8 @@ function CreateAlertChannels({
|
|||||||
const result = await functionToCall();
|
const result = await functionToCall();
|
||||||
logEvent('Alert Channel: Save channel', {
|
logEvent('Alert Channel: Save channel', {
|
||||||
type: value,
|
type: value,
|
||||||
sendResolvedAlert: selectedConfig.send_resolved,
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
name: selectedConfig.name,
|
name: selectedConfig?.name,
|
||||||
new: 'true',
|
new: 'true',
|
||||||
status: result?.status,
|
status: result?.status,
|
||||||
statusMessage: result?.statusMessage,
|
statusMessage: result?.statusMessage,
|
||||||
@ -530,8 +530,8 @@ function CreateAlertChannels({
|
|||||||
|
|
||||||
logEvent('Alert Channel: Test notification', {
|
logEvent('Alert Channel: Test notification', {
|
||||||
type: channelType,
|
type: channelType,
|
||||||
sendResolvedAlert: selectedConfig.send_resolved,
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
name: selectedConfig.name,
|
name: selectedConfig?.name,
|
||||||
new: 'true',
|
new: 'true',
|
||||||
status:
|
status:
|
||||||
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
||||||
|
@ -370,8 +370,8 @@ function EditAlertChannels({
|
|||||||
}
|
}
|
||||||
logEvent('Alert Channel: Save channel', {
|
logEvent('Alert Channel: Save channel', {
|
||||||
type: value,
|
type: value,
|
||||||
sendResolvedAlert: selectedConfig.send_resolved,
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
name: selectedConfig.name,
|
name: selectedConfig?.name,
|
||||||
new: 'false',
|
new: 'false',
|
||||||
status: result?.status,
|
status: result?.status,
|
||||||
statusMessage: result?.statusMessage,
|
statusMessage: result?.statusMessage,
|
||||||
@ -441,8 +441,8 @@ function EditAlertChannels({
|
|||||||
}
|
}
|
||||||
logEvent('Alert Channel: Test notification', {
|
logEvent('Alert Channel: Test notification', {
|
||||||
type: channelType,
|
type: channelType,
|
||||||
sendResolvedAlert: selectedConfig.send_resolved,
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
name: selectedConfig.name,
|
name: selectedConfig?.name,
|
||||||
new: 'false',
|
new: 'false',
|
||||||
status:
|
status:
|
||||||
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
response && response.statusCode === 200 ? 'Test success' : 'Test failed',
|
||||||
|
@ -114,10 +114,10 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
|
|
||||||
const onClickTraceHandler = (): void => {
|
const onClickTraceHandler = (): void => {
|
||||||
logEvent('Exception: Navigate to trace detail page', {
|
logEvent('Exception: Navigate to trace detail page', {
|
||||||
groupId: errorDetail.groupID,
|
groupId: errorDetail?.groupID,
|
||||||
spanId: errorDetail.spanID,
|
spanId: errorDetail.spanID,
|
||||||
traceId: errorDetail.traceID,
|
traceId: errorDetail.traceID,
|
||||||
exceptionId: errorDetail.errorId,
|
exceptionId: errorDetail?.errorId,
|
||||||
});
|
});
|
||||||
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
||||||
};
|
};
|
||||||
@ -126,10 +126,10 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||||
logEvent('Exception: Detail page visited', {
|
logEvent('Exception: Detail page visited', {
|
||||||
groupId: errorDetail.groupID,
|
groupId: errorDetail?.groupID,
|
||||||
spanId: errorDetail.spanID,
|
spanId: errorDetail.spanID,
|
||||||
traceId: errorDetail.traceID,
|
traceId: errorDetail.traceID,
|
||||||
exceptionId: errorDetail.errorId,
|
exceptionId: errorDetail?.errorId,
|
||||||
});
|
});
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
}
|
}
|
||||||
|
@ -256,12 +256,12 @@ function ExplorerOptions({
|
|||||||
if (sourcepage === DataSource.TRACES) {
|
if (sourcepage === DataSource.TRACES) {
|
||||||
logEvent('Traces Explorer: Select view', {
|
logEvent('Traces Explorer: Select view', {
|
||||||
panelType,
|
panelType,
|
||||||
viewName: option.value,
|
viewName: option?.value,
|
||||||
});
|
});
|
||||||
} else if (sourcepage === DataSource.LOGS) {
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
logEvent('Logs Explorer: Select view', {
|
logEvent('Logs Explorer: Select view', {
|
||||||
panelType,
|
panelType,
|
||||||
viewName: option.value,
|
viewName: option?.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
|
@ -88,7 +88,7 @@ function BasicInfo({
|
|||||||
if (!channels.loading && isNewRule) {
|
if (!channels.loading && isNewRule) {
|
||||||
logEvent('Alert: New alert creation page visited', {
|
logEvent('Alert: New alert creation page visited', {
|
||||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||||
numberOfChannels: channels.payload?.length,
|
numberOfChannels: channels?.payload?.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -125,10 +125,9 @@ function GridCardGraph({
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||||
},
|
},
|
||||||
|
// we do not need select columns in case of logs
|
||||||
selectColumns:
|
selectColumns:
|
||||||
initialDataSource === DataSource.LOGS
|
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
|
||||||
? widget.selectedLogFields
|
|
||||||
: widget.selectedTracesFields,
|
|
||||||
},
|
},
|
||||||
fillGaps: widget.fillSpans,
|
fillGaps: widget.fillSpans,
|
||||||
};
|
};
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: -webkit-fill-available;
|
width: -webkit-fill-available;
|
||||||
|
|
||||||
|
@ -940,3 +940,50 @@
|
|||||||
border-color: var(--bg-vanilla-300) !important;
|
border-color: var(--bg-vanilla-300) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-8 {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-12 {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-24 {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-24 {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingestion-setup-details-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(113, 144, 249, 0.1);
|
||||||
|
color: var(--bg-robin-300, #95acfb);
|
||||||
|
|
||||||
|
.learn-more {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
color: var(--bg-robin-300, #95acfb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.ingestion-setup-details-links {
|
||||||
|
background: rgba(113, 144, 249, 0.1);
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
|
||||||
|
.learn-more {
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -34,11 +34,14 @@ import dayjs, { Dayjs } from 'dayjs';
|
|||||||
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
|
ArrowUpRight,
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
Check,
|
Check,
|
||||||
Copy,
|
Copy,
|
||||||
Infinity,
|
Infinity,
|
||||||
|
Info,
|
||||||
Minus,
|
Minus,
|
||||||
PenLine,
|
PenLine,
|
||||||
Plus,
|
Plus,
|
||||||
@ -603,7 +606,13 @@ function MultiIngestionSettings(): JSX.Element {
|
|||||||
|
|
||||||
<div className="limits-data">
|
<div className="limits-data">
|
||||||
<div className="signals">
|
<div className="signals">
|
||||||
{SIGNALS.map((signal) => (
|
{SIGNALS.map((signal) => {
|
||||||
|
const hasValidDayLimit = !isNil(limits[signal]?.config?.day?.size);
|
||||||
|
const hasValidSecondLimit = !isNil(
|
||||||
|
limits[signal]?.config?.second?.size,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="signal" key={signal}>
|
<div className="signal" key={signal}>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="signal-name">{signal}</div>
|
<div className="signal-name">{signal}</div>
|
||||||
@ -790,7 +799,7 @@ function MultiIngestionSettings(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="limit-value">
|
<div className="limit-value">
|
||||||
{limits[signal]?.config?.day?.size ? (
|
{hasValidDayLimit ? (
|
||||||
<>
|
<>
|
||||||
{getYAxisFormattedValue(
|
{getYAxisFormattedValue(
|
||||||
(limits[signal]?.metric?.day?.size || 0).toString(),
|
(limits[signal]?.metric?.day?.size || 0).toString(),
|
||||||
@ -816,7 +825,7 @@ function MultiIngestionSettings(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="limit-value">
|
<div className="limit-value">
|
||||||
{limits[signal]?.config?.second?.size ? (
|
{hasValidSecondLimit ? (
|
||||||
<>
|
<>
|
||||||
{getYAxisFormattedValue(
|
{getYAxisFormattedValue(
|
||||||
(limits[signal]?.metric?.second?.size || 0).toString(),
|
(limits[signal]?.metric?.second?.size || 0).toString(),
|
||||||
@ -839,7 +848,8 @@ function MultiIngestionSettings(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -875,10 +885,35 @@ function MultiIngestionSettings(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div className="ingestion-key-container">
|
<div className="ingestion-key-container">
|
||||||
<div className="ingestion-key-content">
|
<div className="ingestion-key-content">
|
||||||
|
<div className="ingestion-setup-details-links">
|
||||||
|
<Info size={14} />
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Find your ingestion URL and learn more about sending data to SigNoz{' '}
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/ingestion/signoz-cloud/overview/"
|
||||||
|
target="_blank"
|
||||||
|
className="learn-more"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
here <ArrowUpRight size={14} />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<Typography.Title className="title"> Ingestion Keys </Typography.Title>
|
<Typography.Title className="title"> Ingestion Keys </Typography.Title>
|
||||||
<Typography.Text className="subtitle">
|
<Typography.Text className="subtitle">
|
||||||
Create and manage ingestion keys for the SigNoz Cloud
|
Create and manage ingestion keys for the SigNoz Cloud{' '}
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/ingestion/signoz-cloud/keys/"
|
||||||
|
target="_blank"
|
||||||
|
className="learn-more"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
Learn more <ArrowUpRight size={14} />
|
||||||
|
</a>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import { render, screen } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import MultiIngestionSettings from '../MultiIngestionSettings';
|
||||||
|
|
||||||
|
describe('MultiIngestionSettings Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<MultiIngestionSettings />);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders MultiIngestionSettings page without crashing', () => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'Find your ingestion URL and learn more about sending data to SigNoz',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('Ingestion Keys')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText('Create and manage ingestion keys for the SigNoz Cloud'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
const overviewLink = screen.getByRole('link', { name: /here/i });
|
||||||
|
expect(overviewLink).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'https://signoz.io/docs/ingestion/signoz-cloud/overview/',
|
||||||
|
);
|
||||||
|
expect(overviewLink).toHaveAttribute('target', '_blank');
|
||||||
|
expect(overviewLink).toHaveClass('learn-more');
|
||||||
|
expect(overviewLink).toHaveAttribute('rel', 'noreferrer');
|
||||||
|
|
||||||
|
const aboutKeyslink = screen.getByRole('link', { name: /Learn more/i });
|
||||||
|
expect(aboutKeyslink).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'https://signoz.io/docs/ingestion/signoz-cloud/keys/',
|
||||||
|
);
|
||||||
|
expect(aboutKeyslink).toHaveAttribute('target', '_blank');
|
||||||
|
expect(aboutKeyslink).toHaveClass('learn-more');
|
||||||
|
expect(aboutKeyslink).toHaveAttribute('rel', 'noreferrer');
|
||||||
|
});
|
||||||
|
});
|
@ -49,9 +49,9 @@ export const alertActionLogEvent = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
logEvent('Alert: Action', {
|
logEvent('Alert: Action', {
|
||||||
ruleId: record.id,
|
ruleId: record?.id,
|
||||||
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
|
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
|
||||||
name: record.alert,
|
name: record?.alert,
|
||||||
action: actionValue,
|
action: actionValue,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -73,7 +73,6 @@ import { Dashboard } from 'types/api/dashboard/getAll';
|
|||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
import useUrlQuery from '../../hooks/useUrlQuery';
|
|
||||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||||
import ImportJSON from './ImportJSON';
|
import ImportJSON from './ImportJSON';
|
||||||
import { DeleteButton } from './TableComponents/DeleteButton';
|
import { DeleteButton } from './TableComponents/DeleteButton';
|
||||||
@ -86,7 +85,7 @@ import {
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function DashboardsList(): JSX.Element {
|
function DashboardsList(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
data: dashboardListResponse = [],
|
data: dashboardListResponse,
|
||||||
isLoading: isDashboardListLoading,
|
isLoading: isDashboardListLoading,
|
||||||
error: dashboardFetchError,
|
error: dashboardFetchError,
|
||||||
refetch: refetchDashboardList,
|
refetch: refetchDashboardList,
|
||||||
@ -99,12 +98,14 @@ function DashboardsList(): JSX.Element {
|
|||||||
setListSortOrder: setSortOrder,
|
setListSortOrder: setSortOrder,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
|
const [searchString, setSearchString] = useState<string>(
|
||||||
|
sortOrder.search || '',
|
||||||
|
);
|
||||||
const [action, createNewDashboard] = useComponentPermission(
|
const [action, createNewDashboard] = useComponentPermission(
|
||||||
['action', 'create_new_dashboards'],
|
['action', 'create_new_dashboards'],
|
||||||
role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState<string>('');
|
|
||||||
const [
|
const [
|
||||||
showNewDashboardTemplatesModal,
|
showNewDashboardTemplatesModal,
|
||||||
setShowNewDashboardTemplatesModal,
|
setShowNewDashboardTemplatesModal,
|
||||||
@ -123,10 +124,6 @@ function DashboardsList(): JSX.Element {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const params = useUrlQuery();
|
|
||||||
const searchParams = params.get('search');
|
|
||||||
const [searchString, setSearchString] = useState<string>(searchParams || '');
|
|
||||||
|
|
||||||
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
|
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
|
||||||
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
|
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
|
||||||
let dashboardDynamicColumns: DashboardDynamicColumns = {
|
let dashboardDynamicColumns: DashboardDynamicColumns = {
|
||||||
@ -188,14 +185,6 @@ function DashboardsList(): JSX.Element {
|
|||||||
setDashboards(sortedDashboards);
|
setDashboards(sortedDashboards);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
params.set('columnKey', sortOrder.columnKey as string);
|
|
||||||
params.set('order', sortOrder.order as string);
|
|
||||||
params.set('page', sortOrder.pagination || '1');
|
|
||||||
history.replace({ search: params.toString() });
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [sortOrder]);
|
|
||||||
|
|
||||||
const sortHandle = (key: string): void => {
|
const sortHandle = (key: string): void => {
|
||||||
if (!dashboards) return;
|
if (!dashboards) return;
|
||||||
if (key === 'createdAt') {
|
if (key === 'createdAt') {
|
||||||
@ -204,6 +193,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
columnKey: 'createdAt',
|
columnKey: 'createdAt',
|
||||||
order: 'descend',
|
order: 'descend',
|
||||||
pagination: sortOrder.pagination || '1',
|
pagination: sortOrder.pagination || '1',
|
||||||
|
search: sortOrder.search || '',
|
||||||
});
|
});
|
||||||
} else if (key === 'updatedAt') {
|
} else if (key === 'updatedAt') {
|
||||||
sortDashboardsByUpdatedAt(dashboards);
|
sortDashboardsByUpdatedAt(dashboards);
|
||||||
@ -211,21 +201,19 @@ function DashboardsList(): JSX.Element {
|
|||||||
columnKey: 'updatedAt',
|
columnKey: 'updatedAt',
|
||||||
order: 'descend',
|
order: 'descend',
|
||||||
pagination: sortOrder.pagination || '1',
|
pagination: sortOrder.pagination || '1',
|
||||||
|
search: sortOrder.search || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function handlePageSizeUpdate(page: number): void {
|
function handlePageSizeUpdate(page: number): void {
|
||||||
setSortOrder((order) => ({
|
setSortOrder({ ...sortOrder, pagination: String(page) });
|
||||||
...order,
|
|
||||||
pagination: String(page),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filteredDashboards = filterDashboard(
|
const filteredDashboards = filterDashboard(
|
||||||
searchString,
|
searchString,
|
||||||
dashboardListResponse,
|
dashboardListResponse || [],
|
||||||
);
|
);
|
||||||
if (sortOrder.columnKey === 'updatedAt') {
|
if (sortOrder.columnKey === 'updatedAt') {
|
||||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||||
@ -236,6 +224,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
columnKey: 'updatedAt',
|
columnKey: 'updatedAt',
|
||||||
order: 'descend',
|
order: 'descend',
|
||||||
pagination: sortOrder.pagination || '1',
|
pagination: sortOrder.pagination || '1',
|
||||||
|
search: sortOrder.search || '',
|
||||||
});
|
});
|
||||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||||
}
|
}
|
||||||
@ -245,6 +234,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
setSortOrder,
|
setSortOrder,
|
||||||
sortOrder.columnKey,
|
sortOrder.columnKey,
|
||||||
sortOrder.pagination,
|
sortOrder.pagination,
|
||||||
|
sortOrder.search,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [newDashboardState, setNewDashboardState] = useState({
|
const [newDashboardState, setNewDashboardState] = useState({
|
||||||
@ -316,12 +306,15 @@ function DashboardsList(): JSX.Element {
|
|||||||
|
|
||||||
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
|
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
setIsFilteringDashboards(true);
|
setIsFilteringDashboards(true);
|
||||||
setSearchValue(event.target.value);
|
|
||||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||||
const filteredDashboards = filterDashboard(searchText, dashboardListResponse);
|
const filteredDashboards = filterDashboard(
|
||||||
|
searchText,
|
||||||
|
dashboardListResponse || [],
|
||||||
|
);
|
||||||
setDashboards(filteredDashboards);
|
setDashboards(filteredDashboards);
|
||||||
setIsFilteringDashboards(false);
|
setIsFilteringDashboards(false);
|
||||||
setSearchString(searchText);
|
setSearchString(searchText);
|
||||||
|
setSortOrder({ ...sortOrder, search: searchText });
|
||||||
};
|
};
|
||||||
|
|
||||||
const [state, setCopy] = useCopyToClipboard();
|
const [state, setCopy] = useCopyToClipboard();
|
||||||
@ -412,7 +405,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
{
|
{
|
||||||
title: 'Dashboards',
|
title: 'Dashboards',
|
||||||
key: 'dashboard',
|
key: 'dashboard',
|
||||||
render: (dashboard: Data): JSX.Element => {
|
render: (dashboard: Data, _, index): JSX.Element => {
|
||||||
const timeOptions: Intl.DateTimeFormatOptions = {
|
const timeOptions: Intl.DateTimeFormatOptions = {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
@ -461,7 +454,9 @@ function DashboardsList(): JSX.Element {
|
|||||||
style={{ height: '14px', width: '14px' }}
|
style={{ height: '14px', width: '14px' }}
|
||||||
alt="dashboard-image"
|
alt="dashboard-image"
|
||||||
/>
|
/>
|
||||||
<Typography.Text>{dashboard.name}</Typography.Text>
|
<Typography.Text data-testid={`dashboard-title-${index}`}>
|
||||||
|
{dashboard.name}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tags-with-actions">
|
<div className="tags-with-actions">
|
||||||
@ -658,8 +653,9 @@ function DashboardsList(): JSX.Element {
|
|||||||
}}
|
}}
|
||||||
eventName="Dashboard: Facing Issues in dashboard"
|
eventName="Dashboard: Facing Issues in dashboard"
|
||||||
message={dashboardListMessage}
|
message={dashboardListMessage}
|
||||||
buttonText="Facing issues with dashboards?"
|
buttonText="Need help with dashboards?"
|
||||||
onHoverText="Click here to get help with dashboards"
|
onHoverText="Click here to get help with dashboards"
|
||||||
|
intercomMessageDisabled
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
@ -701,7 +697,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
<ArrowUpRight size={16} className="learn-more-arrow" />
|
<ArrowUpRight size={16} className="learn-more-arrow" />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
) : dashboards?.length === 0 && !searchValue ? (
|
) : dashboards?.length === 0 && !searchString ? (
|
||||||
<div className="dashboard-empty-state">
|
<div className="dashboard-empty-state">
|
||||||
<img
|
<img
|
||||||
src="/Icons/dashboards.svg"
|
src="/Icons/dashboards.svg"
|
||||||
@ -739,6 +735,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
className="learn-more"
|
className="learn-more"
|
||||||
|
data-testid="learn-more"
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
window.open(
|
window.open(
|
||||||
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
|
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
|
||||||
@ -758,7 +755,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search by name, description, or tags..."
|
placeholder="Search by name, description, or tags..."
|
||||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||||
value={searchValue}
|
value={searchString}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
{createNewDashboard && (
|
{createNewDashboard && (
|
||||||
@ -786,7 +783,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
<div className="no-search">
|
<div className="no-search">
|
||||||
<img src="/Icons/emptyState.svg" alt="img" className="img" />
|
<img src="/Icons/emptyState.svg" alt="img" className="img" />
|
||||||
<Typography.Text className="text">
|
<Typography.Text className="text">
|
||||||
No dashboards found for {searchValue}. Create a new dashboard?
|
No dashboards found for {searchString}. Create a new dashboard?
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -808,6 +805,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
type="text"
|
type="text"
|
||||||
className={cx('sort-btns')}
|
className={cx('sort-btns')}
|
||||||
onClick={(): void => sortHandle('createdAt')}
|
onClick={(): void => sortHandle('createdAt')}
|
||||||
|
data-testid="sort-by-last-created"
|
||||||
>
|
>
|
||||||
Last created
|
Last created
|
||||||
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
|
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
|
||||||
@ -816,6 +814,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
type="text"
|
type="text"
|
||||||
className={cx('sort-btns')}
|
className={cx('sort-btns')}
|
||||||
onClick={(): void => sortHandle('updatedAt')}
|
onClick={(): void => sortHandle('updatedAt')}
|
||||||
|
data-testid="sort-by-last-updated"
|
||||||
>
|
>
|
||||||
Last updated
|
Last updated
|
||||||
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
|
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
|
||||||
@ -826,7 +825,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
arrow={false}
|
arrow={false}
|
||||||
>
|
>
|
||||||
<ArrowDownWideNarrow size={14} />
|
<ArrowDownWideNarrow size={14} data-testid="sort-by" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover
|
<Popover
|
||||||
|
@ -108,7 +108,7 @@ function DBCall(): JSX.Element {
|
|||||||
|
|
||||||
logEvent('APM: Service detail page visited', {
|
logEvent('APM: Service detail page visited', {
|
||||||
selectedEnvironments,
|
selectedEnvironments,
|
||||||
resourceAttributeUsed: !!queries.length,
|
resourceAttributeUsed: !!queries?.length,
|
||||||
section: 'dbMetrics',
|
section: 'dbMetrics',
|
||||||
});
|
});
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
|
@ -124,7 +124,7 @@ function External(): JSX.Element {
|
|||||||
|
|
||||||
logEvent('APM: Service detail page visited', {
|
logEvent('APM: Service detail page visited', {
|
||||||
selectedEnvironments,
|
selectedEnvironments,
|
||||||
resourceAttributeUsed: !!queries.length,
|
resourceAttributeUsed: !!queries?.length,
|
||||||
section: 'externalMetrics',
|
section: 'externalMetrics',
|
||||||
});
|
});
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
|
@ -91,7 +91,7 @@ function Application(): JSX.Element {
|
|||||||
|
|
||||||
logEvent('APM: Service detail page visited', {
|
logEvent('APM: Service detail page visited', {
|
||||||
selectedEnvironments,
|
selectedEnvironments,
|
||||||
resourceAttributeUsed: !!queries.length,
|
resourceAttributeUsed: !!queries?.length,
|
||||||
section: 'overview',
|
section: 'overview',
|
||||||
});
|
});
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
import { getNonIntegrationDashboardById } from 'mocks-server/__mockdata__/dashboards';
|
||||||
|
import { server } from 'mocks-server/server';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||||
|
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
import DashboardDescription from '..';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: jest.fn(),
|
||||||
|
useRouteMatch: jest.fn().mockReturnValue({
|
||||||
|
params: {
|
||||||
|
dashboardId: 4,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'container/TopNav/DateTimeSelectionV2/index.tsx',
|
||||||
|
() =>
|
||||||
|
function MockDateTimeSelection(): JSX.Element {
|
||||||
|
return <div>MockDateTimeSelection</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Dashboard landing page actions header tests', () => {
|
||||||
|
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
|
||||||
|
const mockLocation = {
|
||||||
|
pathname: `${process.env.FRONTEND_API_ENDPOINT}/dashboard/4`,
|
||||||
|
search: '',
|
||||||
|
};
|
||||||
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<MemoryRouter initialEntries={['/dashboard/4']}>
|
||||||
|
<DashboardProvider>
|
||||||
|
<DashboardDescription
|
||||||
|
handle={{
|
||||||
|
active: false,
|
||||||
|
enter: (): Promise<void> => Promise.resolve(),
|
||||||
|
exit: (): Promise<void> => Promise.resolve(),
|
||||||
|
node: { current: null },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DashboardProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getByTestId('dashboard-title')).toHaveTextContent('thor'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dashboardSettingsTrigger = getByTestId('options');
|
||||||
|
|
||||||
|
await fireEvent.click(dashboardSettingsTrigger);
|
||||||
|
|
||||||
|
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
|
||||||
|
|
||||||
|
await waitFor(() => expect(lockUnlockButton).toBeDisabled());
|
||||||
|
});
|
||||||
|
it('unlock dashboard should not be disabled for non integration created dashboards', async () => {
|
||||||
|
const mockLocation = {
|
||||||
|
pathname: `${process.env.FRONTEND_API_ENDPOINT}/dashboard/4`,
|
||||||
|
search: '',
|
||||||
|
};
|
||||||
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
server.use(
|
||||||
|
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(getNonIntegrationDashboardById)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<MemoryRouter initialEntries={['/dashboard/4']}>
|
||||||
|
<DashboardProvider>
|
||||||
|
<DashboardDescription
|
||||||
|
handle={{
|
||||||
|
active: false,
|
||||||
|
enter: (): Promise<void> => Promise.resolve(),
|
||||||
|
exit: (): Promise<void> => Promise.resolve(),
|
||||||
|
node: { current: null },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DashboardProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getByTestId('dashboard-title')).toHaveTextContent('thor'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dashboardSettingsTrigger = getByTestId('options');
|
||||||
|
|
||||||
|
await fireEvent.click(dashboardSettingsTrigger);
|
||||||
|
|
||||||
|
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
|
||||||
|
|
||||||
|
await waitFor(() => expect(lockUnlockButton).not.toBeDisabled());
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,16 @@
|
|||||||
import './Description.styles.scss';
|
import './Description.styles.scss';
|
||||||
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Popover,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
|
||||||
@ -266,6 +275,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
urlQuery.set('columnKey', listSortOrder.columnKey as string);
|
urlQuery.set('columnKey', listSortOrder.columnKey as string);
|
||||||
urlQuery.set('order', listSortOrder.order as string);
|
urlQuery.set('order', listSortOrder.order as string);
|
||||||
urlQuery.set('page', listSortOrder.pagination as string);
|
urlQuery.set('page', listSortOrder.pagination as string);
|
||||||
|
urlQuery.set('search', listSortOrder.search as string);
|
||||||
urlQuery.delete(QueryParams.relativeTime);
|
urlQuery.delete(QueryParams.relativeTime);
|
||||||
|
|
||||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
|
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
|
||||||
@ -299,17 +309,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
<FacingIssueBtn
|
|
||||||
attributes={{
|
|
||||||
uuid: selectedDashboard?.uuid,
|
|
||||||
title: updatedTitle,
|
|
||||||
screen: 'Dashboard Details',
|
|
||||||
}}
|
|
||||||
eventName="Dashboard: Facing Issues in dashboard"
|
|
||||||
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
|
|
||||||
buttonText="Facing issues with dashboards?"
|
|
||||||
onHoverText="Click here to get help with dashboard details"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<section className="dashbord-details">
|
<section className="dashbord-details">
|
||||||
<div className="left-section">
|
<div className="left-section">
|
||||||
@ -318,10 +317,24 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
alt="dashboard-img"
|
alt="dashboard-img"
|
||||||
style={{ width: '16px', height: '16px' }}
|
style={{ width: '16px', height: '16px' }}
|
||||||
/>
|
/>
|
||||||
<Typography.Text className="dashboard-title">{title}</Typography.Text>
|
<Typography.Text className="dashboard-title" data-testid="dashboard-title">
|
||||||
|
{title}
|
||||||
|
</Typography.Text>
|
||||||
{isDashboardLocked && <LockKeyhole size={14} />}
|
{isDashboardLocked && <LockKeyhole size={14} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="right-section">
|
<div className="right-section">
|
||||||
|
<FacingIssueBtn
|
||||||
|
attributes={{
|
||||||
|
uuid: selectedDashboard?.uuid,
|
||||||
|
title: updatedTitle,
|
||||||
|
screen: 'Dashboard Details',
|
||||||
|
}}
|
||||||
|
eventName="Dashboard: Facing Issues in dashboard"
|
||||||
|
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
|
||||||
|
buttonText="Need help with this dashboard?"
|
||||||
|
onHoverText="Click here to get help with dashboard"
|
||||||
|
intercomMessageDisabled
|
||||||
|
/>
|
||||||
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
|
||||||
<Popover
|
<Popover
|
||||||
open={isDashboardSettingsOpen}
|
open={isDashboardSettingsOpen}
|
||||||
@ -332,13 +345,22 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
<div className="menu-content">
|
<div className="menu-content">
|
||||||
<section className="section-1">
|
<section className="section-1">
|
||||||
{(isAuthor || role === USER_ROLES.ADMIN) && (
|
{(isAuthor || role === USER_ROLES.ADMIN) && (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
selectedDashboard?.created_by === 'integration' &&
|
||||||
|
'Dashboards created by integrations cannot be unlocked'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<LockKeyhole size={14} />}
|
icon={<LockKeyhole size={14} />}
|
||||||
|
disabled={selectedDashboard?.created_by === 'integration'}
|
||||||
onClick={handleLockDashboardToggle}
|
onClick={handleLockDashboardToggle}
|
||||||
|
data-testid="lock-unlock-dashboard"
|
||||||
>
|
>
|
||||||
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isDashboardLocked && editDashboard && (
|
{!isDashboardLocked && editDashboard && (
|
||||||
|
@ -309,6 +309,7 @@ function ExplorerColumnsRenderer({
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="action-btn"
|
className="action-btn"
|
||||||
|
data-testid="add-columns-button"
|
||||||
icon={
|
icon={
|
||||||
<PlusCircle
|
<PlusCircle
|
||||||
size={16}
|
size={16}
|
||||||
|
@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
|
|||||||
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
|
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||||
@ -236,6 +237,21 @@ function QuerySection({
|
|||||||
onChange={handleQueryCategoryChange}
|
onChange={handleQueryCategoryChange}
|
||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
|
<FacingIssueBtn
|
||||||
|
attributes={{
|
||||||
|
uuid: selectedDashboard?.uuid,
|
||||||
|
title: selectedDashboard?.data.title,
|
||||||
|
screen: 'Dashboard widget',
|
||||||
|
panelType: selectedGraph,
|
||||||
|
widgetId: query.id,
|
||||||
|
queryType: currentQuery.queryType,
|
||||||
|
}}
|
||||||
|
eventName="Dashboard: Facing Issues in dashboard"
|
||||||
|
buttonText="Need help with this chart?"
|
||||||
|
// message={chartHelpMessage(selectedDashboard, graphType)}
|
||||||
|
onHoverText="Click here to get help with this dashboard widget"
|
||||||
|
intercomMessageDisabled
|
||||||
|
/>
|
||||||
<TextToolTip
|
<TextToolTip
|
||||||
text="This will temporarily save the current query and graph state. This will persist across tab change"
|
text="This will temporarily save the current query and graph state. This will persist across tab change"
|
||||||
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"
|
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"
|
||||||
|
@ -4,8 +4,6 @@ import './NewWidget.styles.scss';
|
|||||||
import { WarningOutlined } from '@ant-design/icons';
|
import { WarningOutlined } from '@ant-design/icons';
|
||||||
import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd';
|
import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
|
|
||||||
import { chartHelpMessage } from 'components/facingIssueBtn/util';
|
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
@ -79,6 +77,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
stagedQuery,
|
stagedQuery,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
supersetQuery,
|
supersetQuery,
|
||||||
|
setSupersetQuery,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
const isQueryModified = useMemo(
|
const isQueryModified = useMemo(
|
||||||
@ -548,6 +547,17 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
isNewTraceLogsAvailable,
|
isNewTraceLogsAvailable,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* we need this extra handling for superset query because we cannot keep this in sync with current query
|
||||||
|
* always.we do not sync superset query in the initQueryBuilderData because that function is called on all stage and run
|
||||||
|
* actions. we do not want that as we loose out on superset functionalities if we do the same. hence initialising the superset query
|
||||||
|
* on mount here with the currentQuery in the begining itself
|
||||||
|
*/
|
||||||
|
setSupersetQuery(currentQuery);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerShortcut(DashboardShortcuts.SaveChanges, onSaveDashboard);
|
registerShortcut(DashboardShortcuts.SaveChanges, onSaveDashboard);
|
||||||
registerShortcut(DashboardShortcuts.DiscardChanges, onClickDiscardHandler);
|
registerShortcut(DashboardShortcuts.DiscardChanges, onClickDiscardHandler);
|
||||||
@ -563,11 +573,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
if (selectedGraph === PANEL_TYPES.LIST) {
|
if (selectedGraph === PANEL_TYPES.LIST) {
|
||||||
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
||||||
if (initialDataSource === DataSource.LOGS) {
|
if (initialDataSource === DataSource.LOGS) {
|
||||||
|
// we do not need selected log columns in the request data as the entire response contains all the necessary data
|
||||||
setRequestData((prev) => ({
|
setRequestData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
tableParams: {
|
tableParams: {
|
||||||
...prev.tableParams,
|
...prev.tableParams,
|
||||||
selectColumns: selectedLogFields,
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else if (initialDataSource === DataSource.TRACES) {
|
} else if (initialDataSource === DataSource.TRACES) {
|
||||||
@ -596,20 +606,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
<Typography.Text className="configure-panel">
|
<Typography.Text className="configure-panel">
|
||||||
Configure panel
|
Configure panel
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<FacingIssueBtn
|
|
||||||
attributes={{
|
|
||||||
uuid: selectedDashboard?.uuid,
|
|
||||||
title: selectedDashboard?.data.title,
|
|
||||||
screen: 'Dashboard widget',
|
|
||||||
panelType: graphType,
|
|
||||||
widgetId: query.get('widgetId'),
|
|
||||||
queryType: currentQuery.queryType,
|
|
||||||
}}
|
|
||||||
eventName="Dashboard: Facing Issues in dashboard"
|
|
||||||
message={chartHelpMessage(selectedDashboard, graphType)}
|
|
||||||
buttonText="Facing issues with dashboards?"
|
|
||||||
onHoverText="Click here to get help with dashboard widget"
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
{isSaveDisabled && (
|
{isSaveDisabled && (
|
||||||
|
@ -11,7 +11,6 @@ import ROUTES from 'constants/routes';
|
|||||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||||
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
|
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
|
||||||
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
|
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { UserPlus } from 'lucide-react';
|
import { UserPlus } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
@ -104,7 +103,6 @@ export default function Onboarding(): JSX.Element {
|
|||||||
const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS);
|
const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS);
|
||||||
const [activeStep, setActiveStep] = useState(1);
|
const [activeStep, setActiveStep] = useState(1);
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
const { t } = useTranslation(['onboarding']);
|
const { t } = useTranslation(['onboarding']);
|
||||||
|
|
||||||
@ -120,7 +118,7 @@ export default function Onboarding(): JSX.Element {
|
|||||||
} = useOnboardingContext();
|
} = useOnboardingContext();
|
||||||
|
|
||||||
useEffectOnce(() => {
|
useEffectOnce(() => {
|
||||||
trackEvent('Onboarding V2 Started');
|
logEvent('Onboarding V2 Started', {});
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status, data: ingestionData } = useQuery({
|
const { status, data: ingestionData } = useQuery({
|
||||||
@ -231,7 +229,7 @@ export default function Onboarding(): JSX.Element {
|
|||||||
const nextStep = activeStep + 1;
|
const nextStep = activeStep + 1;
|
||||||
|
|
||||||
// on next
|
// on next
|
||||||
trackEvent('Onboarding V2: Get Started', {
|
logEvent('Onboarding V2: Get Started', {
|
||||||
selectedModule: selectedModule.id,
|
selectedModule: selectedModule.id,
|
||||||
nextStepId: nextStep,
|
nextStepId: nextStep,
|
||||||
});
|
});
|
||||||
|
@ -5,9 +5,9 @@ import {
|
|||||||
CloseCircleTwoTone,
|
CloseCircleTwoTone,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import Header from 'container/OnboardingContainer/common/Header/Header';
|
import Header from 'container/OnboardingContainer/common/Header/Header';
|
||||||
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import { useQueryService } from 'hooks/useQueryService';
|
import { useQueryService } from 'hooks/useQueryService';
|
||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||||
@ -41,8 +41,6 @@ export default function ConnectionStatus(): JSX.Element {
|
|||||||
[queries],
|
[queries],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const [retryCount, setRetryCount] = useState(20); // Retry for 3 mins 20s
|
const [retryCount, setRetryCount] = useState(20); // Retry for 3 mins 20s
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isReceivingData, setIsReceivingData] = useState(false);
|
const [isReceivingData, setIsReceivingData] = useState(false);
|
||||||
@ -155,7 +153,7 @@ export default function ConnectionStatus(): JSX.Element {
|
|||||||
if (data || isError) {
|
if (data || isError) {
|
||||||
setRetryCount(retryCount - 1);
|
setRetryCount(retryCount - 1);
|
||||||
if (retryCount < 0) {
|
if (retryCount < 0) {
|
||||||
trackEvent('Onboarding V2: Connection Status', {
|
logEvent('Onboarding V2: Connection Status', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
@ -174,7 +172,7 @@ export default function ConnectionStatus(): JSX.Element {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
setIsReceivingData(true);
|
setIsReceivingData(true);
|
||||||
|
|
||||||
trackEvent('Onboarding V2: Connection Status', {
|
logEvent('Onboarding V2: Connection Status', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
|
@ -5,11 +5,11 @@ import {
|
|||||||
CloseCircleTwoTone,
|
CloseCircleTwoTone,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import Header from 'container/OnboardingContainer/common/Header/Header';
|
import Header from 'container/OnboardingContainer/common/Header/Header';
|
||||||
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { SuccessResponse } from 'types/api';
|
import { SuccessResponse } from 'types/api';
|
||||||
@ -32,7 +32,6 @@ export default function LogsConnectionStatus(): JSX.Element {
|
|||||||
activeStep,
|
activeStep,
|
||||||
selectedEnvironment,
|
selectedEnvironment,
|
||||||
} = useOnboardingContext();
|
} = useOnboardingContext();
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
const [isReceivingData, setIsReceivingData] = useState(false);
|
const [isReceivingData, setIsReceivingData] = useState(false);
|
||||||
const [pollingInterval, setPollingInterval] = useState<number | false>(15000); // initial Polling interval of 15 secs , Set to false after 5 mins
|
const [pollingInterval, setPollingInterval] = useState<number | false>(15000); // initial Polling interval of 15 secs , Set to false after 5 mins
|
||||||
const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins
|
const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins
|
||||||
@ -105,7 +104,7 @@ export default function LogsConnectionStatus(): JSX.Element {
|
|||||||
setRetryCount(retryCount - 1);
|
setRetryCount(retryCount - 1);
|
||||||
|
|
||||||
if (retryCount < 0) {
|
if (retryCount < 0) {
|
||||||
trackEvent('Onboarding V2: Connection Status', {
|
logEvent('Onboarding V2: Connection Status', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
@ -141,7 +140,7 @@ export default function LogsConnectionStatus(): JSX.Element {
|
|||||||
setRetryCount(-1);
|
setRetryCount(-1);
|
||||||
setPollingInterval(false);
|
setPollingInterval(false);
|
||||||
|
|
||||||
trackEvent('Onboarding V2: Connection Status', {
|
logEvent('Onboarding V2: Connection Status', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
|
@ -15,7 +15,6 @@ import ROUTES from 'constants/routes';
|
|||||||
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
|
||||||
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
|
||||||
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
|
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isEmpty, isNull } from 'lodash-es';
|
import { isEmpty, isNull } from 'lodash-es';
|
||||||
import { HelpCircle, UserPlus } from 'lucide-react';
|
import { HelpCircle, UserPlus } from 'lucide-react';
|
||||||
@ -79,7 +78,6 @@ export default function ModuleStepsContainer({
|
|||||||
} = useOnboardingContext();
|
} = useOnboardingContext();
|
||||||
|
|
||||||
const [current, setCurrent] = useState(0);
|
const [current, setCurrent] = useState(0);
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
|
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
|
||||||
const lastStepIndex = selectedModuleSteps.length - 1;
|
const lastStepIndex = selectedModuleSteps.length - 1;
|
||||||
|
|
||||||
@ -143,7 +141,7 @@ export default function ModuleStepsContainer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const redirectToModules = (): void => {
|
const redirectToModules = (): void => {
|
||||||
trackEvent('Onboarding V2 Complete', {
|
logEvent('Onboarding V2 Complete', {
|
||||||
module: selectedModule.id,
|
module: selectedModule.id,
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
@ -186,14 +184,14 @@ export default function ModuleStepsContainer({
|
|||||||
// on next step click track events
|
// on next step click track events
|
||||||
switch (selectedModuleSteps[current].id) {
|
switch (selectedModuleSteps[current].id) {
|
||||||
case stepsMap.dataSource:
|
case stepsMap.dataSource:
|
||||||
trackEvent('Onboarding V2: Data Source Selected', {
|
logEvent('Onboarding V2: Data Source Selected', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.environmentDetails:
|
case stepsMap.environmentDetails:
|
||||||
trackEvent('Onboarding V2: Environment Selected', {
|
logEvent('Onboarding V2: Environment Selected', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
@ -201,7 +199,7 @@ export default function ModuleStepsContainer({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.selectMethod:
|
case stepsMap.selectMethod:
|
||||||
trackEvent('Onboarding V2: Method Selected', {
|
logEvent('Onboarding V2: Method Selected', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
@ -211,7 +209,7 @@ export default function ModuleStepsContainer({
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case stepsMap.setupOtelCollector:
|
case stepsMap.setupOtelCollector:
|
||||||
trackEvent('Onboarding V2: Setup Otel Collector', {
|
logEvent('Onboarding V2: Setup Otel Collector', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
@ -220,7 +218,7 @@ export default function ModuleStepsContainer({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.instrumentApplication:
|
case stepsMap.instrumentApplication:
|
||||||
trackEvent('Onboarding V2: Instrument Application', {
|
logEvent('Onboarding V2: Instrument Application', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
@ -229,13 +227,13 @@ export default function ModuleStepsContainer({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.cloneRepository:
|
case stepsMap.cloneRepository:
|
||||||
trackEvent('Onboarding V2: Clone Repository', {
|
logEvent('Onboarding V2: Clone Repository', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.runApplication:
|
case stepsMap.runApplication:
|
||||||
trackEvent('Onboarding V2: Run Application', {
|
logEvent('Onboarding V2: Run Application', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
framework: selectedFramework,
|
framework: selectedFramework,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
@ -244,95 +242,95 @@ export default function ModuleStepsContainer({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.addHttpDrain:
|
case stepsMap.addHttpDrain:
|
||||||
trackEvent('Onboarding V2: Add HTTP Drain', {
|
logEvent('Onboarding V2: Add HTTP Drain', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.startContainer:
|
case stepsMap.startContainer:
|
||||||
trackEvent('Onboarding V2: Start Container', {
|
logEvent('Onboarding V2: Start Container', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.setupLogDrains:
|
case stepsMap.setupLogDrains:
|
||||||
trackEvent('Onboarding V2: Setup Log Drains', {
|
logEvent('Onboarding V2: Setup Log Drains', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.configureReceiver:
|
case stepsMap.configureReceiver:
|
||||||
trackEvent('Onboarding V2: Configure Receiver', {
|
logEvent('Onboarding V2: Configure Receiver', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.configureAws:
|
case stepsMap.configureAws:
|
||||||
trackEvent('Onboarding V2: Configure AWS', {
|
logEvent('Onboarding V2: Configure AWS', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.sendLogsCloudwatch:
|
case stepsMap.sendLogsCloudwatch:
|
||||||
trackEvent('Onboarding V2: Send Logs Cloudwatch', {
|
logEvent('Onboarding V2: Send Logs Cloudwatch', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.setupDaemonService:
|
case stepsMap.setupDaemonService:
|
||||||
trackEvent('Onboarding V2: Setup ECS Daemon Service', {
|
logEvent('Onboarding V2: Setup ECS Daemon Service', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.createOtelConfig:
|
case stepsMap.createOtelConfig:
|
||||||
trackEvent('Onboarding V2: Create ECS OTel Config', {
|
logEvent('Onboarding V2: Create ECS OTel Config', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.createDaemonService:
|
case stepsMap.createDaemonService:
|
||||||
trackEvent('Onboarding V2: Create ECS Daemon Service', {
|
logEvent('Onboarding V2: Create ECS Daemon Service', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.ecsSendData:
|
case stepsMap.ecsSendData:
|
||||||
trackEvent('Onboarding V2: ECS send traces data', {
|
logEvent('Onboarding V2: ECS send traces data', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.createSidecarCollectorContainer:
|
case stepsMap.createSidecarCollectorContainer:
|
||||||
trackEvent('Onboarding V2: ECS create Sidecar Container', {
|
logEvent('Onboarding V2: ECS create Sidecar Container', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.deployTaskDefinition:
|
case stepsMap.deployTaskDefinition:
|
||||||
trackEvent('Onboarding V2: ECS deploy task definition', {
|
logEvent('Onboarding V2: ECS deploy task definition', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.ecsSendLogsData:
|
case stepsMap.ecsSendLogsData:
|
||||||
trackEvent('Onboarding V2: ECS Fargate send logs data', {
|
logEvent('Onboarding V2: ECS Fargate send logs data', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case stepsMap.monitorDashboard:
|
case stepsMap.monitorDashboard:
|
||||||
trackEvent('Onboarding V2: EKS monitor dashboard', {
|
logEvent('Onboarding V2: EKS monitor dashboard', {
|
||||||
dataSource: selectedDataSource?.id,
|
dataSource: selectedDataSource?.id,
|
||||||
environment: selectedEnvironment,
|
environment: selectedEnvironment,
|
||||||
module: activeStep?.module?.id,
|
module: activeStep?.module?.id,
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { render } from 'tests/test-utils';
|
||||||
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import TablePanelWrapper from '../TablePanelWrapper';
|
||||||
|
import {
|
||||||
|
tablePanelQueryResponse,
|
||||||
|
tablePanelWidgetQuery,
|
||||||
|
} from './tablePanelWrapperHelper';
|
||||||
|
|
||||||
|
describe('Table panel wrappper tests', () => {
|
||||||
|
it('table should render fine with the query response and column units', () => {
|
||||||
|
const { container, getByText } = render(
|
||||||
|
<TablePanelWrapper
|
||||||
|
widget={(tablePanelWidgetQuery as unknown) as Widgets}
|
||||||
|
queryResponse={(tablePanelQueryResponse as unknown) as any}
|
||||||
|
onDragSelect={(): void => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// checking the overall rendering of the table
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
|
||||||
|
// the first row of the table should have the latency value with units
|
||||||
|
expect(getByText('4.35 s')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// the rows should have optimised value for human readability
|
||||||
|
expect(getByText('31.3 ms')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// the applied legend should appear as the column header
|
||||||
|
expect(getByText('latency-per-service')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,38 @@
|
|||||||
|
import { render } from 'tests/test-utils';
|
||||||
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import ValuePanelWrapper from '../ValuePanelWrapper';
|
||||||
|
import {
|
||||||
|
thresholds,
|
||||||
|
valuePanelQueryResponse,
|
||||||
|
valuePanelWidget,
|
||||||
|
} from './valuePanelWrapperHelper';
|
||||||
|
|
||||||
|
describe('Value panel wrappper tests', () => {
|
||||||
|
it('should render value panel correctly with yaxis unit', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ValuePanelWrapper
|
||||||
|
widget={(valuePanelWidget as unknown) as Widgets}
|
||||||
|
queryResponse={(valuePanelQueryResponse as unknown) as any}
|
||||||
|
onDragSelect={(): void => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// selected y axis unit as miliseconds (ms)
|
||||||
|
expect(getByText('295 ms')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render tooltip when there are conflicting thresholds', () => {
|
||||||
|
const { getByTestId, container } = render(
|
||||||
|
<ValuePanelWrapper
|
||||||
|
widget={({ ...valuePanelWidget, thresholds } as unknown) as Widgets}
|
||||||
|
queryResponse={(valuePanelQueryResponse as unknown) as any}
|
||||||
|
onDragSelect={(): void => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByTestId('conflicting-thresholds')).toBeInTheDocument();
|
||||||
|
// added snapshot test here for checking the thresholds color being applied properly
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,389 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Table panel wrappper tests table should render fine with the query response and column units 1`] = `
|
||||||
|
.c1 {
|
||||||
|
position: absolute;
|
||||||
|
right: -0.313rem;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 100%;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 {
|
||||||
|
height: 95%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 .ant-table-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 .ant-spin-nested-loading {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 .ant-spin-container {
|
||||||
|
height: 100%;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 .ant-table {
|
||||||
|
-webkit-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 .ant-table > .ant-table-container > .ant-table-content > table {
|
||||||
|
min-width: 99% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="c0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="query-table"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-spin-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-table ant-table-small ant-table-layout-fixed ant-table-scroll-horizontal"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-table-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-table-content"
|
||||||
|
style="overflow-x: auto; overflow-y: hidden;"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
style="width: auto; min-width: 100%; table-layout: fixed;"
|
||||||
|
>
|
||||||
|
<colgroup>
|
||||||
|
<col
|
||||||
|
style="width: 145px;"
|
||||||
|
/>
|
||||||
|
<col
|
||||||
|
style="width: 145px;"
|
||||||
|
/>
|
||||||
|
</colgroup>
|
||||||
|
<thead
|
||||||
|
class="ant-table-thead"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
aria-label="service_name"
|
||||||
|
class="ant-table-cell ant-table-column-has-sorters react-resizable"
|
||||||
|
scope="col"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-table-column-sorters"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="ant-table-column-title"
|
||||||
|
>
|
||||||
|
service_name
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="ant-table-column-sorter ant-table-column-sorter-full"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="ant-table-column-sorter-inner"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="caret-up"
|
||||||
|
class="anticon anticon-caret-up ant-table-column-sorter-up"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="caret-up"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
aria-label="caret-down"
|
||||||
|
class="anticon anticon-caret-down ant-table-column-sorter-down"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="caret-down"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="c1 react-resizable-handle"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
aria-label="latency-per-service"
|
||||||
|
class="ant-table-cell ant-table-column-has-sorters react-resizable"
|
||||||
|
scope="col"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ant-table-column-sorters"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="ant-table-column-title"
|
||||||
|
>
|
||||||
|
latency-per-service
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="ant-table-column-sorter ant-table-column-sorter-full"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="ant-table-column-sorter-inner"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="caret-up"
|
||||||
|
class="anticon anticon-caret-up ant-table-column-sorter-up"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="caret-up"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
aria-label="caret-down"
|
||||||
|
class="anticon anticon-caret-down ant-table-column-sorter-down"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="caret-down"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="c1 react-resizable-handle"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
class="ant-table-tbody"
|
||||||
|
>
|
||||||
|
<tr
|
||||||
|
aria-hidden="true"
|
||||||
|
class="ant-table-measure-row"
|
||||||
|
style="height: 0px; font-size: 0px;"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
style="padding: 0px; border: 0px; height: 0px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="height: 0px; overflow: hidden;"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style="padding: 0px; border: 0px; height: 0px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="height: 0px; overflow: hidden;"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="ant-table-row ant-table-row-level-0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
demo-app
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
4.35 s
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="ant-table-row ant-table-row-level-0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
customer
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
431 ms
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="ant-table-row ant-table-row-level-0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
mysql
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
431 ms
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="ant-table-row ant-table-row-level-0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
frontend
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
287 ms
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="ant-table-row ant-table-row-level-0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
driver
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
230 ms
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="ant-table-row ant-table-row-level-0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
route
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
66.4 ms
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="ant-table-row ant-table-row-level-0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
redis
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="ant-table-cell"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
31.3 ms
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,76 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Value panel wrappper tests should render tooltip when there are conflicting thresholds 1`] = `
|
||||||
|
.c1 {
|
||||||
|
height: 100%;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-pack: center;
|
||||||
|
-webkit-justify-content: center;
|
||||||
|
-ms-flex-pack: center;
|
||||||
|
justify-content: center;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c0 {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="c0"
|
||||||
|
>
|
||||||
|
<article
|
||||||
|
class="ant-typography css-dev-only-do-not-override-2i2tap"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="value-graph-container"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="ant-typography value-graph-text css-dev-only-do-not-override-2i2tap"
|
||||||
|
style="color: Blue;"
|
||||||
|
>
|
||||||
|
295 ms
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="value-graph-textconflict"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-label="exclamation-circle"
|
||||||
|
class="anticon anticon-exclamation-circle value-graph-icon"
|
||||||
|
data-testid="conflicting-thresholds"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
data-icon="exclamation-circle"
|
||||||
|
fill="currentColor"
|
||||||
|
focusable="false"
|
||||||
|
height="1em"
|
||||||
|
viewBox="64 64 896 896"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,286 @@
|
|||||||
|
export const tablePanelWidgetQuery = {
|
||||||
|
id: '727533b0-7718-4f99-a1db-a1875649325c',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
isStacked: false,
|
||||||
|
nullZeroValues: 'zero',
|
||||||
|
opacity: '1',
|
||||||
|
panelTypes: 'table',
|
||||||
|
query: {
|
||||||
|
clickhouse_sql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'signoz_latency',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'ExponentialHistogram',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||||
|
},
|
||||||
|
timeAggregation: '',
|
||||||
|
spaceAggregation: 'p90',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [
|
||||||
|
{
|
||||||
|
key: 'service_name',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'service_name--string--tag--false',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
legend: 'latency-per-service',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
id: '7feafec2-a450-4b5a-8897-260c1a9fe1e4',
|
||||||
|
queryType: 'builder',
|
||||||
|
},
|
||||||
|
timePreferance: 'GLOBAL_TIME',
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
selectedLogFields: [
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
type: '',
|
||||||
|
name: 'body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
type: '',
|
||||||
|
name: 'timestamp',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedTracesFields: [
|
||||||
|
{
|
||||||
|
key: 'serviceName',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'serviceName--string--tag--true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'name--string--tag--true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'durationNano',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'durationNano--float64--tag--true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'httpMethod',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'httpMethod--string--tag--true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'responseStatusCode',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'responseStatusCode--string--tag--true',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxisUnit: 'none',
|
||||||
|
thresholds: [],
|
||||||
|
fillSpans: false,
|
||||||
|
columnUnits: {
|
||||||
|
A: 'ms',
|
||||||
|
},
|
||||||
|
bucketCount: 30,
|
||||||
|
stackedBarChart: false,
|
||||||
|
bucketWidth: 0,
|
||||||
|
mergeAllActiveQueries: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tablePanelQueryResponse = {
|
||||||
|
status: 'success',
|
||||||
|
isLoading: false,
|
||||||
|
isSuccess: true,
|
||||||
|
isError: false,
|
||||||
|
isIdle: false,
|
||||||
|
data: {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: 'success',
|
||||||
|
payload: {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
resultType: '',
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'service_name',
|
||||||
|
queryName: '',
|
||||||
|
isValueColumn: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
queryName: 'A',
|
||||||
|
isValueColumn: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 4353.81,
|
||||||
|
service_name: 'demo-app',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 431.25,
|
||||||
|
service_name: 'customer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 431.25,
|
||||||
|
service_name: 'mysql',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 287.11,
|
||||||
|
service_name: 'frontend',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 230.02,
|
||||||
|
service_name: 'driver',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 66.37,
|
||||||
|
service_name: 'route',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
A: 31.3,
|
||||||
|
service_name: 'redis',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
start: 1721207225000,
|
||||||
|
end: 1721207525000,
|
||||||
|
step: 60,
|
||||||
|
variables: {},
|
||||||
|
formatForWeb: true,
|
||||||
|
compositeQuery: {
|
||||||
|
queryType: 'builder',
|
||||||
|
panelType: 'table',
|
||||||
|
fillGaps: false,
|
||||||
|
builderQueries: {
|
||||||
|
A: {
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'signoz_latency',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'ExponentialHistogram',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||||
|
},
|
||||||
|
timeAggregation: '',
|
||||||
|
spaceAggregation: 'p90',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [
|
||||||
|
{
|
||||||
|
key: 'service_name',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'service_name--string--tag--false',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataUpdatedAt: 1721207526018,
|
||||||
|
error: null,
|
||||||
|
errorUpdatedAt: 0,
|
||||||
|
failureCount: 0,
|
||||||
|
errorUpdateCount: 0,
|
||||||
|
isFetched: true,
|
||||||
|
isFetchedAfterMount: true,
|
||||||
|
isFetching: false,
|
||||||
|
isRefetching: false,
|
||||||
|
isLoadingError: false,
|
||||||
|
isPlaceholderData: false,
|
||||||
|
isPreviousData: false,
|
||||||
|
isRefetchError: false,
|
||||||
|
isStale: true,
|
||||||
|
};
|
@ -0,0 +1,267 @@
|
|||||||
|
export const valuePanelWidget = {
|
||||||
|
id: 'b8b93086-ef01-47bf-9044-1e7abd583be4',
|
||||||
|
title: 'signoz latency in ms',
|
||||||
|
description: '',
|
||||||
|
isStacked: false,
|
||||||
|
nullZeroValues: 'zero',
|
||||||
|
opacity: '1',
|
||||||
|
panelTypes: 'value',
|
||||||
|
query: {
|
||||||
|
clickhouse_sql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'signoz_latency',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'ExponentialHistogram',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||||
|
},
|
||||||
|
timeAggregation: '',
|
||||||
|
spaceAggregation: 'p90',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
id: '3bec289c-49c3-4d7e-98bb-84d47c79909c',
|
||||||
|
queryType: 'builder',
|
||||||
|
},
|
||||||
|
timePreferance: 'GLOBAL_TIME',
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
selectedLogFields: [
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
type: '',
|
||||||
|
name: 'body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
type: '',
|
||||||
|
name: 'timestamp',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedTracesFields: [
|
||||||
|
{
|
||||||
|
key: 'serviceName',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'serviceName--string--tag--true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'name--string--tag--true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'durationNano',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'durationNano--float64--tag--true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'httpMethod',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'httpMethod--string--tag--true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'responseStatusCode',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'responseStatusCode--string--tag--true',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxisUnit: 'ms',
|
||||||
|
thresholds: [],
|
||||||
|
fillSpans: false,
|
||||||
|
columnUnits: {},
|
||||||
|
bucketCount: 30,
|
||||||
|
stackedBarChart: false,
|
||||||
|
bucketWidth: 0,
|
||||||
|
mergeAllActiveQueries: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const thresholds = [
|
||||||
|
{
|
||||||
|
index: '8eb16a3a-b4f1-47c8-943a-4b1786884583',
|
||||||
|
isEditEnabled: false,
|
||||||
|
thresholdColor: 'Blue',
|
||||||
|
thresholdFormat: 'Text',
|
||||||
|
thresholdOperator: '>',
|
||||||
|
thresholdUnit: 'none',
|
||||||
|
thresholdValue: 100,
|
||||||
|
keyIndex: 1,
|
||||||
|
selectedGraph: 'value',
|
||||||
|
thresholdTableOptions: '',
|
||||||
|
thresholdLabel: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 'eb9c1186-ad7d-42dd-8e7f-3913a321d7cf',
|
||||||
|
isEditEnabled: false,
|
||||||
|
thresholdColor: 'Red',
|
||||||
|
thresholdFormat: 'Text',
|
||||||
|
thresholdOperator: '>',
|
||||||
|
thresholdUnit: 'none',
|
||||||
|
thresholdValue: 0,
|
||||||
|
keyIndex: 0,
|
||||||
|
selectedGraph: 'value',
|
||||||
|
thresholdTableOptions: '',
|
||||||
|
thresholdLabel: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const valuePanelQueryResponse = {
|
||||||
|
status: 'success',
|
||||||
|
isLoading: false,
|
||||||
|
isSuccess: true,
|
||||||
|
isError: false,
|
||||||
|
isIdle: false,
|
||||||
|
data: {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: 'success',
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
metric: {
|
||||||
|
A: 'A',
|
||||||
|
},
|
||||||
|
values: [[0, '295.4299833508185']],
|
||||||
|
queryName: 'A',
|
||||||
|
legend: 'A',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resultType: '',
|
||||||
|
newResult: {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
resultType: '',
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
queryName: 'A',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
labels: {
|
||||||
|
A: 'A',
|
||||||
|
},
|
||||||
|
labelsArray: null,
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
timestamp: 0,
|
||||||
|
value: '295.4299833508185',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
start: 1721203451000,
|
||||||
|
end: 1721203751000,
|
||||||
|
step: 60,
|
||||||
|
variables: {},
|
||||||
|
formatForWeb: false,
|
||||||
|
compositeQuery: {
|
||||||
|
queryType: 'builder',
|
||||||
|
panelType: 'value',
|
||||||
|
fillGaps: false,
|
||||||
|
builderQueries: {
|
||||||
|
A: {
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'signoz_latency',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'ExponentialHistogram',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'signoz_latency--float64--ExponentialHistogram--true',
|
||||||
|
},
|
||||||
|
timeAggregation: '',
|
||||||
|
spaceAggregation: 'p90',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataUpdatedAt: 1721203751775,
|
||||||
|
error: null,
|
||||||
|
errorUpdatedAt: 0,
|
||||||
|
failureCount: 0,
|
||||||
|
errorUpdateCount: 0,
|
||||||
|
isFetched: true,
|
||||||
|
isFetchedAfterMount: true,
|
||||||
|
isFetching: false,
|
||||||
|
isRefetching: false,
|
||||||
|
isLoadingError: false,
|
||||||
|
isPlaceholderData: false,
|
||||||
|
isPreviousData: false,
|
||||||
|
isRefetchError: false,
|
||||||
|
isStale: true,
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { EditFilled, PlusOutlined } from '@ant-design/icons';
|
import { EditFilled, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ActionMode, ActionType, Pipeline } from 'types/api/pipeline/def';
|
import { ActionMode, ActionType, Pipeline } from 'types/api/pipeline/def';
|
||||||
@ -15,7 +15,6 @@ function CreatePipelineButton({
|
|||||||
pipelineData,
|
pipelineData,
|
||||||
}: CreatePipelineButtonProps): JSX.Element {
|
}: CreatePipelineButtonProps): JSX.Element {
|
||||||
const { t } = useTranslation(['pipeline']);
|
const { t } = useTranslation(['pipeline']);
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const isAddNewPipelineVisible = useMemo(
|
const isAddNewPipelineVisible = useMemo(
|
||||||
() => checkDataLength(pipelineData?.pipelines),
|
() => checkDataLength(pipelineData?.pipelines),
|
||||||
@ -26,7 +25,7 @@ function CreatePipelineButton({
|
|||||||
const onEnterEditMode = (): void => {
|
const onEnterEditMode = (): void => {
|
||||||
setActionMode(ActionMode.Editing);
|
setActionMode(ActionMode.Editing);
|
||||||
|
|
||||||
trackEvent('Logs: Pipelines: Entered Edit Mode', {
|
logEvent('Logs: Pipelines: Entered Edit Mode', {
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -34,7 +33,7 @@ function CreatePipelineButton({
|
|||||||
setActionMode(ActionMode.Editing);
|
setActionMode(ActionMode.Editing);
|
||||||
setActionType(ActionType.AddPipeline);
|
setActionType(ActionType.AddPipeline);
|
||||||
|
|
||||||
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
logEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PlusCircleOutlined } from '@ant-design/icons';
|
import { PlusCircleOutlined } from '@ant-design/icons';
|
||||||
import { TableLocale } from 'antd/es/table/interface';
|
import { TableLocale } from 'antd/es/table/interface';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
@ -39,7 +39,6 @@ function PipelineExpandView({
|
|||||||
}: PipelineExpandViewProps): JSX.Element {
|
}: PipelineExpandViewProps): JSX.Element {
|
||||||
const { t } = useTranslation(['pipeline']);
|
const { t } = useTranslation(['pipeline']);
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
const isEditingActionMode = isActionMode === ActionMode.Editing;
|
const isEditingActionMode = isActionMode === ActionMode.Editing;
|
||||||
|
|
||||||
const deleteProcessorHandler = useCallback(
|
const deleteProcessorHandler = useCallback(
|
||||||
@ -192,7 +191,7 @@ function PipelineExpandView({
|
|||||||
const addNewProcessorHandler = useCallback((): void => {
|
const addNewProcessorHandler = useCallback((): void => {
|
||||||
setActionType(ActionType.AddProcessor);
|
setActionType(ActionType.AddProcessor);
|
||||||
|
|
||||||
trackEvent('Logs: Pipelines: Clicked Add New Processor', {
|
logEvent('Logs: Pipelines: Clicked Add New Processor', {
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -5,7 +5,6 @@ import { Card, Modal, Table, Typography } from 'antd';
|
|||||||
import { ExpandableConfig } from 'antd/es/table/interface';
|
import { ExpandableConfig } from 'antd/es/table/interface';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import savePipeline from 'api/pipeline/post';
|
import savePipeline from 'api/pipeline/post';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
@ -100,7 +99,6 @@ function PipelineListsView({
|
|||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const [pipelineSearchValue, setPipelineSearchValue] = useState<string>('');
|
const [pipelineSearchValue, setPipelineSearchValue] = useState<string>('');
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
|
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
|
||||||
cloneDeep(pipelineData?.pipelines || []),
|
cloneDeep(pipelineData?.pipelines || []),
|
||||||
);
|
);
|
||||||
@ -376,7 +374,7 @@ function PipelineListsView({
|
|||||||
const addNewPipelineHandler = useCallback((): void => {
|
const addNewPipelineHandler = useCallback((): void => {
|
||||||
setActionType(ActionType.AddPipeline);
|
setActionType(ActionType.AddPipeline);
|
||||||
|
|
||||||
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
logEvent('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -415,7 +413,7 @@ function PipelineListsView({
|
|||||||
setCurrPipelineData(pipelinesInDB);
|
setCurrPipelineData(pipelinesInDB);
|
||||||
setPrevPipelineData(pipelinesInDB);
|
setPrevPipelineData(pipelinesInDB);
|
||||||
|
|
||||||
trackEvent('Logs: Pipelines: Saved Pipelines', {
|
logEvent('Logs: Pipelines: Saved Pipelines', {
|
||||||
count: pipelinesInDB.length,
|
count: pipelinesInDB.length,
|
||||||
enabled: pipelinesInDB.filter((p) => p.enabled).length,
|
enabled: pipelinesInDB.filter((p) => p.enabled).length,
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { EyeFilled } from '@ant-design/icons';
|
import { EyeFilled } from '@ant-design/icons';
|
||||||
import { Divider, Modal } from 'antd';
|
import { Divider, Modal } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import PipelineProcessingPreview from 'container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview';
|
import PipelineProcessingPreview from 'container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PipelineData } from 'types/api/pipeline/def';
|
import { PipelineData } from 'types/api/pipeline/def';
|
||||||
|
|
||||||
import { iconStyle } from '../../../config';
|
import { iconStyle } from '../../../config';
|
||||||
|
|
||||||
function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
|
function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const [previewKey, setPreviewKey] = useState<string | null>(null);
|
const [previewKey, setPreviewKey] = useState<string | null>(null);
|
||||||
const isModalOpen = Boolean(previewKey);
|
const isModalOpen = Boolean(previewKey);
|
||||||
|
|
||||||
@ -23,7 +21,7 @@ function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
|
|||||||
|
|
||||||
const onOpenPreview = (): void => {
|
const onOpenPreview = (): void => {
|
||||||
openModal();
|
openModal();
|
||||||
trackEvent('Logs: Pipelines: Clicked Preview Pipeline', {
|
logEvent('Logs: Pipelines: Clicked Preview Pipeline', {
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
@ -9,14 +10,7 @@ import store from 'store';
|
|||||||
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
|
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
|
||||||
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
import { pipelineApiResponseMockData } from '../mocks/pipeline';
|
||||||
|
|
||||||
const trackEventVar = jest.fn();
|
jest.mock('api/common/logEvent');
|
||||||
jest.mock('hooks/analytics/useAnalytics', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: jest.fn().mockImplementation(() => ({
|
|
||||||
trackEvent: trackEventVar,
|
|
||||||
trackPageView: jest.fn(),
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('PipelinePage container test', () => {
|
describe('PipelinePage container test', () => {
|
||||||
it('should render CreatePipelineButton section', async () => {
|
it('should render CreatePipelineButton section', async () => {
|
||||||
@ -58,7 +52,7 @@ describe('PipelinePage container test', () => {
|
|||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
await userEvent.click(editButton);
|
await userEvent.click(editButton);
|
||||||
|
|
||||||
expect(trackEventVar).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
|
expect(logEvent).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -83,11 +77,8 @@ describe('PipelinePage container test', () => {
|
|||||||
expect(editButton).toBeInTheDocument();
|
expect(editButton).toBeInTheDocument();
|
||||||
await userEvent.click(editButton);
|
await userEvent.click(editButton);
|
||||||
|
|
||||||
expect(trackEventVar).toBeCalledWith(
|
expect(logEvent).toBeCalledWith('Logs: Pipelines: Clicked Add New Pipeline', {
|
||||||
'Logs: Pipelines: Clicked Add New Pipeline',
|
|
||||||
{
|
|
||||||
source: 'signoz-ui',
|
source: 'signoz-ui',
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -59,7 +59,7 @@ function ServiceTraces(): JSX.Element {
|
|||||||
logEvent('APM: List page visited', {
|
logEvent('APM: List page visited', {
|
||||||
numberOfServices: data?.length,
|
numberOfServices: data?.length,
|
||||||
selectedEnvironments,
|
selectedEnvironments,
|
||||||
resourceAttributeUsed: !!queries.length,
|
resourceAttributeUsed: !!queries?.length,
|
||||||
rps,
|
rps,
|
||||||
});
|
});
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
|
@ -324,8 +324,8 @@ function SideNav({
|
|||||||
onClickHandler(item?.key as string, event);
|
onClickHandler(item?.key as string, event);
|
||||||
}
|
}
|
||||||
logEvent('Sidebar: Menu clicked', {
|
logEvent('Sidebar: Menu clicked', {
|
||||||
menuRoute: item.key,
|
menuRoute: item?.key,
|
||||||
menuLabel: item.label,
|
menuLabel: item?.label,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -455,8 +455,8 @@ function SideNav({
|
|||||||
onClick={(event: MouseEvent): void => {
|
onClick={(event: MouseEvent): void => {
|
||||||
handleUserManagentMenuItemClick(item?.key as string, event);
|
handleUserManagentMenuItemClick(item?.key as string, event);
|
||||||
logEvent('Sidebar: Menu clicked', {
|
logEvent('Sidebar: Menu clicked', {
|
||||||
menuRoute: item.key,
|
menuRoute: item?.key,
|
||||||
menuLabel: item.label,
|
menuLabel: item?.label,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -475,8 +475,8 @@ function SideNav({
|
|||||||
history.push(`${inviteMemberMenuItem.key}`);
|
history.push(`${inviteMemberMenuItem.key}`);
|
||||||
}
|
}
|
||||||
logEvent('Sidebar: Menu clicked', {
|
logEvent('Sidebar: Menu clicked', {
|
||||||
menuRoute: inviteMemberMenuItem.key,
|
menuRoute: inviteMemberMenuItem?.key,
|
||||||
menuLabel: inviteMemberMenuItem.label,
|
menuLabel: inviteMemberMenuItem?.label,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -493,7 +493,7 @@ function SideNav({
|
|||||||
event,
|
event,
|
||||||
);
|
);
|
||||||
logEvent('Sidebar: Menu clicked', {
|
logEvent('Sidebar: Menu clicked', {
|
||||||
menuRoute: userSettingsMenuItem.key,
|
menuRoute: userSettingsMenuItem?.key,
|
||||||
menuLabel: 'User',
|
menuLabel: 'User',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import Card from 'antd/es/card/Card';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const Container = styled(Card)`
|
export const Container = styled(Card)`
|
||||||
|
@ -23,7 +23,7 @@ import NewExplorerCTA from 'container/NewExplorerCTA';
|
|||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import GetMinMax from 'lib/getMinMax';
|
import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax';
|
||||||
import getTimeString from 'lib/getTimeString';
|
import getTimeString from 'lib/getTimeString';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isObject } from 'lodash-es';
|
import { isObject } from 'lodash-es';
|
||||||
@ -73,6 +73,7 @@ function DateTimeSelection({
|
|||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
const searchStartTime = urlQuery.get('startTime');
|
const searchStartTime = urlQuery.get('startTime');
|
||||||
const searchEndTime = urlQuery.get('endTime');
|
const searchEndTime = urlQuery.get('endTime');
|
||||||
|
const relativeTimeFromUrl = urlQuery.get(QueryParams.relativeTime);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false);
|
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false);
|
||||||
const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false);
|
const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false);
|
||||||
@ -404,9 +405,18 @@ function DateTimeSelection({
|
|||||||
time: Time,
|
time: Time,
|
||||||
currentRoute: string,
|
currentRoute: string,
|
||||||
): Time | CustomTimeType => {
|
): Time | CustomTimeType => {
|
||||||
|
// if the relativeTime param is present in the url give top most preference to the same
|
||||||
|
// if the relativeTime param is not valid then move to next preference
|
||||||
|
if (relativeTimeFromUrl != null && isValidTimeFormat(relativeTimeFromUrl)) {
|
||||||
|
return relativeTimeFromUrl as Time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the startTime and endTime params are present in the url give next preference to the them.
|
||||||
if (searchEndTime !== null && searchStartTime !== null) {
|
if (searchEndTime !== null && searchStartTime !== null) {
|
||||||
return 'custom';
|
return 'custom';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if nothing is present in the url for time range then rely on the local storage values
|
||||||
if (
|
if (
|
||||||
(localstorageEndTime === null || localstorageStartTime === null) &&
|
(localstorageEndTime === null || localstorageStartTime === null) &&
|
||||||
time === 'custom'
|
time === 'custom'
|
||||||
@ -414,6 +424,7 @@ function DateTimeSelection({
|
|||||||
return getDefaultOption(currentRoute);
|
return getDefaultOption(currentRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if not present in the local storage as well then rely on the defaults set for the page
|
||||||
if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) {
|
if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) {
|
||||||
return convertOldTimeToNewValidCustomTimeFormat(time);
|
return convertOldTimeToNewValidCustomTimeFormat(time);
|
||||||
}
|
}
|
||||||
@ -448,7 +459,11 @@ function DateTimeSelection({
|
|||||||
|
|
||||||
setRefreshButtonHidden(updatedTime === 'custom');
|
setRefreshButtonHidden(updatedTime === 'custom');
|
||||||
|
|
||||||
|
if (updatedTime !== 'custom') {
|
||||||
|
updateTimeInterval(updatedTime);
|
||||||
|
} else {
|
||||||
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
|
||||||
|
}
|
||||||
|
|
||||||
if (updatedTime !== 'custom') {
|
if (updatedTime !== 'custom') {
|
||||||
urlQuery.delete('startTime');
|
urlQuery.delete('startTime');
|
||||||
|
@ -31,7 +31,14 @@ function TopNav(): JSX.Element | null {
|
|||||||
[location.pathname],
|
[location.pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSignUpPage || isDisabled || isRouteToSkip) {
|
const isNewAlertsLandingPage = useMemo(
|
||||||
|
() =>
|
||||||
|
matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }) &&
|
||||||
|
!location.search,
|
||||||
|
[location.pathname, location.search],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSignUpPage || isDisabled || isRouteToSkip || isNewAlertsLandingPage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import './TraceDetails.styles.scss';
|
import './TraceDetails.styles.scss';
|
||||||
|
|
||||||
import { FilterOutlined } from '@ant-design/icons';
|
import { FilterOutlined } from '@ant-design/icons';
|
||||||
import { Button, Col, Typography } from 'antd';
|
import { Button, Col, Layout, Typography } from 'antd';
|
||||||
import Sider from 'antd/es/layout/Sider';
|
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import {
|
import {
|
||||||
StyledCol,
|
StyledCol,
|
||||||
@ -42,6 +41,8 @@ import {
|
|||||||
INTERVAL_UNITS,
|
INTERVAL_UNITS,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
|
const { Sider } = Layout;
|
||||||
|
|
||||||
function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||||
const spanServiceColors = useMemo(
|
const spanServiceColors = useMemo(
|
||||||
() => spanServiceNameToColorMapping(response[0].events),
|
() => spanServiceNameToColorMapping(response[0].events),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Col } from 'antd';
|
import { Card, Col } from 'antd';
|
||||||
import Card from 'antd/es/card/Card';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const Container = styled(Card)`
|
export const Container = styled(Card)`
|
||||||
|
@ -32,16 +32,6 @@ const useAnalytics = (): any => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// // Perform any setup or cleanup related to the analytics library
|
|
||||||
// // For example, initialize analytics library here
|
|
||||||
|
|
||||||
// // Clean-up function (optional)
|
|
||||||
// return () => {
|
|
||||||
// // Perform cleanup if needed
|
|
||||||
// };
|
|
||||||
// }, []); // The empty dependency array ensures that this effect runs only once when the component mounts
|
|
||||||
|
|
||||||
return { trackPageView, trackEvent };
|
return { trackPageView, trackEvent };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,7 +61,10 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
|||||||
});
|
});
|
||||||
queryRangeMutation.mutate(queryPayload, {
|
queryRangeMutation.mutate(queryPayload, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
const updatedQuery = mapQueryDataFromApi(data.compositeQuery);
|
const updatedQuery = mapQueryDataFromApi(
|
||||||
|
data.compositeQuery,
|
||||||
|
widget?.query,
|
||||||
|
);
|
||||||
|
|
||||||
history.push(
|
history.push(
|
||||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
import { mapQueryDataFromApi } from '../mapQueryDataFromApi';
|
||||||
|
import {
|
||||||
|
compositeQueriesWithFunctions,
|
||||||
|
compositeQueryWithoutVariables,
|
||||||
|
compositeQueryWithVariables,
|
||||||
|
defaultOutput,
|
||||||
|
outputWithFunctions,
|
||||||
|
replaceVariables,
|
||||||
|
stepIntervalUnchanged,
|
||||||
|
widgetQueriesWithFunctions,
|
||||||
|
widgetQueryWithoutVariables,
|
||||||
|
widgetQueryWithVariables,
|
||||||
|
} from './mapQueryDataFromApiInputs';
|
||||||
|
|
||||||
|
jest.mock('uuid', () => ({
|
||||||
|
v4: (): string => 'test-id',
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('mapQueryDataFromApi function tests', () => {
|
||||||
|
it('should not update the step interval when query is passed', () => {
|
||||||
|
const output = mapQueryDataFromApi(
|
||||||
|
compositeQueryWithoutVariables,
|
||||||
|
widgetQueryWithoutVariables,
|
||||||
|
);
|
||||||
|
|
||||||
|
// composite query is the response from the `v3/query_range/format` API call.
|
||||||
|
// even if the composite query returns stepInterval updated do not modify it
|
||||||
|
expect(output).toStrictEqual(stepIntervalUnchanged);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update filter from the composite query', () => {
|
||||||
|
const output = mapQueryDataFromApi(
|
||||||
|
compositeQueryWithVariables,
|
||||||
|
widgetQueryWithVariables,
|
||||||
|
);
|
||||||
|
|
||||||
|
// replace the variables in the widget query and leave the rest items untouched
|
||||||
|
expect(output).toStrictEqual(replaceVariables);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update the step intervals with multiple queries and functions', () => {
|
||||||
|
const output = mapQueryDataFromApi(
|
||||||
|
compositeQueriesWithFunctions,
|
||||||
|
widgetQueriesWithFunctions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toStrictEqual(outputWithFunctions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the default query values and the compositeQuery object when query is not passed', () => {
|
||||||
|
const output = mapQueryDataFromApi(compositeQueryWithoutVariables);
|
||||||
|
|
||||||
|
// when the query object is not passed take the initial values and merge the composite query on top of it
|
||||||
|
expect(output).toStrictEqual(defaultOutput);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,741 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export const compositeQueryWithoutVariables = ({
|
||||||
|
builderQueries: {
|
||||||
|
A: {
|
||||||
|
queryName: 'A',
|
||||||
|
stepInterval: 240,
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
aggregateOperator: 'rate',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'system_disk_operations',
|
||||||
|
dataType: DataTypes.Float64,
|
||||||
|
type: 'Sum',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 0,
|
||||||
|
reduceTo: 'avg',
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
ShiftBy: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
|
} as unknown) as ICompositeMetricQuery;
|
||||||
|
|
||||||
|
export const widgetQueryWithoutVariables = ({
|
||||||
|
clickhouse_sql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'rate',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'system_disk_operations',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'Sum',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'system_disk_operations--float64--Sum--true',
|
||||||
|
},
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
id: '2bbbd8d8-db99-40be-b9c6-9e197c5bc537',
|
||||||
|
queryType: 'builder',
|
||||||
|
} as unknown) as Query;
|
||||||
|
|
||||||
|
export const stepIntervalUnchanged = {
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
aggregateAttribute: {
|
||||||
|
dataType: 'float64',
|
||||||
|
id: 'system_disk_operations--float64--Sum--true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'system_disk_operations',
|
||||||
|
type: 'Sum',
|
||||||
|
},
|
||||||
|
aggregateOperator: 'rate',
|
||||||
|
dataSource: 'metrics',
|
||||||
|
disabled: false,
|
||||||
|
expression: 'A',
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
functions: [],
|
||||||
|
groupBy: [],
|
||||||
|
having: [],
|
||||||
|
legend: '',
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
queryName: 'A',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
stepInterval: 60,
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
clickhouse_sql: [
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'test-id',
|
||||||
|
promql: [
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryType: 'builder',
|
||||||
|
unit: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compositeQueryWithVariables = ({
|
||||||
|
builderQueries: {
|
||||||
|
A: {
|
||||||
|
queryName: 'A',
|
||||||
|
stepInterval: 240,
|
||||||
|
dataSource: 'metrics',
|
||||||
|
aggregateOperator: 'sum_rate',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'signoz_calls_total',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: '',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
key: 'deployment_environment',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
value: 'default',
|
||||||
|
op: 'in',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
key: 'service_name',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
value: 'frontend',
|
||||||
|
op: 'in',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
key: 'operation',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
value: 'HTTP GET /dispatch',
|
||||||
|
op: 'in',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
groupBy: [
|
||||||
|
{
|
||||||
|
key: 'service_name',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'operation',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
legend: '{{service_name}}-{{operation}}',
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 0,
|
||||||
|
reduceTo: 'sum',
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
ShiftBy: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
panelType: 'graph',
|
||||||
|
queryType: 'builder',
|
||||||
|
} as unknown) as ICompositeMetricQuery;
|
||||||
|
|
||||||
|
export const widgetQueryWithVariables = ({
|
||||||
|
clickhouse_sql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'sum_rate',
|
||||||
|
aggregateAttribute: {
|
||||||
|
dataType: 'float64',
|
||||||
|
id: 'signoz_calls_total--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'signoz_calls_total',
|
||||||
|
type: '',
|
||||||
|
},
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'aa56621e',
|
||||||
|
key: {
|
||||||
|
dataType: 'string',
|
||||||
|
id: 'deployment_environment--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'deployment_environment',
|
||||||
|
type: 'tag',
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['{{.deployment_environment}}'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '97055a02',
|
||||||
|
key: {
|
||||||
|
dataType: 'string',
|
||||||
|
id: 'service_name--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'service_name',
|
||||||
|
type: 'tag',
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['{{.service_name}}'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8c4599f2',
|
||||||
|
key: {
|
||||||
|
dataType: 'string',
|
||||||
|
id: 'operation--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'operation',
|
||||||
|
type: 'tag',
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['{{.endpoint}}'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'service_name',
|
||||||
|
type: 'tag',
|
||||||
|
id: 'service_name--string--tag--false',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'operation',
|
||||||
|
type: 'tag',
|
||||||
|
id: 'operation--string--tag--false',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
legend: '{{service_name}}-{{operation}}',
|
||||||
|
reduceTo: 'sum',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
id: '64fcd7be-61d0-4f92-bbb2-1449b089f766',
|
||||||
|
queryType: 'builder',
|
||||||
|
} as unknown) as Query;
|
||||||
|
|
||||||
|
export const replaceVariables = {
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
aggregateAttribute: {
|
||||||
|
dataType: 'float64',
|
||||||
|
id: 'signoz_calls_total--float64----true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'signoz_calls_total',
|
||||||
|
type: '',
|
||||||
|
},
|
||||||
|
aggregateOperator: 'sum_rate',
|
||||||
|
dataSource: 'metrics',
|
||||||
|
disabled: false,
|
||||||
|
expression: 'A',
|
||||||
|
filters: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
dataType: 'string',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'deployment_environment',
|
||||||
|
type: 'tag',
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
dataType: 'string',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'service_name',
|
||||||
|
type: 'tag',
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: 'frontend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
dataType: 'string',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'operation',
|
||||||
|
type: 'tag',
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: 'HTTP GET /dispatch',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
functions: [],
|
||||||
|
groupBy: [
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
id: 'service_name--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'service_name',
|
||||||
|
type: 'tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataType: 'string',
|
||||||
|
id: 'operation--string--tag--false',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'operation',
|
||||||
|
type: 'tag',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
having: [],
|
||||||
|
legend: '{{service_name}}-{{operation}}',
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
queryName: 'A',
|
||||||
|
reduceTo: 'sum',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
stepInterval: 60,
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
clickhouse_sql: [
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'test-id',
|
||||||
|
promql: [
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryType: 'builder',
|
||||||
|
unit: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultOutput = {
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
ShiftBy: 0,
|
||||||
|
aggregateAttribute: {
|
||||||
|
dataType: 'float64',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'system_disk_operations',
|
||||||
|
type: 'Sum',
|
||||||
|
},
|
||||||
|
aggregateOperator: 'rate',
|
||||||
|
dataSource: 'metrics',
|
||||||
|
disabled: false,
|
||||||
|
expression: 'A',
|
||||||
|
filters: { items: [], op: 'AND' },
|
||||||
|
functions: [],
|
||||||
|
groupBy: [],
|
||||||
|
having: [],
|
||||||
|
legend: '',
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
orderBy: [],
|
||||||
|
pageSize: 0,
|
||||||
|
queryName: 'A',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
stepInterval: 240,
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||||
|
id: 'test-id',
|
||||||
|
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||||
|
queryType: 'builder',
|
||||||
|
unit: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compositeQueriesWithFunctions = ({
|
||||||
|
builderQueries: {
|
||||||
|
A: {
|
||||||
|
queryName: 'A',
|
||||||
|
stepInterval: 60,
|
||||||
|
dataSource: 'metrics',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'signoz_latency_bucket',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'Histogram',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 0,
|
||||||
|
reduceTo: 'avg',
|
||||||
|
spaceAggregation: 'p90',
|
||||||
|
ShiftBy: 0,
|
||||||
|
},
|
||||||
|
B: {
|
||||||
|
queryName: 'B',
|
||||||
|
stepInterval: 120,
|
||||||
|
dataSource: 'metrics',
|
||||||
|
aggregateOperator: 'rate',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'system_disk_io',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'Sum',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
expression: 'B',
|
||||||
|
disabled: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 0,
|
||||||
|
reduceTo: 'avg',
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
ShiftBy: 0,
|
||||||
|
},
|
||||||
|
F1: {
|
||||||
|
queryName: 'F1',
|
||||||
|
stepInterval: 1,
|
||||||
|
dataSource: '',
|
||||||
|
aggregateOperator: '',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: '',
|
||||||
|
dataType: '',
|
||||||
|
type: '',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
expression: 'A / B ',
|
||||||
|
disabled: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 0,
|
||||||
|
ShiftBy: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
panelType: 'graph',
|
||||||
|
queryType: 'builder',
|
||||||
|
} as unknown) as ICompositeMetricQuery;
|
||||||
|
|
||||||
|
export const widgetQueriesWithFunctions = ({
|
||||||
|
clickhouse_sql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
promql: [
|
||||||
|
{
|
||||||
|
name: 'A',
|
||||||
|
query: '',
|
||||||
|
legend: '',
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
dataType: 'float64',
|
||||||
|
id: 'signoz_latency_bucket--float64--Histogram--true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'signoz_latency_bucket',
|
||||||
|
type: 'Histogram',
|
||||||
|
},
|
||||||
|
timeAggregation: '',
|
||||||
|
spaceAggregation: 'p90',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 120,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'B',
|
||||||
|
aggregateOperator: 'rate',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'system_disk_io',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'Sum',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'system_disk_io--float64--Sum--true',
|
||||||
|
},
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'B',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 120,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [
|
||||||
|
{
|
||||||
|
queryName: 'F1',
|
||||||
|
expression: 'A / B ',
|
||||||
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id: '5d1844fe-9b44-4f15-b6fe-f1b843550b77',
|
||||||
|
queryType: 'builder',
|
||||||
|
} as unknown) as Query;
|
||||||
|
|
||||||
|
export const outputWithFunctions = {
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
dataType: 'float64',
|
||||||
|
id: 'signoz_latency_bucket--float64--Histogram--true',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
key: 'signoz_latency_bucket',
|
||||||
|
type: 'Histogram',
|
||||||
|
},
|
||||||
|
timeAggregation: '',
|
||||||
|
spaceAggregation: 'p90',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 120,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dataSource: 'metrics',
|
||||||
|
queryName: 'B',
|
||||||
|
aggregateOperator: 'rate',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'system_disk_io',
|
||||||
|
dataType: 'float64',
|
||||||
|
type: 'Sum',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'system_disk_io--float64--Sum--true',
|
||||||
|
},
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
expression: 'B',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 120,
|
||||||
|
having: [],
|
||||||
|
limit: null,
|
||||||
|
orderBy: [],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [
|
||||||
|
{
|
||||||
|
queryName: 'F1',
|
||||||
|
expression: 'A / B ',
|
||||||
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||||
|
id: 'test-id',
|
||||||
|
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
|
||||||
|
queryType: 'builder',
|
||||||
|
unit: undefined,
|
||||||
|
};
|
@ -7,9 +7,13 @@ import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataMode
|
|||||||
|
|
||||||
export const mapQueryDataFromApi = (
|
export const mapQueryDataFromApi = (
|
||||||
compositeQuery: ICompositeMetricQuery,
|
compositeQuery: ICompositeMetricQuery,
|
||||||
|
query?: Query,
|
||||||
): Query => {
|
): Query => {
|
||||||
const builder = compositeQuery.builderQueries
|
const builder = compositeQuery.builderQueries
|
||||||
? transformQueryBuilderDataModel(compositeQuery.builderQueries)
|
? transformQueryBuilderDataModel(
|
||||||
|
compositeQuery.builderQueries,
|
||||||
|
query?.builder,
|
||||||
|
)
|
||||||
: initialQueryState.builder;
|
: initialQueryState.builder;
|
||||||
|
|
||||||
const promql = compositeQuery.promQueries
|
const promql = compositeQuery.promQueries
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import { FORMULA_REGEXP } from 'constants/regExp';
|
import { FORMULA_REGEXP } from 'constants/regExp';
|
||||||
|
import { isUndefined } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
BuilderQueryDataResourse,
|
BuilderQueryDataResourse,
|
||||||
IBuilderFormula,
|
IBuilderFormula,
|
||||||
@ -12,6 +13,7 @@ import { QueryBuilderData } from 'types/common/queryBuilder';
|
|||||||
|
|
||||||
export const transformQueryBuilderDataModel = (
|
export const transformQueryBuilderDataModel = (
|
||||||
data: BuilderQueryDataResourse,
|
data: BuilderQueryDataResourse,
|
||||||
|
query?: QueryBuilderData,
|
||||||
): QueryBuilderData => {
|
): QueryBuilderData => {
|
||||||
const queryData: QueryBuilderData['queryData'] = [];
|
const queryData: QueryBuilderData['queryData'] = [];
|
||||||
const queryFormulas: QueryBuilderData['queryFormulas'] = [];
|
const queryFormulas: QueryBuilderData['queryFormulas'] = [];
|
||||||
@ -19,10 +21,37 @@ export const transformQueryBuilderDataModel = (
|
|||||||
Object.entries(data).forEach(([, value]) => {
|
Object.entries(data).forEach(([, value]) => {
|
||||||
if (FORMULA_REGEXP.test(value.queryName)) {
|
if (FORMULA_REGEXP.test(value.queryName)) {
|
||||||
const formula = value as IBuilderFormula;
|
const formula = value as IBuilderFormula;
|
||||||
queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula });
|
const baseFormula = query?.queryFormulas?.find(
|
||||||
|
(f) => f.queryName === value.queryName,
|
||||||
|
);
|
||||||
|
if (!isUndefined(baseFormula)) {
|
||||||
|
// this is part of the flow where we create alerts from dashboard.
|
||||||
|
// we pass the formula as is from the widget query as we do not want anything to update in formula from the format api call
|
||||||
|
queryFormulas.push({ ...baseFormula });
|
||||||
} else {
|
} else {
|
||||||
const query = value as IBuilderQuery;
|
queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula });
|
||||||
queryData.push({ ...initialQueryBuilderFormValuesMap.metrics, ...query });
|
}
|
||||||
|
} else {
|
||||||
|
const queryFromData = value as IBuilderQuery;
|
||||||
|
const baseQuery = query?.queryData?.find(
|
||||||
|
(q) => q.queryName === queryFromData.queryName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUndefined(baseQuery)) {
|
||||||
|
// this is part of the flow where we create alerts from dashboard.
|
||||||
|
// we pass the widget query as the base query and accept the filters from the format API response.
|
||||||
|
// which fills the variable values inside the same and is used to create alerts
|
||||||
|
// do not accept the full object as the stepInterval field is subject to changes
|
||||||
|
queryData.push({
|
||||||
|
...baseQuery,
|
||||||
|
filters: queryFromData.filters,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
queryData.push({
|
||||||
|
...initialQueryBuilderFormValuesMap.metrics,
|
||||||
|
...queryFromData,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
101
frontend/src/mocks-server/__mockdata__/dashboards.ts
Normal file
101
frontend/src/mocks-server/__mockdata__/dashboards.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
export const dashboardSuccessResponse = {
|
||||||
|
status: 'success',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
uuid: '1',
|
||||||
|
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||||
|
created_by: null,
|
||||||
|
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||||
|
updated_by: 'thor@avengers.io',
|
||||||
|
isLocked: 0,
|
||||||
|
data: {
|
||||||
|
collapsableRowsMigrated: true,
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
panelMap: {},
|
||||||
|
tags: ['linux'],
|
||||||
|
title: 'thor',
|
||||||
|
uploadedGrafana: false,
|
||||||
|
uuid: '',
|
||||||
|
version: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
uuid: '2',
|
||||||
|
created_at: '2022-11-16T13:20:47.064874419Z',
|
||||||
|
created_by: null,
|
||||||
|
updated_at: '2024-05-21T06:42:30.546630961Z',
|
||||||
|
updated_by: 'captain-america@avengers.io',
|
||||||
|
isLocked: 0,
|
||||||
|
data: {
|
||||||
|
collapsableRowsMigrated: true,
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
panelMap: {},
|
||||||
|
tags: ['linux'],
|
||||||
|
title: 'captain america',
|
||||||
|
uploadedGrafana: false,
|
||||||
|
uuid: '',
|
||||||
|
version: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dashboardEmptyState = {
|
||||||
|
status: 'sucsess',
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDashboardById = {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
uuid: '1',
|
||||||
|
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||||
|
created_by: 'integration',
|
||||||
|
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||||
|
updated_by: 'thor@avengers.io',
|
||||||
|
isLocked: true,
|
||||||
|
data: {
|
||||||
|
collapsableRowsMigrated: true,
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
panelMap: {},
|
||||||
|
tags: ['linux'],
|
||||||
|
title: 'thor',
|
||||||
|
uploadedGrafana: false,
|
||||||
|
uuid: '',
|
||||||
|
version: '',
|
||||||
|
variables: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNonIntegrationDashboardById = {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
uuid: '1',
|
||||||
|
created_at: '2022-11-16T13:29:47.064874419Z',
|
||||||
|
created_by: 'thor',
|
||||||
|
updated_at: '2024-05-21T06:41:30.546630961Z',
|
||||||
|
updated_by: 'thor@avengers.io',
|
||||||
|
isLocked: true,
|
||||||
|
data: {
|
||||||
|
collapsableRowsMigrated: true,
|
||||||
|
description: '',
|
||||||
|
name: '',
|
||||||
|
panelMap: {},
|
||||||
|
tags: ['linux'],
|
||||||
|
title: 'thor',
|
||||||
|
uploadedGrafana: false,
|
||||||
|
uuid: '',
|
||||||
|
version: '',
|
||||||
|
variables: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
81
frontend/src/mocks-server/__mockdata__/explorer_views.ts
Normal file
81
frontend/src/mocks-server/__mockdata__/explorer_views.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
export const explorerView = {
|
||||||
|
status: 'success',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
uuid: 'test-uuid-1',
|
||||||
|
name: 'Table View',
|
||||||
|
category: '',
|
||||||
|
createdAt: '2023-08-29T18:04:10.906310033Z',
|
||||||
|
createdBy: 'test-user-1',
|
||||||
|
updatedAt: '2024-01-29T10:42:47.346331133Z',
|
||||||
|
updatedBy: 'test-user-1',
|
||||||
|
sourcePage: 'traces',
|
||||||
|
tags: [''],
|
||||||
|
compositeQuery: {
|
||||||
|
builderQueries: {
|
||||||
|
A: {
|
||||||
|
queryName: 'A',
|
||||||
|
stepInterval: 60,
|
||||||
|
dataSource: 'traces',
|
||||||
|
aggregateOperator: 'count',
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: 'component',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
key: 'component',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
value: 'test-component',
|
||||||
|
op: '!=',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
groupBy: [
|
||||||
|
{
|
||||||
|
key: 'component',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'client-uuid',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
limit: 0,
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 0,
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
columnName: 'timestamp',
|
||||||
|
order: 'desc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
reduceTo: 'sum',
|
||||||
|
ShiftBy: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
panelType: 'table',
|
||||||
|
queryType: 'builder',
|
||||||
|
},
|
||||||
|
extraData: '{"color":"#00ffd0"}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -1,6 +1,11 @@
|
|||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
|
|
||||||
import { billingSuccessResponse } from './__mockdata__/billing';
|
import { billingSuccessResponse } from './__mockdata__/billing';
|
||||||
|
import {
|
||||||
|
dashboardSuccessResponse,
|
||||||
|
getDashboardById,
|
||||||
|
} from './__mockdata__/dashboards';
|
||||||
|
import { explorerView } from './__mockdata__/explorer_views';
|
||||||
import { inviteUser } from './__mockdata__/invite_user';
|
import { inviteUser } from './__mockdata__/invite_user';
|
||||||
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
||||||
import { membersResponse } from './__mockdata__/members';
|
import { membersResponse } from './__mockdata__/members';
|
||||||
@ -54,6 +59,51 @@ export const handlers = [
|
|||||||
const metricName = req.url.searchParams.get('metricName');
|
const metricName = req.url.searchParams.get('metricName');
|
||||||
const tagKey = req.url.searchParams.get('tagKey');
|
const tagKey = req.url.searchParams.get('tagKey');
|
||||||
|
|
||||||
|
const attributeKey = req.url.searchParams.get('attributeKey');
|
||||||
|
|
||||||
|
if (attributeKey === 'serviceName') {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
stringAttributeValues: [
|
||||||
|
'customer',
|
||||||
|
'demo-app',
|
||||||
|
'driver',
|
||||||
|
'frontend',
|
||||||
|
'mysql',
|
||||||
|
'redis',
|
||||||
|
'route',
|
||||||
|
'go-grpc-otel-server',
|
||||||
|
'test',
|
||||||
|
],
|
||||||
|
numberAttributeValues: null,
|
||||||
|
boolAttributeValues: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeKey === 'name') {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
stringAttributeValues: [
|
||||||
|
'HTTP GET',
|
||||||
|
'HTTP GET /customer',
|
||||||
|
'HTTP GET /dispatch',
|
||||||
|
'HTTP GET /route',
|
||||||
|
],
|
||||||
|
numberAttributeValues: null,
|
||||||
|
boolAttributeValues: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
metricName === 'signoz_calls_total' &&
|
metricName === 'signoz_calls_total' &&
|
||||||
tagKey === 'resource_signoz_collector_id'
|
tagKey === 'resource_signoz_collector_id'
|
||||||
@ -86,15 +136,49 @@ export const handlers = [
|
|||||||
res(ctx.status(200), ctx.json(licensesSuccessResponse)),
|
res(ctx.status(200), ctx.json(licensesSuccessResponse)),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ?licenseKey=58707e3d-3bdb-44e7-8c89-a9be237939f4
|
|
||||||
rest.get('http://localhost/api/v1/billing', (req, res, ctx) =>
|
rest.get('http://localhost/api/v1/billing', (req, res, ctx) =>
|
||||||
res(ctx.status(200), ctx.json(billingSuccessResponse)),
|
res(ctx.status(200), ctx.json(billingSuccessResponse)),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
rest.get('http://localhost/api/v1/dashboards', (_, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(dashboardSuccessResponse)),
|
||||||
|
),
|
||||||
|
|
||||||
|
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(getDashboardById)),
|
||||||
|
),
|
||||||
|
|
||||||
rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
|
rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||||
res(ctx.status(200), ctx.json(inviteUser)),
|
res(ctx.status(200), ctx.json(inviteUser)),
|
||||||
),
|
),
|
||||||
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
|
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||||
res(ctx.status(200), ctx.json(inviteUser)),
|
res(ctx.status(200), ctx.json(inviteUser)),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
rest.get(
|
||||||
|
'http://localhost/api/v3/autocomplete/aggregate_attributes',
|
||||||
|
(req, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
status: 'success',
|
||||||
|
data: { attributeKeys: null },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
rest.get('http://localhost/api/v1/explorer/views', (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(explorerView)),
|
||||||
|
),
|
||||||
|
|
||||||
|
rest.post('http://localhost/api/v1/event', (req, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
payload: 'Event Processed Successfully',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
@ -0,0 +1,207 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import DashboardsList from 'container/ListOfDashboard';
|
||||||
|
import { dashboardEmptyState } from 'mocks-server/__mockdata__/dashboards';
|
||||||
|
import { server } from 'mocks-server/server';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||||
|
import { fireEvent, render, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: jest.fn(),
|
||||||
|
useRouteMatch: jest.fn().mockReturnValue({
|
||||||
|
params: {
|
||||||
|
dashboardId: 4,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockWindowOpen = jest.fn();
|
||||||
|
window.open = mockWindowOpen;
|
||||||
|
|
||||||
|
describe('dashboard list page', () => {
|
||||||
|
// should render on updatedAt and descend when the column key and order is messed up
|
||||||
|
it('should render the list even when the columnKey or the order is mismatched', async () => {
|
||||||
|
const mockLocation = {
|
||||||
|
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||||
|
search: `columnKey=asgard&order=stones&page=1`,
|
||||||
|
};
|
||||||
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<MemoryRouter
|
||||||
|
initialEntries={['/dashbords?columnKey=asgard&order=stones&page=1']}
|
||||||
|
>
|
||||||
|
<DashboardProvider>
|
||||||
|
<DashboardsList />
|
||||||
|
</DashboardProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
|
||||||
|
const firstElement = getByTestId('dashboard-title-0');
|
||||||
|
expect(firstElement.textContent).toBe('captain america');
|
||||||
|
const secondElement = getByTestId('dashboard-title-1');
|
||||||
|
expect(secondElement.textContent).toBe('thor');
|
||||||
|
});
|
||||||
|
|
||||||
|
// should render correctly when the column key is createdAt and order is descend
|
||||||
|
it('should render the list even when the columnKey and the order are given', async () => {
|
||||||
|
const mockLocation = {
|
||||||
|
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||||
|
search: `columnKey=createdAt&order=descend&page=1`,
|
||||||
|
};
|
||||||
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<MemoryRouter
|
||||||
|
initialEntries={['/dashbords?columnKey=createdAt&order=descend&page=1']}
|
||||||
|
>
|
||||||
|
<DashboardProvider>
|
||||||
|
<DashboardsList />
|
||||||
|
</DashboardProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
|
||||||
|
const firstElement = getByTestId('dashboard-title-0');
|
||||||
|
expect(firstElement.textContent).toBe('thor');
|
||||||
|
const secondElement = getByTestId('dashboard-title-1');
|
||||||
|
expect(secondElement.textContent).toBe('captain america');
|
||||||
|
});
|
||||||
|
|
||||||
|
// change the sort by order and dashboards list ot be updated accordingly
|
||||||
|
it('dashboards list should be correctly updated on choosing the different sortBy from dropdown values', async () => {
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<MemoryRouter
|
||||||
|
initialEntries={[
|
||||||
|
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DashboardProvider>
|
||||||
|
<DashboardsList />
|
||||||
|
</DashboardProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
|
||||||
|
|
||||||
|
const firstElement = getByTestId('dashboard-title-0');
|
||||||
|
expect(firstElement.textContent).toBe('thor');
|
||||||
|
const secondElement = getByTestId('dashboard-title-1');
|
||||||
|
expect(secondElement.textContent).toBe('captain america');
|
||||||
|
|
||||||
|
// click on the sort button
|
||||||
|
const sortByButton = getByTestId('sort-by');
|
||||||
|
expect(sortByButton).toBeInTheDocument();
|
||||||
|
fireEvent.click(sortByButton!);
|
||||||
|
|
||||||
|
// change the sort order
|
||||||
|
const sortByUpdatedBy = getByTestId('sort-by-last-updated');
|
||||||
|
await waitFor(() => expect(sortByUpdatedBy).toBeInTheDocument());
|
||||||
|
fireEvent.click(sortByUpdatedBy!);
|
||||||
|
|
||||||
|
// expect the new order
|
||||||
|
const updatedFirstElement = getByTestId('dashboard-title-0');
|
||||||
|
expect(updatedFirstElement.textContent).toBe('captain america');
|
||||||
|
const updatedSecondElement = getByTestId('dashboard-title-1');
|
||||||
|
expect(updatedSecondElement.textContent).toBe('thor');
|
||||||
|
});
|
||||||
|
|
||||||
|
// should filter correctly on search string
|
||||||
|
it('should filter dashboards based on search string', async () => {
|
||||||
|
const mockLocation = {
|
||||||
|
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||||
|
search: `columnKey=createdAt&order=descend&page=1&search=tho`,
|
||||||
|
};
|
||||||
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
const { getByText, getByTestId, queryByText } = render(
|
||||||
|
<MemoryRouter
|
||||||
|
initialEntries={[
|
||||||
|
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DashboardProvider>
|
||||||
|
<DashboardsList />
|
||||||
|
</DashboardProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
|
||||||
|
const firstElement = getByTestId('dashboard-title-0');
|
||||||
|
expect(firstElement.textContent).toBe('thor');
|
||||||
|
expect(queryByText('captain america')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// the pagination item should not be present in the list when number of items are less than one page size
|
||||||
|
expect(
|
||||||
|
document.querySelector('.ant-table-pagination'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dashboard empty search state', async () => {
|
||||||
|
const mockLocation = {
|
||||||
|
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||||
|
search: `columnKey=createdAt&order=descend&page=1&search=someRandomString`,
|
||||||
|
};
|
||||||
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
const { getByText } = render(
|
||||||
|
<MemoryRouter
|
||||||
|
initialEntries={[
|
||||||
|
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DashboardProvider>
|
||||||
|
<DashboardsList />
|
||||||
|
</DashboardProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
getByText(
|
||||||
|
'No dashboards found for someRandomString. Create a new dashboard?',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dashboard empty state', async () => {
|
||||||
|
const mockLocation = {
|
||||||
|
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
|
||||||
|
search: `columnKey=createdAt&order=descend&page=1`,
|
||||||
|
};
|
||||||
|
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||||
|
server.use(
|
||||||
|
rest.get('http://localhost/api/v1/dashboards', (_, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(dashboardEmptyState)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<MemoryRouter
|
||||||
|
initialEntries={[
|
||||||
|
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DashboardProvider>
|
||||||
|
<DashboardsList />
|
||||||
|
</DashboardProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(getByText('No dashboards yet.')).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const learnMoreButton = getByTestId('learn-more');
|
||||||
|
expect(learnMoreButton).toBeInTheDocument();
|
||||||
|
fireEvent.click(learnMoreButton);
|
||||||
|
|
||||||
|
// test the correct link to be added for the dashboards empty state
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||||
|
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
|
||||||
|
'_blank',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -4,7 +4,6 @@ import { Button, Typography } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
|
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ function Configure(props: ConfigurationProps): JSX.Element {
|
|||||||
const { configuration, integrationId } = props;
|
const { configuration, integrationId } = props;
|
||||||
const [selectedConfigStep, setSelectedConfigStep] = useState(0);
|
const [selectedConfigStep, setSelectedConfigStep] = useState(0);
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const handleMenuClick = (index: number, config: any): void => {
|
const handleMenuClick = (index: number, config: any): void => {
|
||||||
setSelectedConfigStep(index);
|
setSelectedConfigStep(index);
|
||||||
logEvent('Integrations Detail Page: Configure tab', {
|
logEvent('Integrations Detail Page: Configure tab', {
|
||||||
@ -29,7 +26,7 @@ function Configure(props: ConfigurationProps): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
trackEvent(
|
logEvent(
|
||||||
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION,
|
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION,
|
||||||
{
|
{
|
||||||
integration: integrationId,
|
integration: integrationId,
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
import './IntegrationDetailPage.styles.scss';
|
import './IntegrationDetailPage.styles.scss';
|
||||||
|
|
||||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import installIntegration from 'api/Integrations/installIntegration';
|
import installIntegration from 'api/Integrations/installIntegration';
|
||||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { ArrowLeftRight, Check } from 'lucide-react';
|
import { ArrowLeftRight, Check } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -43,8 +43,6 @@ function IntegrationDetailHeader(
|
|||||||
} = props;
|
} = props;
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const showModal = (): void => {
|
const showModal = (): void => {
|
||||||
@ -137,11 +135,11 @@ function IntegrationDetailHeader(
|
|||||||
disabled={isInstallLoading}
|
disabled={isInstallLoading}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
if (connectionState === ConnectionStates.NotInstalled) {
|
if (connectionState === ConnectionStates.NotInstalled) {
|
||||||
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, {
|
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, {
|
||||||
integration: id,
|
integration: id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
trackEvent(
|
logEvent(
|
||||||
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_TEST_CONNECTION,
|
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_TEST_CONNECTION,
|
||||||
{
|
{
|
||||||
integration: id,
|
integration: id,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import './IntegrationDetailPage.styles.scss';
|
import './IntegrationDetailPage.styles.scss';
|
||||||
|
|
||||||
import { Button, Modal, Typography } from 'antd';
|
import { Button, Modal, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import unInstallIntegration from 'api/Integrations/uninstallIntegration';
|
import unInstallIntegration from 'api/Integrations/uninstallIntegration';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -30,8 +30,6 @@ function IntergrationsUninstallBar(
|
|||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: uninstallIntegration,
|
mutate: uninstallIntegration,
|
||||||
isLoading: isUninstallLoading,
|
isLoading: isUninstallLoading,
|
||||||
@ -52,7 +50,7 @@ function IntergrationsUninstallBar(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = (): void => {
|
const handleOk = (): void => {
|
||||||
trackEvent(
|
logEvent(
|
||||||
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_REMOVE_INTEGRATION,
|
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_REMOVE_INTEGRATION,
|
||||||
{
|
{
|
||||||
integration: integrationId,
|
integration: integrationId,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import './Integrations.styles.scss';
|
import './Integrations.styles.scss';
|
||||||
|
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
import logEvent from 'api/common/logEvent';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
@ -16,8 +16,6 @@ function Integrations(): JSX.Element {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const selectedIntegration = useMemo(() => urlQuery.get('integration'), [
|
const selectedIntegration = useMemo(() => urlQuery.get('integration'), [
|
||||||
urlQuery,
|
urlQuery,
|
||||||
]);
|
]);
|
||||||
@ -25,7 +23,7 @@ function Integrations(): JSX.Element {
|
|||||||
const setSelectedIntegration = useCallback(
|
const setSelectedIntegration = useCallback(
|
||||||
(integration: string | null) => {
|
(integration: string | null) => {
|
||||||
if (integration) {
|
if (integration) {
|
||||||
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, {
|
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, {
|
||||||
integration,
|
integration,
|
||||||
});
|
});
|
||||||
urlQuery.set('integration', integration);
|
urlQuery.set('integration', integration);
|
||||||
@ -35,7 +33,7 @@ function Integrations(): JSX.Element {
|
|||||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||||
history.push(generatedUrl);
|
history.push(generatedUrl);
|
||||||
},
|
},
|
||||||
[history, location.pathname, trackEvent, urlQuery],
|
[history, location.pathname, urlQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [activeDetailTab, setActiveDetailTab] = useState<string | null>(
|
const [activeDetailTab, setActiveDetailTab] = useState<string | null>(
|
||||||
@ -43,7 +41,7 @@ function Integrations(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_LIST_VISITED);
|
logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_LIST_VISITED, {});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -149,11 +149,11 @@ function SaveView(): JSX.Element {
|
|||||||
if (!logEventCalledRef.current && !isLoading) {
|
if (!logEventCalledRef.current && !isLoading) {
|
||||||
if (sourcepage === DataSource.TRACES) {
|
if (sourcepage === DataSource.TRACES) {
|
||||||
logEvent('Traces Views: Views visited', {
|
logEvent('Traces Views: Views visited', {
|
||||||
number: viewsData?.data.data.length,
|
number: viewsData?.data?.data?.length,
|
||||||
});
|
});
|
||||||
} else if (sourcepage === DataSource.LOGS) {
|
} else if (sourcepage === DataSource.LOGS) {
|
||||||
logEvent('Logs Views: Views visited', {
|
logEvent('Logs Views: Views visited', {
|
||||||
number: viewsData?.data.data.length,
|
number: viewsData?.data?.data?.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
logEventCalledRef.current = true;
|
logEventCalledRef.current = true;
|
||||||
|
@ -26,7 +26,10 @@ export const getRoutes = (
|
|||||||
settings.push(...organizationSettings(t));
|
settings.push(...organizationSettings(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGatewayEnabled && userRole === USER_ROLES.ADMIN) {
|
if (
|
||||||
|
isGatewayEnabled &&
|
||||||
|
(userRole === USER_ROLES.ADMIN || userRole === USER_ROLES.EDITOR)
|
||||||
|
) {
|
||||||
settings.push(...multiIngestionSettings(t));
|
settings.push(...multiIngestionSettings(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Button, Form, Input, Space, Switch, Typography } from 'antd';
|
import { Button, Form, Input, Space, Switch, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import editOrg from 'api/user/editOrg';
|
import editOrg from 'api/user/editOrg';
|
||||||
import getInviteDetails from 'api/user/getInviteDetails';
|
import getInviteDetails from 'api/user/getInviteDetails';
|
||||||
import loginApi from 'api/user/login';
|
import loginApi from 'api/user/login';
|
||||||
@ -7,7 +8,6 @@ import afterLogin from 'AppRoutes/utils';
|
|||||||
import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
|
import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@ -57,7 +57,6 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
const token = params.get('token');
|
const token = params.get('token');
|
||||||
const [isDetailsDisable, setIsDetailsDisable] = useState<boolean>(false);
|
const [isDetailsDisable, setIsDetailsDisable] = useState<boolean>(false);
|
||||||
@ -88,7 +87,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
|||||||
form.setFieldValue('organizationName', responseDetails.organization);
|
form.setFieldValue('organizationName', responseDetails.organization);
|
||||||
setIsDetailsDisable(true);
|
setIsDetailsDisable(true);
|
||||||
|
|
||||||
trackEvent('Account Creation Page Visited', {
|
logEvent('Account Creation Page Visited', {
|
||||||
email: responseDetails.email,
|
email: responseDetails.email,
|
||||||
name: responseDetails.name,
|
name: responseDetails.name,
|
||||||
company_name: responseDetails.organization,
|
company_name: responseDetails.organization,
|
||||||
@ -241,7 +240,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
if (!isPasswordValid(values.password)) {
|
if (!isPasswordValid(values.password)) {
|
||||||
trackEvent('Account Creation Page - Invalid Password', {
|
logEvent('Account Creation Page - Invalid Password', {
|
||||||
email: values.email,
|
email: values.email,
|
||||||
name: values.firstName,
|
name: values.firstName,
|
||||||
});
|
});
|
||||||
@ -253,7 +252,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
|
|||||||
if (isPreferenceVisible) {
|
if (isPreferenceVisible) {
|
||||||
await commonHandler(values, onAdminAfterLogin);
|
await commonHandler(values, onAdminAfterLogin);
|
||||||
} else {
|
} else {
|
||||||
trackEvent('Account Created Successfully', {
|
logEvent('Account Created Successfully', {
|
||||||
email: values.email,
|
email: values.email,
|
||||||
name: values.firstName,
|
name: values.firstName,
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import './Support.styles.scss';
|
import './Support.styles.scss';
|
||||||
|
|
||||||
import { Button, Card, Typography } from 'antd';
|
import { Button, Card, Typography } from 'antd';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
import logEvent from 'api/common/logEvent';
|
||||||
import {
|
import {
|
||||||
Book,
|
Book,
|
||||||
Cable,
|
Cable,
|
||||||
@ -85,7 +85,6 @@ const supportChannels = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function Support(): JSX.Element {
|
export default function Support(): JSX.Element {
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const handleChannelWithRedirects = (url: string): void => {
|
const handleChannelWithRedirects = (url: string): void => {
|
||||||
@ -97,7 +96,7 @@ export default function Support(): JSX.Element {
|
|||||||
const histroyState = history?.location?.state as any;
|
const histroyState = history?.location?.state as any;
|
||||||
|
|
||||||
if (histroyState && histroyState?.from) {
|
if (histroyState && histroyState?.from) {
|
||||||
trackEvent(`Support : From URL : ${histroyState.from}`);
|
logEvent(`Support : From URL : ${histroyState.from}`, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +128,7 @@ export default function Support(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChannelClick = (channel: Channel): void => {
|
const handleChannelClick = (channel: Channel): void => {
|
||||||
trackEvent(`Support : ${channel.name}`);
|
logEvent(`Support : ${channel.name}`, {});
|
||||||
|
|
||||||
switch (channel.key) {
|
switch (channel.key) {
|
||||||
case channelsMap.documentation:
|
case channelsMap.documentation:
|
||||||
|
@ -109,6 +109,7 @@ export function DurationSection(props: DurationProps): JSX.Element {
|
|||||||
className="min-max-input"
|
className="min-max-input"
|
||||||
onChange={onChangeMinHandler}
|
onChange={onChangeMinHandler}
|
||||||
value={preMin}
|
value={preMin}
|
||||||
|
data-testid="min-input"
|
||||||
addonAfter="ms"
|
addonAfter="ms"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@ -118,6 +119,7 @@ export function DurationSection(props: DurationProps): JSX.Element {
|
|||||||
className="min-max-input"
|
className="min-max-input"
|
||||||
onChange={onChangeMaxHandler}
|
onChange={onChangeMaxHandler}
|
||||||
value={preMax}
|
value={preMax}
|
||||||
|
data-testid="max-input"
|
||||||
addonAfter="ms"
|
addonAfter="ms"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -224,13 +224,18 @@ export function Filter(props: FilterProps): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
onClick={(): void => handleRun({ resetAll: true })}
|
onClick={(): void => handleRun({ resetAll: true })}
|
||||||
className="sync-icon"
|
className="sync-icon"
|
||||||
|
data-testid="reset-filters"
|
||||||
>
|
>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Tooltip title="Collapse" placement="right">
|
<Tooltip title="Collapse" placement="right">
|
||||||
<Button onClick={(): void => setOpen(false)} className="arrow-icon">
|
<Button
|
||||||
|
onClick={(): void => setOpen(false)}
|
||||||
|
className="arrow-icon"
|
||||||
|
data-testid="toggle-filter-panel"
|
||||||
|
>
|
||||||
<VerticalAlignTopOutlined rotate={270} />
|
<VerticalAlignTopOutlined rotate={270} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -64,7 +64,7 @@ export function Section(props: SectionProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Divider plain className="divider" />
|
<Divider plain className="divider" />
|
||||||
<div className="section-body-header">
|
<div className="section-body-header" data-testid={`collapse-${panelName}`}>
|
||||||
<Collapse
|
<Collapse
|
||||||
bordered={false}
|
bordered={false}
|
||||||
className="collapseContainer"
|
className="collapseContainer"
|
||||||
@ -96,7 +96,11 @@ export function Section(props: SectionProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Button type="link" onClick={onClearHandler}>
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={onClearHandler}
|
||||||
|
data-testid={`collapse-${panelName}-clearBtn`}
|
||||||
|
>
|
||||||
Clear All
|
Clear All
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -145,6 +145,7 @@ export function SectionBody(props: SectionBodyProps): JSX.Element {
|
|||||||
key={`${type}-${item}`}
|
key={`${type}-${item}`}
|
||||||
onChange={(e): void => onCheckHandler(e, item)}
|
onChange={(e): void => onCheckHandler(e, item)}
|
||||||
checked={checkboxMatcher(item)}
|
checked={checkboxMatcher(item)}
|
||||||
|
data-testid={`${type}-${item}`}
|
||||||
>
|
>
|
||||||
<div className="checkbox-label">
|
<div className="checkbox-label">
|
||||||
<div className={labelClassname(item)} />
|
<div className={labelClassname(item)} />
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
/* eslint-disable sonarjs/no-duplicate-string */
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
/* eslint-disable no-restricted-syntax */
|
/* eslint-disable no-restricted-syntax */
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import {
|
import {
|
||||||
initialQueriesMap,
|
initialQueriesMap,
|
||||||
initialQueryBuilderFormValues,
|
initialQueryBuilderFormValues,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
import { render } from 'tests/test-utils';
|
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||||
|
import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import TracesExplorer from '..';
|
||||||
import { Filter } from '../Filter/Filter';
|
import { Filter } from '../Filter/Filter';
|
||||||
import { AllTraceFilterKeyValue } from '../Filter/filterUtils';
|
import { AllTraceFilterKeyValue } from '../Filter/filterUtils';
|
||||||
|
|
||||||
@ -37,6 +40,48 @@ jest.mock('uplot', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'container/TopNav/DateTimeSelectionV2/index.tsx',
|
||||||
|
() =>
|
||||||
|
function MockDateTimeSelection(): JSX.Element {
|
||||||
|
return <div>MockDateTimeSelection</div>;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function checkIfSectionIsOpen(
|
||||||
|
getByTestId: (testId: string) => HTMLElement,
|
||||||
|
panelName: string,
|
||||||
|
): void {
|
||||||
|
const section = getByTestId(`collapse-${panelName}`);
|
||||||
|
expect(section.querySelector('.ant-collapse-item-active')).not.toBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfSectionIsNotOpen(
|
||||||
|
getByTestId: (testId: string) => HTMLElement,
|
||||||
|
panelName: string,
|
||||||
|
): void {
|
||||||
|
const section = getByTestId(`collapse-${panelName}`);
|
||||||
|
expect(section.querySelector('.ant-collapse-item-active')).toBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOpenSections = ['hasError', 'durationNano', 'serviceName'];
|
||||||
|
|
||||||
|
const defaultClosedSections = Object.keys(AllTraceFilterKeyValue).filter(
|
||||||
|
(section) =>
|
||||||
|
![...defaultOpenSections, 'durationNanoMin', 'durationNanoMax'].includes(
|
||||||
|
section,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
async function checkForSectionContent(values: string[]): Promise<void> {
|
||||||
|
for (const val of values) {
|
||||||
|
const sectionContent = await screen.findByText(val);
|
||||||
|
await waitFor(() => expect(sectionContent).toBeInTheDocument());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectWithQueryBuilderData = jest.fn();
|
||||||
|
|
||||||
const compositeQuery: Query = {
|
const compositeQuery: Query = {
|
||||||
...initialQueriesMap.traces,
|
...initialQueriesMap.traces,
|
||||||
builder: {
|
builder: {
|
||||||
@ -81,6 +126,157 @@ const compositeQuery: Query = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('TracesExplorer - ', () => {
|
describe('TracesExplorer - ', () => {
|
||||||
|
// Initial filter panel rendering
|
||||||
|
// Test the initial state like which filters section are opened, default state of duration slider, etc.
|
||||||
|
it('should render the Trace filter', async () => {
|
||||||
|
const { getByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
|
||||||
|
|
||||||
|
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||||
|
expect(getByText(filter)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check default state of duration slider
|
||||||
|
const minDuration = getByTestId('min-input') as HTMLInputElement;
|
||||||
|
const maxDuration = getByTestId('max-input') as HTMLInputElement;
|
||||||
|
expect(minDuration).toHaveValue(null);
|
||||||
|
expect(minDuration).toHaveProperty('placeholder', '0');
|
||||||
|
expect(maxDuration).toHaveValue(null);
|
||||||
|
expect(maxDuration).toHaveProperty('placeholder', '100000000');
|
||||||
|
|
||||||
|
// Check which all filter section are opened by default
|
||||||
|
defaultOpenSections.forEach((section) =>
|
||||||
|
checkIfSectionIsOpen(getByTestId, section),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check which all filter section are closed by default
|
||||||
|
defaultClosedSections.forEach((section) =>
|
||||||
|
checkIfSectionIsNotOpen(getByTestId, section),
|
||||||
|
);
|
||||||
|
|
||||||
|
// check for the status section content
|
||||||
|
await checkForSectionContent(['Ok', 'Error']);
|
||||||
|
|
||||||
|
// check for the service name section content from API response
|
||||||
|
await checkForSectionContent([
|
||||||
|
'customer',
|
||||||
|
'demo-app',
|
||||||
|
'driver',
|
||||||
|
'frontend',
|
||||||
|
'mysql',
|
||||||
|
'redis',
|
||||||
|
'route',
|
||||||
|
'go-grpc-otel-server',
|
||||||
|
'test',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// test the filter panel actions like opening and closing the sections, etc.
|
||||||
|
it('filter panel actions', async () => {
|
||||||
|
const { getByTestId } = render(<Filter setOpen={jest.fn()} />);
|
||||||
|
|
||||||
|
// Check if the section is closed
|
||||||
|
checkIfSectionIsNotOpen(getByTestId, 'name');
|
||||||
|
// Open the section
|
||||||
|
const name = getByTestId('collapse-name');
|
||||||
|
expect(name).toBeInTheDocument();
|
||||||
|
|
||||||
|
userEvent.click(within(name).getByText(AllTraceFilterKeyValue.name));
|
||||||
|
await waitFor(() => checkIfSectionIsOpen(getByTestId, 'name'));
|
||||||
|
|
||||||
|
await checkForSectionContent([
|
||||||
|
'HTTP GET',
|
||||||
|
'HTTP GET /customer',
|
||||||
|
'HTTP GET /dispatch',
|
||||||
|
'HTTP GET /route',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Close the section
|
||||||
|
userEvent.click(within(name).getByText(AllTraceFilterKeyValue.name));
|
||||||
|
await waitFor(() => checkIfSectionIsNotOpen(getByTestId, 'name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checking filters should update the query', async () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<QueryBuilderContext.Provider
|
||||||
|
value={
|
||||||
|
{
|
||||||
|
currentQuery: {
|
||||||
|
...initialQueriesMap.traces,
|
||||||
|
builder: {
|
||||||
|
...initialQueriesMap.traces.builder,
|
||||||
|
queryData: [initialQueryBuilderFormValues],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Filter setOpen={jest.fn()} />
|
||||||
|
</QueryBuilderContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const okCheckbox = getByText('Ok');
|
||||||
|
fireEvent.click(okCheckbox);
|
||||||
|
expect(
|
||||||
|
redirectWithQueryBuilderData.mock.calls[
|
||||||
|
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||||
|
][0].builder.queryData[0].filters.items,
|
||||||
|
).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: {
|
||||||
|
id: expect.any(String),
|
||||||
|
key: 'hasError',
|
||||||
|
type: 'tag',
|
||||||
|
dataType: 'bool',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['false'],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the query is updated when the error checkbox is clicked
|
||||||
|
const errorCheckbox = getByText('Error');
|
||||||
|
fireEvent.click(errorCheckbox);
|
||||||
|
expect(
|
||||||
|
redirectWithQueryBuilderData.mock.calls[
|
||||||
|
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||||
|
][0].builder.queryData[0].filters.items,
|
||||||
|
).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: {
|
||||||
|
id: expect.any(String),
|
||||||
|
key: 'hasError',
|
||||||
|
type: 'tag',
|
||||||
|
dataType: 'bool',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['false', 'true'],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the trace filter with the given query', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(compositeQueryHook, 'useGetCompositeQueryParam')
|
||||||
|
.mockReturnValue(compositeQuery);
|
||||||
|
|
||||||
|
const { findByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
|
||||||
|
|
||||||
|
// check if the default query is applied - composite query has filters - serviceName : demo-app and name : HTTP GET /customer
|
||||||
|
expect(await findByText('demo-app')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('serviceName-demo-app')).toBeChecked();
|
||||||
|
expect(await findByText('HTTP GET /customer')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('name-HTTP GET /customer')).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
it('test edge cases of undefined filters', async () => {
|
it('test edge cases of undefined filters', async () => {
|
||||||
jest.spyOn(compositeQueryHook, 'useGetCompositeQueryParam').mockReturnValue({
|
jest.spyOn(compositeQueryHook, 'useGetCompositeQueryParam').mockReturnValue({
|
||||||
...compositeQuery,
|
...compositeQuery,
|
||||||
@ -98,7 +294,6 @@ describe('TracesExplorer - ', () => {
|
|||||||
|
|
||||||
const { getByText } = render(<Filter setOpen={jest.fn()} />);
|
const { getByText } = render(<Filter setOpen={jest.fn()} />);
|
||||||
|
|
||||||
// we should have all the filters
|
|
||||||
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||||
expect(getByText(filter)).toBeInTheDocument();
|
expect(getByText(filter)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -124,9 +319,141 @@ describe('TracesExplorer - ', () => {
|
|||||||
|
|
||||||
const { getByText } = render(<Filter setOpen={jest.fn()} />);
|
const { getByText } = render(<Filter setOpen={jest.fn()} />);
|
||||||
|
|
||||||
// we should have all the filters
|
|
||||||
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||||
expect(getByText(filter)).toBeInTheDocument();
|
expect(getByText(filter)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should clear filter on clear & reset button click', async () => {
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<QueryBuilderContext.Provider
|
||||||
|
value={
|
||||||
|
{
|
||||||
|
currentQuery: {
|
||||||
|
...initialQueriesMap.traces,
|
||||||
|
builder: {
|
||||||
|
...initialQueriesMap.traces.builder,
|
||||||
|
queryData: [initialQueryBuilderFormValues],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Filter setOpen={jest.fn()} />
|
||||||
|
</QueryBuilderContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// check for the status section content
|
||||||
|
await checkForSectionContent(['Ok', 'Error']);
|
||||||
|
|
||||||
|
// check for the service name section content from API response
|
||||||
|
await checkForSectionContent([
|
||||||
|
'customer',
|
||||||
|
'demo-app',
|
||||||
|
'driver',
|
||||||
|
'frontend',
|
||||||
|
'mysql',
|
||||||
|
'redis',
|
||||||
|
'route',
|
||||||
|
'go-grpc-otel-server',
|
||||||
|
'test',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const okCheckbox = getByText('Ok');
|
||||||
|
fireEvent.click(okCheckbox);
|
||||||
|
|
||||||
|
const frontendCheckbox = getByText('frontend');
|
||||||
|
fireEvent.click(frontendCheckbox);
|
||||||
|
|
||||||
|
// check if checked and present in query
|
||||||
|
expect(
|
||||||
|
redirectWithQueryBuilderData.mock.calls[
|
||||||
|
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||||
|
][0].builder.queryData[0].filters.items,
|
||||||
|
).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: {
|
||||||
|
id: expect.any(String),
|
||||||
|
key: 'hasError',
|
||||||
|
type: 'tag',
|
||||||
|
dataType: 'bool',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['false'],
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
key: {
|
||||||
|
key: 'serviceName',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: expect.any(String),
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['frontend'],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearButton = getByTestId('collapse-serviceName-clearBtn');
|
||||||
|
expect(clearButton).toBeInTheDocument();
|
||||||
|
fireEvent.click(clearButton);
|
||||||
|
|
||||||
|
// check if cleared and not present in query
|
||||||
|
expect(
|
||||||
|
redirectWithQueryBuilderData.mock.calls[
|
||||||
|
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||||
|
][0].builder.queryData[0].filters.items,
|
||||||
|
).not.toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: {
|
||||||
|
key: 'serviceName',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: expect.any(String),
|
||||||
|
},
|
||||||
|
op: 'in',
|
||||||
|
value: ['frontend'],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// check if reset button is present
|
||||||
|
const resetButton = getByTestId('reset-filters');
|
||||||
|
expect(resetButton).toBeInTheDocument();
|
||||||
|
fireEvent.click(resetButton);
|
||||||
|
|
||||||
|
// check if reset id done
|
||||||
|
expect(
|
||||||
|
redirectWithQueryBuilderData.mock.calls[
|
||||||
|
redirectWithQueryBuilderData.mock.calls.length - 1
|
||||||
|
][0].builder.queryData[0].filters.items,
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filter panel should collapse & uncollapsed', async () => {
|
||||||
|
const { getByText, getByTestId } = render(<TracesExplorer />);
|
||||||
|
|
||||||
|
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
|
||||||
|
expect(getByText(filter)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter panel should collapse
|
||||||
|
const collapseButton = getByTestId('toggle-filter-panel');
|
||||||
|
expect(collapseButton).toBeInTheDocument();
|
||||||
|
fireEvent.click(collapseButton);
|
||||||
|
|
||||||
|
// uncollapse btn should be present
|
||||||
|
expect(
|
||||||
|
await screen.findByTestId('filter-uncollapse-btn'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -251,6 +251,7 @@ function TracesExplorer(): JSX.Element {
|
|||||||
<Button
|
<Button
|
||||||
onClick={(): void => setOpen(!isOpen)}
|
onClick={(): void => setOpen(!isOpen)}
|
||||||
className="filter-outlined-btn"
|
className="filter-outlined-btn"
|
||||||
|
data-testid="filter-uncollapse-btn"
|
||||||
>
|
>
|
||||||
<FilterOutlined />
|
<FilterOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -8,10 +8,10 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Button, Card, Skeleton, Typography } from 'antd';
|
import { Button, Card, Skeleton, Typography } from 'antd';
|
||||||
import updateCreditCardApi from 'api/billing/checkout';
|
import updateCreditCardApi from 'api/billing/checkout';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
|
||||||
import useLicense from 'hooks/useLicense';
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@ -27,7 +27,6 @@ export default function WorkspaceBlocked(): JSX.Element {
|
|||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const isAdmin = role === 'ADMIN';
|
const isAdmin = role === 'ADMIN';
|
||||||
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
||||||
const { trackEvent } = useAnalytics();
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
@ -74,7 +73,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateCreditCard = useCallback(async () => {
|
const handleUpdateCreditCard = useCallback(async () => {
|
||||||
trackEvent('Workspace Blocked: User Clicked Update Credit Card');
|
logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
|
||||||
|
|
||||||
updateCreditCard({
|
updateCreditCard({
|
||||||
licenseKey: activeLicense?.key || '',
|
licenseKey: activeLicense?.key || '',
|
||||||
@ -85,7 +84,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
|||||||
}, [activeLicense?.key, updateCreditCard]);
|
}, [activeLicense?.key, updateCreditCard]);
|
||||||
|
|
||||||
const handleExtendTrial = (): void => {
|
const handleExtendTrial = (): void => {
|
||||||
trackEvent('Workspace Blocked: User Clicked Extend Trial');
|
logEvent('Workspace Blocked: User Clicked Extend Trial', {});
|
||||||
|
|
||||||
notifications.info({
|
notifications.info({
|
||||||
message: 'Extend Trial',
|
message: 'Extend Trial',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import getDashboard from 'api/dashboard/get';
|
import getDashboard from 'api/dashboard/get';
|
||||||
import lockDashboardApi from 'api/dashboard/lockDashboard';
|
import lockDashboardApi from 'api/dashboard/lockDashboard';
|
||||||
@ -11,6 +12,7 @@ import useAxiosError from 'hooks/useAxiosError';
|
|||||||
import useTabVisibility from 'hooks/useTabFocus';
|
import useTabVisibility from 'hooks/useTabFocus';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||||
|
import history from 'lib/history';
|
||||||
import { defaultTo } from 'lodash-es';
|
import { defaultTo } from 'lodash-es';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import isUndefined from 'lodash-es/isUndefined';
|
import isUndefined from 'lodash-es/isUndefined';
|
||||||
@ -38,7 +40,7 @@ import AppReducer from 'types/reducer/app';
|
|||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
import { IDashboardContext } from './types';
|
import { DashboardSortOrder, IDashboardContext } from './types';
|
||||||
import { sortLayout } from './util';
|
import { sortLayout } from './util';
|
||||||
|
|
||||||
const DashboardContext = createContext<IDashboardContext>({
|
const DashboardContext = createContext<IDashboardContext>({
|
||||||
@ -52,7 +54,12 @@ const DashboardContext = createContext<IDashboardContext>({
|
|||||||
layouts: [],
|
layouts: [],
|
||||||
panelMap: {},
|
panelMap: {},
|
||||||
setPanelMap: () => {},
|
setPanelMap: () => {},
|
||||||
listSortOrder: { columnKey: 'createdAt', order: 'descend', pagination: '1' },
|
listSortOrder: {
|
||||||
|
columnKey: 'createdAt',
|
||||||
|
order: 'descend',
|
||||||
|
pagination: '1',
|
||||||
|
search: '',
|
||||||
|
},
|
||||||
setListSortOrder: () => {},
|
setListSortOrder: () => {},
|
||||||
setLayouts: () => {},
|
setLayouts: () => {},
|
||||||
setSelectedDashboard: () => {},
|
setSelectedDashboard: () => {},
|
||||||
@ -68,6 +75,7 @@ interface Props {
|
|||||||
dashboardId: string;
|
dashboardId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export function DashboardProvider({
|
export function DashboardProvider({
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren): JSX.Element {
|
}: PropsWithChildren): JSX.Element {
|
||||||
@ -82,17 +90,50 @@ export function DashboardProvider({
|
|||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const params = useUrlQuery();
|
const isDashboardListPage = useRouteMatch<Props>({
|
||||||
const orderColumnParam = params.get('columnKey');
|
path: ROUTES.ALL_DASHBOARD,
|
||||||
const orderQueryParam = params.get('order');
|
exact: true,
|
||||||
const paginationParam = params.get('page');
|
|
||||||
|
|
||||||
const [listSortOrder, setListSortOrder] = useState({
|
|
||||||
columnKey: orderColumnParam || 'updatedAt',
|
|
||||||
order: orderQueryParam || 'descend',
|
|
||||||
pagination: paginationParam || '1',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// added extra checks here in case wrong values appear use the default values rather than empty dashboards
|
||||||
|
const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
|
||||||
|
|
||||||
|
const supportedOrderKeys = ['ascend', 'descend'];
|
||||||
|
|
||||||
|
const params = useUrlQuery();
|
||||||
|
// since the dashboard provider is wrapped at the very top of the application hence it initialises these values from other pages as well.
|
||||||
|
// pick the below params from URL only if the user is on the dashboards list page.
|
||||||
|
const orderColumnParam = isDashboardListPage && params.get('columnKey');
|
||||||
|
const orderQueryParam = isDashboardListPage && params.get('order');
|
||||||
|
const paginationParam = isDashboardListPage && params.get('page');
|
||||||
|
const searchParam = isDashboardListPage && params.get('search');
|
||||||
|
|
||||||
|
const [listSortOrder, setListOrder] = useState({
|
||||||
|
columnKey: orderColumnParam
|
||||||
|
? supportedOrderColumnKeys.includes(orderColumnParam)
|
||||||
|
? orderColumnParam
|
||||||
|
: 'updatedAt'
|
||||||
|
: 'updatedAt',
|
||||||
|
order: orderQueryParam
|
||||||
|
? supportedOrderKeys.includes(orderQueryParam)
|
||||||
|
? orderQueryParam
|
||||||
|
: 'descend'
|
||||||
|
: 'descend',
|
||||||
|
pagination: paginationParam || '1',
|
||||||
|
search: searchParam || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
function setListSortOrder(sortOrder: DashboardSortOrder): void {
|
||||||
|
if (!isEqual(sortOrder, listSortOrder)) {
|
||||||
|
setListOrder(sortOrder);
|
||||||
|
}
|
||||||
|
params.set('columnKey', sortOrder.columnKey as string);
|
||||||
|
params.set('order', sortOrder.order as string);
|
||||||
|
params.set('page', sortOrder.pagination || '1');
|
||||||
|
params.set('search', sortOrder.search || '');
|
||||||
|
history.replace({ search: params.toString() });
|
||||||
|
}
|
||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
export interface DashboardSortOrder {
|
||||||
|
columnKey: string;
|
||||||
|
order: string;
|
||||||
|
pagination: string;
|
||||||
|
search: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IDashboardContext {
|
export interface IDashboardContext {
|
||||||
isDashboardSliderOpen: boolean;
|
isDashboardSliderOpen: boolean;
|
||||||
isDashboardLocked: boolean;
|
isDashboardLocked: boolean;
|
||||||
@ -15,18 +21,8 @@ export interface IDashboardContext {
|
|||||||
layouts: Layout[];
|
layouts: Layout[];
|
||||||
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
|
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
|
||||||
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||||
listSortOrder: {
|
listSortOrder: DashboardSortOrder;
|
||||||
columnKey: string;
|
setListSortOrder: (sortOrder: DashboardSortOrder) => void;
|
||||||
order: string;
|
|
||||||
pagination: string;
|
|
||||||
};
|
|
||||||
setListSortOrder: Dispatch<
|
|
||||||
SetStateAction<{
|
|
||||||
columnKey: string;
|
|
||||||
order: string;
|
|
||||||
pagination: string;
|
|
||||||
}>
|
|
||||||
>;
|
|
||||||
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
|
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
|
||||||
setSelectedDashboard: React.Dispatch<
|
setSelectedDashboard: React.Dispatch<
|
||||||
React.SetStateAction<Dashboard | undefined>
|
React.SetStateAction<Dashboard | undefined>
|
||||||
|
@ -27,7 +27,7 @@ import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
|||||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||||
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
||||||
import { replaceIncorrectObjectFields } from 'lib/replaceIncorrectObjectFields';
|
import { replaceIncorrectObjectFields } from 'lib/replaceIncorrectObjectFields';
|
||||||
import { get, merge, set } from 'lodash-es';
|
import { cloneDeep, get, merge, set } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
@ -532,7 +532,7 @@ export function QueryBuilderProvider({
|
|||||||
if (!panelType) {
|
if (!panelType) {
|
||||||
return newQueryItem;
|
return newQueryItem;
|
||||||
}
|
}
|
||||||
const queryItem = item as IBuilderQuery;
|
const queryItem = cloneDeep(item) as IBuilderQuery;
|
||||||
const propsRequired =
|
const propsRequired =
|
||||||
panelTypeDataSourceFormValuesMap[panelType as keyof PartialPanelTypes]?.[
|
panelTypeDataSourceFormValuesMap[panelType as keyof PartialPanelTypes]?.[
|
||||||
queryItem.dataSource
|
queryItem.dataSource
|
||||||
@ -829,7 +829,7 @@ export function QueryBuilderProvider({
|
|||||||
unit,
|
unit,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
[setCurrentQuery],
|
[setCurrentQuery, setSupersetQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
const query: Query = useMemo(
|
const query: Query = useMemo(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import ROUTES from 'constants/routes';
|
||||||
import { parseQuery } from 'lib/logql';
|
import { parseQuery } from 'lib/logql';
|
||||||
import { OrderPreferenceItems } from 'pages/Logs/config';
|
import { OrderPreferenceItems } from 'pages/Logs/config';
|
||||||
import {
|
import {
|
||||||
@ -29,6 +30,30 @@ import {
|
|||||||
} from 'types/actions/logs';
|
} from 'types/actions/logs';
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
const supportedLogsOrder = [
|
||||||
|
OrderPreferenceItems.ASC,
|
||||||
|
OrderPreferenceItems.DESC,
|
||||||
|
];
|
||||||
|
|
||||||
|
function getLogsOrder(): OrderPreferenceItems {
|
||||||
|
// set the value of order from the URL only when order query param is present and the user is landing on the old logs explorer page
|
||||||
|
if (window.location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||||
|
const orderParam = new URLSearchParams(window.location.search).get('order');
|
||||||
|
|
||||||
|
if (orderParam) {
|
||||||
|
// check if the order passed is supported else pass the default order
|
||||||
|
if (supportedLogsOrder.includes(orderParam as OrderPreferenceItems)) {
|
||||||
|
return orderParam as OrderPreferenceItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OrderPreferenceItems.DESC;
|
||||||
|
}
|
||||||
|
return OrderPreferenceItems.DESC;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OrderPreferenceItems.DESC;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: ILogsReducer = {
|
const initialState: ILogsReducer = {
|
||||||
fields: {
|
fields: {
|
||||||
interesting: [],
|
interesting: [],
|
||||||
@ -51,10 +76,7 @@ const initialState: ILogsReducer = {
|
|||||||
liveTailStartRange: 15,
|
liveTailStartRange: 15,
|
||||||
selectedLogId: null,
|
selectedLogId: null,
|
||||||
detailedLog: null,
|
detailedLog: null,
|
||||||
order:
|
order: getLogsOrder(),
|
||||||
(new URLSearchParams(window.location.search).get(
|
|
||||||
'order',
|
|
||||||
) as ILogsReducer['order']) ?? OrderPreferenceItems.DESC,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsReducer = (
|
export const LogsReducer = (
|
||||||
|
@ -42,6 +42,7 @@ const mockStored = (role?: string): any =>
|
|||||||
accessJwt: '',
|
accessJwt: '',
|
||||||
refreshJwt: '',
|
refreshJwt: '',
|
||||||
},
|
},
|
||||||
|
isLoggedIn: true,
|
||||||
org: [
|
org: [
|
||||||
{
|
{
|
||||||
createdAt: 0,
|
createdAt: 0,
|
||||||
|
2
go.mod
2
go.mod
@ -6,7 +6,7 @@ require (
|
|||||||
github.com/ClickHouse/clickhouse-go/v2 v2.20.0
|
github.com/ClickHouse/clickhouse-go/v2 v2.20.0
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||||
github.com/SigNoz/signoz-otel-collector v0.102.2
|
github.com/SigNoz/signoz-otel-collector v0.102.3
|
||||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
|
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
|
||||||
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
|
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
|
||||||
github.com/antonmedv/expr v1.15.3
|
github.com/antonmedv/expr v1.15.3
|
||||||
|
4
go.sum
4
go.sum
@ -64,8 +64,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
|
|||||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||||
github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg=
|
github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg=
|
||||||
github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I=
|
github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I=
|
||||||
github.com/SigNoz/signoz-otel-collector v0.102.2 h1:SmjsBZjMjTVVpuOlfJXlsDJQbdefQP/9Wz3CyzSuZuU=
|
github.com/SigNoz/signoz-otel-collector v0.102.3 h1:q6iS5kqqwopwC2pS2UvYL3IiJMP75UdyK6d+rculXn4=
|
||||||
github.com/SigNoz/signoz-otel-collector v0.102.2/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0=
|
github.com/SigNoz/signoz-otel-collector v0.102.3/go.mod h1:61WqwhnrtFjwj1FyfDYMXjxFx8gWgKok1Xy1C6LbjWo=
|
||||||
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
|
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
|
||||||
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
|
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
|
||||||
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=
|
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=
|
||||||
|
@ -706,21 +706,25 @@ func (r *ClickHouseReader) GetServicesList(ctx context.Context) (*[]string, erro
|
|||||||
return &services, nil
|
return &services, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig *model.SkipConfig, start, end time.Time) (*map[string][]string, *map[string][]string, *model.ApiError) {
|
func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig *model.SkipConfig, start, end time.Time, services []string) (*map[string][]string, *model.ApiError) {
|
||||||
|
|
||||||
start = start.In(time.UTC)
|
start = start.In(time.UTC)
|
||||||
|
|
||||||
// The `top_level_operations` that have `time` >= start
|
// The `top_level_operations` that have `time` >= start
|
||||||
operations := map[string][]string{}
|
operations := map[string][]string{}
|
||||||
// All top level operations for a service
|
// We can't use the `end` because the `top_level_operations` table has the most recent instances of the operations
|
||||||
allOperations := map[string][]string{}
|
// We can only use the `start` time to filter the operations
|
||||||
query := fmt.Sprintf(`SELECT DISTINCT name, serviceName, time FROM %s.%s`, r.TraceDB, r.topLevelOperationsTable)
|
query := fmt.Sprintf(`SELECT name, serviceName, max(time) as ts FROM %s.%s WHERE time >= @start`, r.TraceDB, r.topLevelOperationsTable)
|
||||||
|
if len(services) > 0 {
|
||||||
|
query += ` AND serviceName IN @services`
|
||||||
|
}
|
||||||
|
query += ` GROUP BY name, serviceName ORDER BY ts DESC LIMIT 5000`
|
||||||
|
|
||||||
rows, err := r.db.Query(ctx, query)
|
rows, err := r.db.Query(ctx, query, clickhouse.Named("start", start), clickhouse.Named("services", services))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("Error in processing sql query", zap.Error(err))
|
zap.L().Error("Error in processing sql query", zap.Error(err))
|
||||||
return nil, nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")}
|
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@ -728,25 +732,17 @@ func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig
|
|||||||
var name, serviceName string
|
var name, serviceName string
|
||||||
var t time.Time
|
var t time.Time
|
||||||
if err := rows.Scan(&name, &serviceName, &t); err != nil {
|
if err := rows.Scan(&name, &serviceName, &t); err != nil {
|
||||||
return nil, nil, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error in reading data")}
|
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error in reading data")}
|
||||||
}
|
}
|
||||||
if _, ok := operations[serviceName]; !ok {
|
if _, ok := operations[serviceName]; !ok {
|
||||||
operations[serviceName] = []string{}
|
operations[serviceName] = []string{"overflow_operation"}
|
||||||
}
|
|
||||||
if _, ok := allOperations[serviceName]; !ok {
|
|
||||||
allOperations[serviceName] = []string{}
|
|
||||||
}
|
}
|
||||||
if skipConfig.ShouldSkip(serviceName, name) {
|
if skipConfig.ShouldSkip(serviceName, name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
allOperations[serviceName] = append(allOperations[serviceName], name)
|
|
||||||
// We can't use the `end` because the `top_level_operations` table has the most recent instances of the operations
|
|
||||||
// We can only use the `start` time to filter the operations
|
|
||||||
if t.After(start) {
|
|
||||||
operations[serviceName] = append(operations[serviceName], name)
|
operations[serviceName] = append(operations[serviceName], name)
|
||||||
}
|
}
|
||||||
}
|
return &operations, nil
|
||||||
return &operations, &allOperations, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.GetServicesParams, skipConfig *model.SkipConfig) (*[]model.ServiceItem, *model.ApiError) {
|
func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.GetServicesParams, skipConfig *model.SkipConfig) (*[]model.ServiceItem, *model.ApiError) {
|
||||||
@ -755,7 +751,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
|
|||||||
return nil, &model.ApiError{Typ: model.ErrorExec, Err: ErrNoIndexTable}
|
return nil, &model.ApiError{Typ: model.ErrorExec, Err: ErrNoIndexTable}
|
||||||
}
|
}
|
||||||
|
|
||||||
topLevelOps, allTopLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End)
|
topLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End, nil)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
@ -779,7 +775,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
|
|||||||
// the top level operations are high, we want to warn to let user know the issue
|
// the top level operations are high, we want to warn to let user know the issue
|
||||||
// with the instrumentation
|
// with the instrumentation
|
||||||
serviceItem.DataWarning = model.DataWarning{
|
serviceItem.DataWarning = model.DataWarning{
|
||||||
TopLevelOps: (*allTopLevelOps)[svc],
|
TopLevelOps: (*topLevelOps)[svc],
|
||||||
}
|
}
|
||||||
|
|
||||||
// default max_query_size = 262144
|
// default max_query_size = 262144
|
||||||
@ -868,7 +864,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
|
|||||||
|
|
||||||
func (r *ClickHouseReader) GetServiceOverview(ctx context.Context, queryParams *model.GetServiceOverviewParams, skipConfig *model.SkipConfig) (*[]model.ServiceOverviewItem, *model.ApiError) {
|
func (r *ClickHouseReader) GetServiceOverview(ctx context.Context, queryParams *model.GetServiceOverviewParams, skipConfig *model.SkipConfig) (*[]model.ServiceOverviewItem, *model.ApiError) {
|
||||||
|
|
||||||
topLevelOps, _, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End)
|
topLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End, nil)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
return nil, apiErr
|
return nil, apiErr
|
||||||
}
|
}
|
||||||
@ -5005,3 +5001,27 @@ func (r *ClickHouseReader) LiveTailLogsV3(ctx context.Context, query string, tim
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ClickHouseReader) GetMinAndMaxTimestampForTraceID(ctx context.Context, traceID []string) (int64, int64, error) {
|
||||||
|
var minTime, maxTime time.Time
|
||||||
|
|
||||||
|
query := fmt.Sprintf("SELECT min(timestamp), max(timestamp) FROM %s.%s WHERE traceID IN ('%s')",
|
||||||
|
r.TraceDB, r.SpansTable, strings.Join(traceID, "','"))
|
||||||
|
|
||||||
|
zap.L().Debug("GetMinAndMaxTimestampForTraceID", zap.String("query", query))
|
||||||
|
|
||||||
|
err := r.db.QueryRow(ctx, query).Scan(&minTime, &maxTime)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error while executing query", zap.Error(err))
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if minTime.IsZero() || maxTime.IsZero() {
|
||||||
|
zap.L().Debug("minTime or maxTime is zero")
|
||||||
|
return 0, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Debug("GetMinAndMaxTimestampForTraceID", zap.Any("minTime", minTime), zap.Any("maxTime", maxTime))
|
||||||
|
|
||||||
|
return minTime.UnixNano(), maxTime.UnixNano(), nil
|
||||||
|
}
|
||||||
|
@ -29,12 +29,14 @@ import (
|
|||||||
logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
|
logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/metrics"
|
"go.signoz.io/signoz/pkg/query-service/app/metrics"
|
||||||
metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
|
metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/app/preferences"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/querier"
|
"go.signoz.io/signoz/pkg/query-service/app/querier"
|
||||||
querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2"
|
querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2"
|
||||||
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
|
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
|
||||||
tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3"
|
tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3"
|
||||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||||
"go.signoz.io/signoz/pkg/query-service/cache"
|
"go.signoz.io/signoz/pkg/query-service/cache"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/common"
|
||||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
"go.signoz.io/signoz/pkg/query-service/postprocess"
|
"go.signoz.io/signoz/pkg/query-service/postprocess"
|
||||||
@ -398,6 +400,22 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
|
|||||||
|
|
||||||
router.HandleFunc("/api/v1/disks", am.ViewAccess(aH.getDisks)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/disks", am.ViewAccess(aH.getDisks)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
// === Preference APIs ===
|
||||||
|
|
||||||
|
// user actions
|
||||||
|
router.HandleFunc("/api/v1/user/preferences", am.ViewAccess(aH.getAllUserPreferences)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
router.HandleFunc("/api/v1/user/preferences/{preferenceId}", am.ViewAccess(aH.getUserPreference)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
router.HandleFunc("/api/v1/user/preferences/{preferenceId}", am.ViewAccess(aH.updateUserPreference)).Methods(http.MethodPut)
|
||||||
|
|
||||||
|
// org actions
|
||||||
|
router.HandleFunc("/api/v1/org/preferences", am.AdminAccess(aH.getAllOrgPreferences)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
router.HandleFunc("/api/v1/org/preferences/{preferenceId}", am.AdminAccess(aH.getOrgPreference)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
router.HandleFunc("/api/v1/org/preferences/{preferenceId}", am.AdminAccess(aH.updateOrgPreference)).Methods(http.MethodPut)
|
||||||
|
|
||||||
// === Authentication APIs ===
|
// === Authentication APIs ===
|
||||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet)
|
||||||
@ -1329,8 +1347,44 @@ func (aH *APIHandler) getServiceOverview(w http.ResponseWriter, r *http.Request)
|
|||||||
func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Request) {
|
func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
var start, end time.Time
|
var start, end time.Time
|
||||||
|
var services []string
|
||||||
|
|
||||||
result, _, apiErr := aH.reader.GetTopLevelOperations(r.Context(), aH.skipConfig, start, end)
|
type topLevelOpsParams struct {
|
||||||
|
Service string `json:"service"`
|
||||||
|
Start string `json:"start"`
|
||||||
|
End string `json:"end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var params topLevelOpsParams
|
||||||
|
err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("Error in getting req body for get top operations API", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Service != "" {
|
||||||
|
services = []string{params.Service}
|
||||||
|
}
|
||||||
|
|
||||||
|
startEpoch := params.Start
|
||||||
|
if startEpoch != "" {
|
||||||
|
startEpochInt, err := strconv.ParseInt(startEpoch, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading start time")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start = time.Unix(0, startEpochInt)
|
||||||
|
}
|
||||||
|
endEpoch := params.End
|
||||||
|
if endEpoch != "" {
|
||||||
|
endEpochInt, err := strconv.ParseInt(endEpoch, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading end time")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end = time.Unix(0, endEpochInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, apiErr := aH.reader.GetTopLevelOperations(r.Context(), aH.skipConfig, start, end, services)
|
||||||
if apiErr != nil {
|
if apiErr != nil {
|
||||||
RespondError(w, apiErr, nil)
|
RespondError(w, apiErr, nil)
|
||||||
return
|
return
|
||||||
@ -2192,6 +2246,115 @@ func (aH *APIHandler) WriteJSON(w http.ResponseWriter, r *http.Request, response
|
|||||||
w.Write(resp)
|
w.Write(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
|
||||||
|
func (ah *APIHandler) getUserPreference(
|
||||||
|
w http.ResponseWriter, r *http.Request,
|
||||||
|
) {
|
||||||
|
preferenceId := mux.Vars(r)["preferenceId"]
|
||||||
|
user := common.GetUserFromContext(r.Context())
|
||||||
|
|
||||||
|
preference, apiErr := preferences.GetUserPreference(
|
||||||
|
r.Context(), preferenceId, user.User.OrgId, user.User.Id,
|
||||||
|
)
|
||||||
|
if apiErr != nil {
|
||||||
|
RespondError(w, apiErr, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ah.Respond(w, preference)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *APIHandler) updateUserPreference(
|
||||||
|
w http.ResponseWriter, r *http.Request,
|
||||||
|
) {
|
||||||
|
preferenceId := mux.Vars(r)["preferenceId"]
|
||||||
|
user := common.GetUserFromContext(r.Context())
|
||||||
|
req := preferences.UpdatePreference{}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
RespondError(w, model.BadRequest(err), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.Id)
|
||||||
|
if apiErr != nil {
|
||||||
|
RespondError(w, apiErr, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ah.Respond(w, preference)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *APIHandler) getAllUserPreferences(
|
||||||
|
w http.ResponseWriter, r *http.Request,
|
||||||
|
) {
|
||||||
|
user := common.GetUserFromContext(r.Context())
|
||||||
|
preference, apiErr := preferences.GetAllUserPreferences(
|
||||||
|
r.Context(), user.User.OrgId, user.User.Id,
|
||||||
|
)
|
||||||
|
if apiErr != nil {
|
||||||
|
RespondError(w, apiErr, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ah.Respond(w, preference)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *APIHandler) getOrgPreference(
|
||||||
|
w http.ResponseWriter, r *http.Request,
|
||||||
|
) {
|
||||||
|
preferenceId := mux.Vars(r)["preferenceId"]
|
||||||
|
user := common.GetUserFromContext(r.Context())
|
||||||
|
preference, apiErr := preferences.GetOrgPreference(
|
||||||
|
r.Context(), preferenceId, user.User.OrgId,
|
||||||
|
)
|
||||||
|
if apiErr != nil {
|
||||||
|
RespondError(w, apiErr, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ah.Respond(w, preference)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *APIHandler) updateOrgPreference(
|
||||||
|
w http.ResponseWriter, r *http.Request,
|
||||||
|
) {
|
||||||
|
preferenceId := mux.Vars(r)["preferenceId"]
|
||||||
|
req := preferences.UpdatePreference{}
|
||||||
|
user := common.GetUserFromContext(r.Context())
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
RespondError(w, model.BadRequest(err), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.OrgId)
|
||||||
|
if apiErr != nil {
|
||||||
|
RespondError(w, apiErr, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ah.Respond(w, preference)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *APIHandler) getAllOrgPreferences(
|
||||||
|
w http.ResponseWriter, r *http.Request,
|
||||||
|
) {
|
||||||
|
user := common.GetUserFromContext(r.Context())
|
||||||
|
preference, apiErr := preferences.GetAllOrgPreferences(
|
||||||
|
r.Context(), user.User.OrgId,
|
||||||
|
)
|
||||||
|
if apiErr != nil {
|
||||||
|
RespondError(w, apiErr, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ah.Respond(w, preference)
|
||||||
|
}
|
||||||
|
|
||||||
// Integrations
|
// Integrations
|
||||||
func (ah *APIHandler) RegisterIntegrationRoutes(router *mux.Router, am *AuthMiddleware) {
|
func (ah *APIHandler) RegisterIntegrationRoutes(router *mux.Router, am *AuthMiddleware) {
|
||||||
subRouter := router.PathPrefix("/api/v1/integrations").Subrouter()
|
subRouter := router.PathPrefix("/api/v1/integrations").Subrouter()
|
||||||
@ -3050,6 +3213,22 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WARN: Only works for AND operator in traces query
|
||||||
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
||||||
|
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
|
||||||
|
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
|
||||||
|
if isUsed == true && len(traceIDs) > 0 {
|
||||||
|
zap.L().Debug("traceID used as filter in traces query")
|
||||||
|
// query signoz_spans table with traceID to get min and max timestamp
|
||||||
|
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
|
||||||
|
if err == nil {
|
||||||
|
// add timestamp filter to queryRange params
|
||||||
|
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
|
||||||
|
zap.L().Debug("post adding timestamp filter in traces query", zap.Any("queryRangeParams", queryRangeParams))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result, errQuriesByName, err = aH.querier.QueryRange(ctx, queryRangeParams, spanKeys)
|
result, errQuriesByName, err = aH.querier.QueryRange(ctx, queryRangeParams, spanKeys)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -3319,6 +3498,22 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WARN: Only works for AND operator in traces query
|
||||||
|
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
|
||||||
|
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
|
||||||
|
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
|
||||||
|
if isUsed == true && len(traceIDs) > 0 {
|
||||||
|
zap.L().Debug("traceID used as filter in traces query")
|
||||||
|
// query signoz_spans table with traceID to get min and max timestamp
|
||||||
|
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
|
||||||
|
if err == nil {
|
||||||
|
// add timestamp filter to queryRange params
|
||||||
|
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
|
||||||
|
zap.L().Debug("post adding timestamp filter in traces query", zap.Any("queryRangeParams", queryRangeParams))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result, errQuriesByName, err = aH.querierV2.QueryRange(ctx, queryRangeParams, spanKeys)
|
result, errQuriesByName, err = aH.querierV2.QueryRange(ctx, queryRangeParams, spanKeys)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -110,6 +110,13 @@ service:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### If using non-default nginx log format, adjust log parsing regex
|
||||||
|
|
||||||
|
If you are using a [custom nginx log format](https://docs.nginx.com/nginx/admin-guide/monitoring/logging/#setting-up-the-access-log),
|
||||||
|
please adjust the regex used for parsing logs in the receivers named
|
||||||
|
`filelog/nginx-access-logs` and `filelog/nginx-error-logs` in collector config.
|
||||||
|
|
||||||
|
|
||||||
#### Set Environment Variables
|
#### Set Environment Variables
|
||||||
|
|
||||||
Set the following environment variables in your otel-collector environment:
|
Set the following environment variables in your otel-collector environment:
|
||||||
|
@ -17,6 +17,7 @@ const (
|
|||||||
ARRAY_INT64 = "Array(Int64)"
|
ARRAY_INT64 = "Array(Int64)"
|
||||||
ARRAY_FLOAT64 = "Array(Float64)"
|
ARRAY_FLOAT64 = "Array(Float64)"
|
||||||
ARRAY_BOOL = "Array(Bool)"
|
ARRAY_BOOL = "Array(Bool)"
|
||||||
|
NGRAM_SIZE = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
var dataTypeMapping = map[string]string{
|
var dataTypeMapping = map[string]string{
|
||||||
@ -72,6 +73,7 @@ func getPath(keyArr []string) string {
|
|||||||
|
|
||||||
func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (string, error) {
|
func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (string, error) {
|
||||||
keyArr := strings.Split(key.Key, ".")
|
keyArr := strings.Split(key.Key, ".")
|
||||||
|
// i.e it should be at least body.name, and not something like body
|
||||||
if len(keyArr) < 2 {
|
if len(keyArr) < 2 {
|
||||||
return "", fmt.Errorf("incorrect key, should contain at least 2 parts")
|
return "", fmt.Errorf("incorrect key, should contain at least 2 parts")
|
||||||
}
|
}
|
||||||
@ -106,6 +108,29 @@ func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (
|
|||||||
return keyname, nil
|
return keyname, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// takes the path and the values and generates where clauses for better usage of index
|
||||||
|
func getPathIndexFilter(path string) string {
|
||||||
|
filters := []string{}
|
||||||
|
keyArr := strings.Split(path, ".")
|
||||||
|
if len(keyArr) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, key := range keyArr {
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = strings.TrimSuffix(key, "[*]")
|
||||||
|
if len(key) >= NGRAM_SIZE {
|
||||||
|
filters = append(filters, strings.ToLower(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(filters) > 0 {
|
||||||
|
return fmt.Sprintf("lower(body) like lower('%%%s%%')", strings.Join(filters, "%"))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func GetJSONFilter(item v3.FilterItem) (string, error) {
|
func GetJSONFilter(item v3.FilterItem) (string, error) {
|
||||||
|
|
||||||
dataType := item.Key.DataType
|
dataType := item.Key.DataType
|
||||||
@ -154,11 +179,28 @@ func GetJSONFilter(item v3.FilterItem) (string, error) {
|
|||||||
return "", fmt.Errorf("unsupported operator: %s", op)
|
return "", fmt.Errorf("unsupported operator: %s", op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters := []string{}
|
||||||
|
|
||||||
|
pathFilter := getPathIndexFilter(item.Key.Key)
|
||||||
|
if pathFilter != "" {
|
||||||
|
filters = append(filters, pathFilter)
|
||||||
|
}
|
||||||
|
if op == v3.FilterOperatorContains ||
|
||||||
|
op == v3.FilterOperatorEqual ||
|
||||||
|
op == v3.FilterOperatorHas {
|
||||||
|
val, ok := item.Value.(string)
|
||||||
|
if ok && len(val) >= NGRAM_SIZE {
|
||||||
|
filters = append(filters, fmt.Sprintf("lower(body) like lower('%%%s%%')", utils.QuoteEscapedString(strings.ToLower(val))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// add exists check for non array items as default values of int/float/bool will corrupt the results
|
// add exists check for non array items as default values of int/float/bool will corrupt the results
|
||||||
if !isArray && !(item.Operator == v3.FilterOperatorExists || item.Operator == v3.FilterOperatorNotExists) {
|
if !isArray && !(item.Operator == v3.FilterOperatorExists || item.Operator == v3.FilterOperatorNotExists) {
|
||||||
existsFilter := fmt.Sprintf("JSON_EXISTS(body, '$.%s')", getPath(strings.Split(item.Key.Key, ".")[1:]))
|
existsFilter := fmt.Sprintf("JSON_EXISTS(body, '$.%s')", getPath(strings.Split(item.Key.Key, ".")[1:]))
|
||||||
filter = fmt.Sprintf("%s AND %s", existsFilter, filter)
|
filter = fmt.Sprintf("%s AND %s", existsFilter, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter, nil
|
filters = append(filters, filter)
|
||||||
|
|
||||||
|
return strings.Join(filters, " AND "), nil
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "has",
|
Operator: "has",
|
||||||
Value: "index_service",
|
Value: "index_service",
|
||||||
},
|
},
|
||||||
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service')",
|
Filter: "lower(body) like lower('%requestor_list%') AND lower(body) like lower('%index_service%') AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service')",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Array membership int64",
|
Name: "Array membership int64",
|
||||||
@ -181,7 +181,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "has",
|
Operator: "has",
|
||||||
Value: 2,
|
Value: 2,
|
||||||
},
|
},
|
||||||
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"int_numbers\"[*]'), '" + ARRAY_INT64 + "'), 2)",
|
Filter: "lower(body) like lower('%int_numbers%') AND has(JSONExtract(JSON_QUERY(body, '$.\"int_numbers\"[*]'), '" + ARRAY_INT64 + "'), 2)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Array membership float64",
|
Name: "Array membership float64",
|
||||||
@ -194,7 +194,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "nhas",
|
Operator: "nhas",
|
||||||
Value: 2.2,
|
Value: 2.2,
|
||||||
},
|
},
|
||||||
Filter: "NOT has(JSONExtract(JSON_QUERY(body, '$.\"nested_num\"[*].\"float_nums\"[*]'), '" + ARRAY_FLOAT64 + "'), 2.200000)",
|
Filter: "lower(body) like lower('%nested_num%float_nums%') AND NOT has(JSONExtract(JSON_QUERY(body, '$.\"nested_num\"[*].\"float_nums\"[*]'), '" + ARRAY_FLOAT64 + "'), 2.200000)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Array membership bool",
|
Name: "Array membership bool",
|
||||||
@ -207,7 +207,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "has",
|
Operator: "has",
|
||||||
Value: true,
|
Value: true,
|
||||||
},
|
},
|
||||||
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"bool\"[*]'), '" + ARRAY_BOOL + "'), true)",
|
Filter: "lower(body) like lower('%bool%') AND has(JSONExtract(JSON_QUERY(body, '$.\"bool\"[*]'), '" + ARRAY_BOOL + "'), true)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "eq operator",
|
Name: "eq operator",
|
||||||
@ -220,7 +220,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "=",
|
Operator: "=",
|
||||||
Value: "hello",
|
Value: "hello",
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') = 'hello'",
|
Filter: "lower(body) like lower('%message%') AND lower(body) like lower('%hello%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') = 'hello'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "eq operator number",
|
Name: "eq operator number",
|
||||||
@ -233,7 +233,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "=",
|
Operator: "=",
|
||||||
Value: 1,
|
Value: 1,
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') = 1",
|
Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') = 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "neq operator number",
|
Name: "neq operator number",
|
||||||
@ -246,7 +246,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "=",
|
Operator: "=",
|
||||||
Value: 1.1,
|
Value: 1.1,
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + FLOAT64 + "') = 1.100000",
|
Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + FLOAT64 + "') = 1.100000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "eq operator bool",
|
Name: "eq operator bool",
|
||||||
@ -259,7 +259,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "=",
|
Operator: "=",
|
||||||
Value: true,
|
Value: true,
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"boolkey\"') AND JSONExtract(JSON_VALUE(body, '$.\"boolkey\"'), '" + BOOL + "') = true",
|
Filter: "lower(body) like lower('%boolkey%') AND JSON_EXISTS(body, '$.\"boolkey\"') AND JSONExtract(JSON_VALUE(body, '$.\"boolkey\"'), '" + BOOL + "') = true",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "greater than operator",
|
Name: "greater than operator",
|
||||||
@ -272,7 +272,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: ">",
|
Operator: ">",
|
||||||
Value: 1,
|
Value: 1,
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') > 1",
|
Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') > 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "regex operator",
|
Name: "regex operator",
|
||||||
@ -285,7 +285,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "regex",
|
Operator: "regex",
|
||||||
Value: "a*",
|
Value: "a*",
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"message\"') AND match(JSON_VALUE(body, '$.\"message\"'), 'a*')",
|
Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND match(JSON_VALUE(body, '$.\"message\"'), 'a*')",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "contains operator",
|
Name: "contains operator",
|
||||||
@ -298,7 +298,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "contains",
|
Operator: "contains",
|
||||||
Value: "a",
|
Value: "a",
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%'",
|
Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "contains operator with quotes",
|
Name: "contains operator with quotes",
|
||||||
@ -311,7 +311,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "contains",
|
Operator: "contains",
|
||||||
Value: "hello 'world'",
|
Value: "hello 'world'",
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%hello \\'world\\'%'",
|
Filter: "lower(body) like lower('%message%') AND lower(body) like lower('%hello \\'world\\'%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%hello \\'world\\'%'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "exists",
|
Name: "exists",
|
||||||
@ -324,7 +324,7 @@ var testGetJSONFilterData = []struct {
|
|||||||
Operator: "exists",
|
Operator: "exists",
|
||||||
Value: "",
|
Value: "",
|
||||||
},
|
},
|
||||||
Filter: "JSON_EXISTS(body, '$.\"message\"')",
|
Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"')",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +51,8 @@ var logOperators = map[v3.FilterOperator]string{
|
|||||||
v3.FilterOperatorNotExists: "not has(%s_%s_key, '%s')",
|
v3.FilterOperatorNotExists: "not has(%s_%s_key, '%s')",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BODY = "body"
|
||||||
|
|
||||||
func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string {
|
func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string {
|
||||||
if columnType == v3.AttributeKeyTypeTag {
|
if columnType == v3.AttributeKeyTypeTag {
|
||||||
return "attributes"
|
return "attributes"
|
||||||
@ -193,10 +195,24 @@ func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey,
|
|||||||
case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
|
case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
|
||||||
columnName := getClickhouseColumnName(item.Key)
|
columnName := getClickhouseColumnName(item.Key)
|
||||||
val := utils.QuoteEscapedString(fmt.Sprintf("%v", item.Value))
|
val := utils.QuoteEscapedString(fmt.Sprintf("%v", item.Value))
|
||||||
|
if columnName == BODY {
|
||||||
|
logsOp = strings.Replace(logsOp, "ILIKE", "LIKE", 1) // removing i from ilike and not ilike
|
||||||
|
conditions = append(conditions, fmt.Sprintf("lower(%s) %s lower('%%%s%%')", columnName, logsOp, val))
|
||||||
|
} else {
|
||||||
conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, logsOp, val))
|
conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, logsOp, val))
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
columnName := getClickhouseColumnName(item.Key)
|
columnName := getClickhouseColumnName(item.Key)
|
||||||
fmtVal := utils.ClickHouseFormattedValue(value)
|
fmtVal := utils.ClickHouseFormattedValue(value)
|
||||||
|
|
||||||
|
// for use lower for like and ilike
|
||||||
|
if op == v3.FilterOperatorLike || op == v3.FilterOperatorNotLike {
|
||||||
|
if columnName == BODY {
|
||||||
|
logsOp = strings.Replace(logsOp, "ILIKE", "LIKE", 1) // removing i from ilike and not ilike
|
||||||
|
columnName = fmt.Sprintf("lower(%s)", columnName)
|
||||||
|
fmtVal = fmt.Sprintf("lower(%s)", fmtVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
conditions = append(conditions, fmt.Sprintf("%s %s %s", columnName, logsOp, fmtVal))
|
conditions = append(conditions, fmt.Sprintf("%s %s %s", columnName, logsOp, fmtVal))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -477,7 +493,7 @@ type Options struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isOrderByTs(orderBy []v3.OrderBy) bool {
|
func isOrderByTs(orderBy []v3.OrderBy) bool {
|
||||||
if len(orderBy) == 1 && orderBy[0].Key == constants.TIMESTAMP {
|
if len(orderBy) == 1 && (orderBy[0].Key == constants.TIMESTAMP || orderBy[0].ColumnName == constants.TIMESTAMP) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -130,6 +130,14 @@ var timeSeriesFilterQueryData = []struct {
|
|||||||
}},
|
}},
|
||||||
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
|
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Test attribute and resource attribute with different case",
|
||||||
|
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||||
|
{Key: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "%JoHn%", Operator: "like"},
|
||||||
|
{Key: v3.AttributeKey{Key: "k8s_namespace", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "%MyService%", Operator: "nlike"},
|
||||||
|
}},
|
||||||
|
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] ILIKE '%JoHn%' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] NOT ILIKE '%MyService%'",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "Test materialized column",
|
Name: "Test materialized column",
|
||||||
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||||
@ -287,6 +295,22 @@ var timeSeriesFilterQueryData = []struct {
|
|||||||
}},
|
}},
|
||||||
ExpectedFilter: "`attribute_int64_status_exists`=false",
|
ExpectedFilter: "`attribute_int64_status_exists`=false",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Test for body contains and ncontains",
|
||||||
|
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||||
|
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "contains", Value: "test"},
|
||||||
|
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "ncontains", Value: "test1"},
|
||||||
|
}},
|
||||||
|
ExpectedFilter: "lower(body) LIKE lower('%test%') AND lower(body) NOT LIKE lower('%test1%')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Test for body like and nlike",
|
||||||
|
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||||
|
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "like", Value: "test"},
|
||||||
|
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "nlike", Value: "test1"},
|
||||||
|
}},
|
||||||
|
ExpectedFilter: "lower(body) LIKE lower('test') AND lower(body) NOT LIKE lower('test1')",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildLogsTimeSeriesFilterQuery(t *testing.T) {
|
func TestBuildLogsTimeSeriesFilterQuery(t *testing.T) {
|
||||||
@ -851,7 +875,7 @@ var testBuildLogsQueryData = []struct {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
TableName: "logs",
|
TableName: "logs",
|
||||||
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND body ILIKE '%test%' AND has(attributes_string_key, 'name') group by ts having value > 10 order by value DESC",
|
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) LIKE lower('%test%') AND has(attributes_string_key, 'name') group by ts having value > 10 order by value DESC",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Test attribute with same name as top level key",
|
Name: "Test attribute with same name as top level key",
|
||||||
@ -981,7 +1005,7 @@ var testBuildLogsQueryData = []struct {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
TableName: "logs",
|
TableName: "logs",
|
||||||
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%' AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
|
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%' AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "TABLE: Test count with JSON Filter Array, groupBy, orderBy",
|
Name: "TABLE: Test count with JSON Filter Array, groupBy, orderBy",
|
||||||
@ -1015,7 +1039,7 @@ var testBuildLogsQueryData = []struct {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
TableName: "logs",
|
TableName: "logs",
|
||||||
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service') AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
|
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) like lower('%requestor_list%') AND lower(body) like lower('%index_service%') AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service') AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1380,6 +1404,66 @@ var testPrepLogsQueryData = []struct {
|
|||||||
ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC LIMIT 10",
|
ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC LIMIT 10",
|
||||||
Options: Options{},
|
Options: Options{},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Ignore offset if order by is timestamp in list queries",
|
||||||
|
PanelType: v3.PanelTypeList,
|
||||||
|
Start: 1680066360726,
|
||||||
|
End: 1680066458000,
|
||||||
|
BuilderQuery: &v3.BuilderQuery{
|
||||||
|
QueryName: "A",
|
||||||
|
StepInterval: 60,
|
||||||
|
AggregateOperator: v3.AggregateOperatorNoOp,
|
||||||
|
Expression: "A",
|
||||||
|
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||||
|
{Key: v3.AttributeKey{Key: "id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "logid", Operator: "<"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OrderBy: []v3.OrderBy{
|
||||||
|
{
|
||||||
|
ColumnName: "timestamp",
|
||||||
|
Order: "DESC",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Offset: 100,
|
||||||
|
PageSize: 100,
|
||||||
|
},
|
||||||
|
TableName: "logs",
|
||||||
|
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as " +
|
||||||
|
"attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as " +
|
||||||
|
"attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
|
||||||
|
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < 'logid' order by " +
|
||||||
|
"timestamp DESC LIMIT 100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Don't ignore offset if order by is not timestamp",
|
||||||
|
PanelType: v3.PanelTypeList,
|
||||||
|
Start: 1680066360726,
|
||||||
|
End: 1680066458000,
|
||||||
|
BuilderQuery: &v3.BuilderQuery{
|
||||||
|
QueryName: "A",
|
||||||
|
StepInterval: 60,
|
||||||
|
AggregateOperator: v3.AggregateOperatorNoOp,
|
||||||
|
Expression: "A",
|
||||||
|
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||||
|
{Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OrderBy: []v3.OrderBy{
|
||||||
|
{
|
||||||
|
ColumnName: "mycolumn",
|
||||||
|
Order: "DESC",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Offset: 100,
|
||||||
|
PageSize: 100,
|
||||||
|
},
|
||||||
|
TableName: "logs",
|
||||||
|
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as " +
|
||||||
|
"attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as " +
|
||||||
|
"attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
|
||||||
|
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' order by " +
|
||||||
|
"resources_string_value[indexOf(resources_string_key, 'mycolumn')] DESC LIMIT 100 OFFSET 100",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareLogsQuery(t *testing.T) {
|
func TestPrepareLogsQuery(t *testing.T) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user