Compare commits

..

1 Commits

Author SHA1 Message Date
Rahul Keswani
e31f58a338 chore: added unit tests for help and support section 2024-07-22 13:28:22 +05:30
754 changed files with 4161 additions and 41086 deletions

6
.github/CODEOWNERS vendored
View File

@@ -5,6 +5,6 @@
/frontend/ @YounixM
/frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
/deploy/ @SigNoz/devops
/sample-apps/ @SigNoz/devops
.github @SigNoz/devops
/deploy/ @prashant-shahi
/sample-apps/ @prashant-shahi
.github @prashant-shahi

View File

@@ -30,7 +30,6 @@ jobs:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@@ -52,4 +51,4 @@ jobs:
make build-frontend-amd64
make run-testing
EOF
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@@ -30,7 +30,6 @@ jobs:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: |
read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@@ -53,4 +52,4 @@ jobs:
make build-frontend-amd64
make run-testing
EOF
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

3
.gitignore vendored
View File

@@ -67,6 +67,3 @@ e2e/.auth
# go
vendor/
**/main/**
# git-town
.git-branches.toml

View File

@@ -649,12 +649,12 @@
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
-->
<!--
<macros>
<shard>01</shard>
<replica>example01-01-1</replica>
</macros>
-->
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->

View File

@@ -211,7 +211,6 @@ services:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
- DOCKER_MULTI_NODE_CLUSTER=false

View File

@@ -36,7 +36,6 @@ receivers:
# endpoint: 0.0.0.0:6832
hostmetrics:
collection_interval: 30s
root_path: /hostfs
scrapers:
cpu: {}
load: {}

View File

@@ -649,12 +649,12 @@
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/#creating-replicated-tables
-->
<!--
<macros>
<shard>01</shard>
<replica>example01-01-1</replica>
</macros>
-->
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->

View File

@@ -93,8 +93,6 @@ services:
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
ports:

View File

@@ -244,7 +244,6 @@ services:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false

View File

@@ -243,7 +243,6 @@ services:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ./otel-collector-opamp-config.yaml:/etc/manager-config.yaml
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /:/hostfs:ro
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- DOCKER_MULTI_NODE_CLUSTER=false

View File

@@ -36,7 +36,6 @@ receivers:
# endpoint: 0.0.0.0:6832
hostmetrics:
collection_interval: 30s
root_path: /hostfs
scrapers:
cpu: {}
load: {}

View File

@@ -1,8 +1,3 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 3301;
server_name _;
@@ -47,14 +42,6 @@ server {
proxy_read_timeout 600s;
}
location /ws {
proxy_pass http://query-service:8080/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade "websocket";
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;

View File

@@ -1,9 +1,7 @@
package api
import (
"errors"
"net/http"
"strings"
"github.com/gorilla/mux"
"go.signoz.io/signoz/pkg/query-service/app/dashboards"
@@ -31,10 +29,6 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
// Get the dashboard UUID from the request
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)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())

View File

@@ -1,48 +1,17 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"go.signoz.io/signoz/ee/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
featureSet, err := ah.FF().GetFeatureFlags()
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
}
if constants.FetchFeatures == "true" {
zap.L().Debug("fetching license")
license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
if err != nil {
zap.L().Error("failed to fetch license", zap.Error(err))
} else if license == nil {
zap.L().Debug("no active license found")
} else {
licenseKey := license.Key
zap.L().Debug("fetching zeus features")
zeusFeatures, err := fetchZeusFeatures(constants.ZeusFeaturesURL, licenseKey)
if err == nil {
zap.L().Debug("fetched zeus features", zap.Any("features", zeusFeatures))
// merge featureSet and zeusFeatures in featureSet with higher priority to zeusFeatures
featureSet = MergeFeatureSets(zeusFeatures, featureSet)
} else {
zap.L().Error("failed to fetch zeus features", zap.Error(err))
}
}
}
if ah.opts.PreferSpanMetrics {
for idx := range featureSet {
feature := &featureSet[idx]
@@ -51,96 +20,5 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
}
}
}
ah.Respond(w, featureSet)
}
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
// and returns the FeatureSet.
func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
}
// Check if the licenseKey is empty
if licenseKey == "" {
return nil, fmt.Errorf("licenseKey is empty")
}
// Creating an HTTP client with a timeout for better control
client := &http.Client{
Timeout: 10 * time.Second,
}
// Creating a new GET request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Setting the custom header
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
// Making the GET request
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make GET request: %w", err)
}
defer func() {
if resp != nil {
resp.Body.Close()
}
}()
// Check for non-OK status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d %s", errors.New("received non-OK HTTP status code"), resp.StatusCode, http.StatusText(resp.StatusCode))
}
// Reading and decoding the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var zeusResponse ZeusFeaturesResponse
if err := json.Unmarshal(body, &zeusResponse); err != nil {
return nil, fmt.Errorf("%w: %v", errors.New("failed to decode response body"), err)
}
if zeusResponse.Status != "success" {
return nil, fmt.Errorf("%w: %s", errors.New("failed to fetch zeus features"), zeusResponse.Status)
}
return zeusResponse.Data, nil
}
type ZeusFeaturesResponse struct {
Status string `json:"status"`
Data basemodel.FeatureSet `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basemodel.FeatureSet {
// Create a map to store the merged features
featureMap := make(map[string]basemodel.Feature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
featureMap[feature.Name] = feature
}
// Add all features from the zeusFeatures set to the map
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
for _, feature := range zeusFeatures {
featureMap[feature.Name] = feature
}
// Convert the map back to a FeatureSet slice
var mergedFeatures basemodel.FeatureSet
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}
return mergedFeatures
}

View File

@@ -1,88 +0,0 @@
package api
import (
"testing"
"github.com/stretchr/testify/assert"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
)
func TestMergeFeatureSets(t *testing.T) {
tests := []struct {
name string
zeusFeatures basemodel.FeatureSet
internalFeatures basemodel.FeatureSet
expected basemodel.FeatureSet
}{
{
name: "empty zeusFeatures and internalFeatures",
zeusFeatures: basemodel.FeatureSet{},
internalFeatures: basemodel.FeatureSet{},
expected: basemodel.FeatureSet{},
},
{
name: "non-empty zeusFeatures and empty internalFeatures",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: basemodel.FeatureSet{},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "empty zeusFeatures and non-empty internalFeatures",
zeusFeatures: basemodel.FeatureSet{},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature3", Active: false},
},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature2", Active: true},
{Name: "Feature4", Active: false},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: true},
{Name: "Feature3", Active: false},
{Name: "Feature4", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
zeusFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: basemodel.FeatureSet{
{Name: "Feature1", Active: false},
{Name: "Feature3", Active: true},
},
expected: basemodel.FeatureSet{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
{Name: "Feature3", Active: true},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := MergeFeatureSets(test.zeusFeatures, test.internalFeatures)
assert.ElementsMatch(t, test.expected, actual)
})
}
}

View File

@@ -1,15 +1,14 @@
package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
_ "net/http/pprof" // http profiler
"os"
"regexp"
@@ -29,8 +28,6 @@ import (
"go.signoz.io/signoz/ee/query-service/integrations/gateway"
"go.signoz.io/signoz/ee/query-service/interfaces"
baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
licensepkg "go.signoz.io/signoz/ee/query-service/license"
@@ -44,7 +41,6 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/app/opamp"
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"
baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/healthcheck"
@@ -114,10 +110,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
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)
if err != nil {
@@ -126,13 +118,33 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
localDB.SetMaxOpenConns(10)
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
if err != nil {
return nil, err
gatewayFeature := basemodel.Feature{
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 {
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
lm, err := licensepkg.StartManager("sqlite", localDB)
lm, err := licensepkg.StartManager("sqlite", localDB, gatewayFeature)
if err != nil {
return nil, err
}
@@ -181,13 +193,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err
}
go func() {
err = migrate.ClickHouseMigrate(reader.GetConn(), serverOptions.Cluster)
if err != nil {
zap.L().Error("error while running clickhouse migrations", zap.Error(err))
}
}()
// initiate opamp
_, err = opAmpModel.InitDB(localDB)
if err != nil {
@@ -318,7 +323,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
// ip here for alert manager
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "SIGNOZ-API-KEY"},
})
handler := c.Handler(r)
@@ -335,17 +340,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
// add auth middleware
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
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
return auth.GetUserFromRequest(r, apiHandler)
}
am := baseapp.NewAuthMiddleware(getUserFromRequest)
@@ -359,13 +354,11 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
})
handler := c.Handler(r)
@@ -377,7 +370,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
}, nil
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// loggingMiddleware is used for logging public api calls
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -389,7 +381,6 @@ func loggingMiddleware(next http.Handler) http.Handler {
})
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// loggingMiddlewarePrivate is used for logging private api calls
// from internal services like alert manager
func loggingMiddlewarePrivate(next http.Handler) http.Handler {
@@ -402,41 +393,27 @@ func loggingMiddlewarePrivate(next http.Handler) http.Handler {
})
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
// WriteHeader(int) is not called if our response implicitly returns 200 OK, so
// we default to that status code.
return &loggingResponseWriter{w, http.StatusOK}
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Flush implements the http.Flush interface.
func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// Support websockets
func (lrw *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lrw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("hijack not supported")
}
return h.Hijack()
}
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
@@ -573,7 +550,6 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
})
}
// TODO(remove): Implemented at pkg/http/middleware/timeout.go
func setTimeoutMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -756,7 +732,6 @@ func makeRulesManager(
DisableRules: disableRules,
FeatureFlags: fm,
Reader: ch,
EvalDelay: baseconst.GetEvalDelay(),
}
// create Manager

View File

@@ -13,8 +13,6 @@ var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var SpanRenderLimitStr = GetOrDefaultEnv("SPAN_RENDER_LIMIT", "2500")
var MaxSpansInTraceStr = GetOrDefaultEnv("MAX_SPANS_IN_TRACE", "250000")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)

View File

@@ -20,14 +20,11 @@ import (
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
// get auth domain from email domain
domain, apierr := m.GetDomainByEmail(ctx, email)
if apierr != nil {
zap.L().Error("failed to get domain from email", zap.Error(apierr))
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())
if err != nil {

View File

@@ -5,5 +5,5 @@ import (
)
func NewNoopProxy() (*httputil.ReverseProxy, error) {
return &httputil.ReverseProxy{}, nil
return nil, nil
}

View File

@@ -147,7 +147,7 @@ func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, a
for _, l := range licenses {
l.ParsePlan()
if lm.activeLicense != nil && l.Key == lm.activeLicense.Key {
if l.Key == lm.activeLicense.Key {
l.IsCurrent = true
}

View File

@@ -11,9 +11,6 @@ const Enterprise = "ENTERPRISE_PLAN"
const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2"
var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{
@@ -114,27 +111,6 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: Gateway,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var ProPlan = basemodel.FeatureSet{
@@ -229,27 +205,6 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -358,25 +313,4 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}

View File

@@ -88,7 +88,7 @@
"lucide-react": "0.379.0",
"mini-css-extract-plugin": "2.4.5",
"papaparse": "5.4.1",
"posthog-js": "1.160.3",
"posthog-js": "1.142.1",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",
@@ -110,8 +110,6 @@
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"overlayscrollbars-react": "^0.5.6",
"overlayscrollbars": "^2.8.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",

View File

@@ -1 +0,0 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4344_1236)" stroke="#C0C1C3" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583"/><path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z"/></g><defs><clipPath id="prefix__clip0_4344_1236"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 878 B

View File

@@ -1 +0,0 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#prefix__clip0_4062_7291)" stroke-width="1.167" stroke-linecap="round" stroke-linejoin="round"><path d="M7 12.833A5.833 5.833 0 107 1.167a5.833 5.833 0 000 11.666z" fill="#E5484D" stroke="#E5484D"/><path d="M8.75 5.25l-3.5 3.5M5.25 5.25l3.5 3.5" stroke="#121317"/></g><defs><clipPath id="prefix__clip0_4062_7291"><path fill="#fff" d="M0 0h14v14H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 467 B

View File

@@ -1,30 +0,0 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}

View File

@@ -38,7 +38,5 @@
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"SUPPORT": "SigNoz | Support",
"DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview"
"DEFAULT": "Open source Observability Platform | SigNoz"
}

View File

@@ -1,7 +1,6 @@
{
"create_dashboard": "Create Dashboard",
"import_json": "Import Dashboard JSON",
"view_template": "View templates",
"import_grafana_json": "Import Grafana JSON",
"copy_to_clipboard": "Copy To ClipBoard",
"download_json": "Download JSON",

View File

@@ -1,30 +0,0 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}

View File

@@ -49,8 +49,5 @@
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS": "SigNoz | Integrations",
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues"
"INTEGRATIONS": "SigNoz | Integrations"
}

View File

@@ -76,8 +76,9 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
isUserFetching: false,
},
});
if (!isLoggedIn) {
history.push(ROUTES.LOGIN, { from: pathname });
history.push(ROUTES.LOGIN);
}
};

View File

@@ -1,7 +1,6 @@
import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
@@ -19,7 +18,6 @@ import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history';
import { identity, pick, pickBy } from 'lodash-es';
import posthog from 'posthog-js';
import AlertRuleProvider from 'providers/Alert';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useEffect, useState } from 'react';
@@ -50,7 +48,7 @@ function App(): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const { trackPageView } = useAnalytics();
const { trackPageView, trackEvent } = useAnalytics();
const { hostname, pathname } = window.location;
@@ -67,14 +65,6 @@ function App(): JSX.Element {
allFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)?.active ||
false;
const isPremiumSupportEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)?.active ||
false;
const showAddCreditCardModal =
!isPremiumSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
dispatch({
type: UPDATE_FEATURE_FLAG_RESPONSE,
payload: {
@@ -91,7 +81,7 @@ function App(): JSX.Element {
setRoutes(newRoutes);
}
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
if (isLoggedInState && isChatSupportEnabled) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.Intercom('boot', {
@@ -209,7 +199,7 @@ function App(): JSX.Element {
LOCALSTORAGE.THEME_ANALYTICS_V1,
);
if (!isThemeAnalyticsSent) {
logEvent('Theme Analytics', {
trackEvent('Theme Analytics', {
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
user: pick(user, ['email', 'userId', 'name']),
org,
@@ -237,24 +227,22 @@ function App(): JSX.Element {
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>

View File

@@ -92,14 +92,6 @@ export const CreateNewAlerts = Loadable(
() => import(/* webpackChunkName: "Create Alerts" */ 'pages/CreateAlert'),
);
export const AlertHistory = Loadable(
() => import(/* webpackChunkName: "Alert History" */ 'pages/AlertList'),
);
export const AlertOverview = Loadable(
() => import(/* webpackChunkName: "Alert Overview" */ 'pages/AlertList'),
);
export const CreateAlertChannelAlerts = Loadable(
() =>
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
@@ -212,15 +204,3 @@ export const InstalledIntegrations = Loadable(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const MessagingQueues = Loadable(
() =>
import(/* webpackChunkName: "MessagingQueues" */ 'pages/MessagingQueues'),
);
export const MQDetailPage = Loadable(
() =>
import(
/* webpackChunkName: "MQDetailPage" */ 'pages/MessagingQueues/MQDetailPage'
),
);

View File

@@ -2,8 +2,6 @@ import ROUTES from 'constants/routes';
import { RouteProps } from 'react-router-dom';
import {
AlertHistory,
AlertOverview,
AllAlertChannels,
AllErrors,
APIKeys,
@@ -25,8 +23,6 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueues,
MQDetailPage,
MySettings,
NewDashboardPage,
OldLogsExplorer,
@@ -173,20 +169,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'ALERTS_NEW',
},
{
path: ROUTES.ALERT_HISTORY,
exact: true,
component: AlertHistory,
isPrivate: true,
key: 'ALERT_HISTORY',
},
{
path: ROUTES.ALERT_OVERVIEW,
exact: true,
component: AlertOverview,
isPrivate: true,
key: 'ALERT_OVERVIEW',
},
{
path: ROUTES.TRACE,
exact: true,
@@ -369,20 +351,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'INTEGRATIONS',
},
{
path: ROUTES.MESSAGING_QUEUES,
exact: true,
component: MessagingQueues,
key: 'MESSAGING_QUEUES',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_DETAIL,
exact: true,
component: MQDetailPage,
key: 'MESSAGING_QUEUES_DETAIL',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -9,9 +9,9 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
// making the error status code as standard Error Status Code
const statusCode = response.status as ErrorStatusCode;
const { data } = response as AxiosResponse;
if (statusCode >= 400 && statusCode < 500) {
const { data } = response as AxiosResponse;
if (statusCode === 404) {
return {
statusCode,
@@ -34,11 +34,12 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
body: JSON.stringify((response.data as any).data),
};
}
return {
statusCode,
payload: null,
error: 'Something went wrong',
message: data?.error,
message: null,
};
}
if (request) {

View File

@@ -1,20 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/create';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.post('/rules', {
...props.data,
});
try {
const response = await axios.post('/rules', {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default create;

View File

@@ -1,18 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/delete';
const deleteAlerts = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.delete(`/rules/${props.id}`);
try {
const response = await axios.delete(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.rules,
};
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.rules,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default deleteAlerts;

View File

@@ -1,16 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/get';
const get = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.get(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
try {
const response = await axios.get(`/rules/${props.id}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default get;

View File

@@ -1,20 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/patch';
const patch = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.patch(`/rules/${props.id}`, {
...props.data,
});
try {
const response = await axios.patch(`/rules/${props.id}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default patch;

View File

@@ -1,20 +1,26 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/save';
const put = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.put(`/rules/${props.id}`, {
...props.data,
});
try {
const response = await axios.put(`/rules/${props.id}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default put;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleStatsPayload } from 'types/api/alerts/def';
import { RuleStatsProps } from 'types/api/alerts/ruleStats';
const ruleStats = async (
props: RuleStatsProps,
): Promise<SuccessResponse<AlertRuleStatsPayload> | ErrorResponse> => {
try {
const response = await axios.post(`/rules/${props.id}/history/stats`, {
start: props.start,
end: props.end,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default ruleStats;

View File

@@ -1,33 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTimelineGraphResponsePayload } from 'types/api/alerts/def';
import { GetTimelineGraphRequestProps } from 'types/api/alerts/timelineGraph';
const timelineGraph = async (
props: GetTimelineGraphRequestProps,
): Promise<
SuccessResponse<AlertRuleTimelineGraphResponsePayload> | ErrorResponse
> => {
try {
const response = await axios.post(
`/rules/${props.id}/history/overall_status`,
{
start: props.start,
end: props.end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default timelineGraph;

View File

@@ -1,36 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTimelineTableResponsePayload } from 'types/api/alerts/def';
import { GetTimelineTableRequestProps } from 'types/api/alerts/timelineTable';
const timelineTable = async (
props: GetTimelineTableRequestProps,
): Promise<
SuccessResponse<AlertRuleTimelineTableResponsePayload> | ErrorResponse
> => {
try {
const response = await axios.post(`/rules/${props.id}/history/timeline`, {
start: props.start,
end: props.end,
offset: props.offset,
limit: props.limit,
order: props.order,
state: props.state,
// TODO(shaheer): implement filters
filters: props.filters,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default timelineTable;

View File

@@ -1,33 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTopContributorsPayload } from 'types/api/alerts/def';
import { TopContributorsProps } from 'types/api/alerts/topContributors';
const topContributors = async (
props: TopContributorsProps,
): Promise<
SuccessResponse<AlertRuleTopContributorsPayload> | ErrorResponse
> => {
try {
const response = await axios.post(
`/rules/${props.id}/history/top_contributors`,
{
start: props.start,
end: props.end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default topContributors;

View File

@@ -3,7 +3,7 @@ const apiV1 = '/api/v1/';
export const apiV2 = '/api/v2/';
export const apiV3 = '/api/v3/';
export const apiV4 = '/api/v4/';
export const gatewayApiV1 = '/api/gateway/v1/';
export const apiAlertManager = '/api/alertmanager/';
export const gatewayApiV1 = '/api/gateway/v1';
export const apiAlertManager = '/api/alertmanager';
export default apiV1;

View File

@@ -1,62 +0,0 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { isEmpty } from 'lodash-es';
export interface WsDataEvent {
read_rows: number;
read_bytes: number;
elapsed_ms: number;
}
interface GetQueryStatsProps {
queryId: string;
setData: React.Dispatch<React.SetStateAction<WsDataEvent | undefined>>;
}
function getURL(baseURL: string, queryId: string): URL | string {
if (baseURL && !isEmpty(baseURL)) {
return `${baseURL}/ws/query_progress?q=${queryId}`;
}
const url = new URL(`/ws/query_progress?q=${queryId}`, window.location.href);
if (window.location.protocol === 'http:') {
url.protocol = 'ws';
} else {
url.protocol = 'wss';
}
return url;
}
export function getQueryStats(props: GetQueryStatsProps): void {
const { queryId, setData } = props;
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
// https://github.com/whatwg/websockets/issues/20 reason for not using the relative URLs
const url = getURL(ENVIRONMENT.wsURL, queryId);
const socket = new WebSocket(url, token);
socket.addEventListener('message', (event) => {
try {
const parsedData = JSON.parse(event?.data);
setData(parsedData);
} catch {
setData(event?.data);
}
});
socket.addEventListener('error', (event) => {
console.error(event);
});
socket.addEventListener('close', (event) => {
// 1000 is a normal closure status code
if (event.code !== 1000) {
console.error('WebSocket closed with error:', event);
} else {
console.error('WebSocket closed normally.');
}
});
}

View File

@@ -1,4 +1,4 @@
import { ApiBaseInstance as axios } from 'api';
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -21,7 +21,6 @@ const logEvent = async (
payload: response.data.data,
};
} catch (error) {
console.error(error);
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,8 +1,6 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import store from 'store';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
Props,
@@ -13,26 +11,7 @@ const dashboardVariablesQuery = async (
props: Props,
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
try {
const { globalTime } = store.getState();
const { start, end } = getStartEndRangeTime({
type: 'GLOBAL_TIME',
interval: globalTime.selectedTime,
});
const timeVariables: Record<string, number> = {
start_timestamp_ms: parseInt(start, 10) * 1e3,
end_timestamp_ms: parseInt(end, 10) * 1e3,
start_timestamp_nano: parseInt(start, 10) * 1e9,
end_timestamp_nano: parseInt(end, 10) * 1e9,
start_timestamp: parseInt(start, 10),
end_timestamp: parseInt(end, 10),
};
const payload = { ...props };
payload.variables = { ...payload.variables, ...timeVariables };
const response = await axios.post(`/variables/query`, payload);
const response = await axios.post(`/variables/query`, props);
return {
statusCode: 200,

View File

@@ -96,10 +96,6 @@ const interceptorRejected = async (
}
};
const interceptorRejectedBase = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => Promise.reject(value);
const instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
});
@@ -144,18 +140,6 @@ ApiV4Instance.interceptors.response.use(
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
export const GatewayApiV1Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,

View File

@@ -12,13 +12,10 @@ export const getMetricsQueryRange = async (
props: QueryRangePayload,
version: string,
signal: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try {
if (version && version === ENTITY_VERSION_V4) {
const response = await ApiV4Instance.post('/query_range', props, {
signal,
});
const response = await ApiV4Instance.post('/query_range', props, { signal });
return {
statusCode: 200,
@@ -29,10 +26,7 @@ export const getMetricsQueryRange = async (
};
}
const response = await ApiV3Instance.post('/query_range', props, {
signal,
headers,
});
const response = await ApiV3Instance.post('/query_range', props, { signal });
return {
statusCode: 200,

View File

@@ -1,20 +1,7 @@
import axios from 'api';
import { isNil } from 'lodash-es';
interface GetTopLevelOperationsProps {
service?: string;
start?: number;
end?: number;
}
const getTopLevelOperations = async (
props: GetTopLevelOperationsProps,
): Promise<ServiceDataProps> => {
const response = await axios.post(`/service/top_level_operations`, {
start: !isNil(props.start) ? `${props.start}` : undefined,
end: !isNil(props.end) ? `${props.end}` : undefined,
service: props.service,
});
const getTopLevelOperations = async (): Promise<ServiceDataProps> => {
const response = await axios.post(`/service/top_level_operations`);
return response.data;
};

View File

@@ -1,7 +1,6 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { Dayjs } from 'dayjs';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Recurrence } from './getAllDowntimeSchedules';
@@ -12,8 +11,8 @@ export interface DowntimeSchedulePayload {
alertIds: string[];
schedule: {
timezone?: string;
startTime?: string | Dayjs;
endTime?: string | Dayjs;
startTime?: string;
endTime?: string;
recurrence?: Recurrence;
};
}

View File

@@ -1,6 +1,6 @@
import axios from 'api';
import { AxiosError, AxiosResponse } from 'axios';
import { Option } from 'container/PlannedDowntime/PlannedDowntimeutils';
import { Option } from 'container/PlannedDowntime/DropdownWithSubMenu/DropdownWithSubMenu';
import { useQuery, UseQueryResult } from 'react-query';
export type Recurrence = {
@@ -28,7 +28,6 @@ export interface DowntimeSchedules {
createdBy: string | null;
updatedAt: string | null;
updatedBy: string | null;
kind: string | null;
}
export type PayloadProps = { data: DowntimeSchedules[] };

View File

@@ -1,63 +0,0 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { encode } from 'js-base64';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IGetAttributeSuggestionsPayload,
IGetAttributeSuggestionsSuccessResponse,
} from 'types/api/queryBuilder/getAttributeSuggestions';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const getAttributeSuggestions = async ({
searchText,
dataSource,
filters,
}: IGetAttributeSuggestionsPayload): Promise<
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
> => {
try {
let base64EncodedFiltersString;
try {
// the replace function is to remove the padding at the end of base64 encoded string which is auto added to make it a multiple of 4
// why ? because the current working of qs doesn't work well with padding
base64EncodedFiltersString = encode(JSON.stringify(filters)).replace(
/=+$/,
'',
);
} catch {
// default base64 encoded string for empty filters object
base64EncodedFiltersString = 'eyJpdGVtcyI6W10sIm9wIjoiQU5EIn0';
}
const response: AxiosResponse<{
data: IGetAttributeSuggestionsSuccessResponse;
}> = await ApiV3Instance.get(
`/filter_suggestions?${createQueryParams({
searchText,
dataSource,
existingFilter: base64EncodedFiltersString,
})}`,
);
const payload: BaseAutocompleteData[] =
response.data.data.attributes?.map(({ id: _, ...item }) => ({
...item,
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
})) || [];
return {
statusCode: 200,
error: null,
message: response.statusText,
payload: {
attributes: payload,
example_queries: response.data.data.example_queries,
},
};
} catch (e) {
return ErrorResponseHandler(e as AxiosError);
}
};

View File

@@ -1,41 +0,0 @@
interface ConfigureIconProps {
width?: number;
height?: number;
fill?: string;
}
function ConfigureIcon({
width,
height,
fill,
}: ConfigureIconProps): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
fill={fill}
>
<path
stroke="#C0C1C3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.333"
d="M9.71 4.745a.576.576 0 000 .806l.922.922a.576.576 0 00.806 0l2.171-2.171a3.455 3.455 0 01-4.572 4.572l-3.98 3.98a1.222 1.222 0 11-1.727-1.728l3.98-3.98a3.455 3.455 0 014.572-4.572L9.717 4.739l-.006.006z"
/>
<path
stroke="#C0C1C3"
strokeLinecap="round"
strokeWidth="1.333"
d="M4 7L2.527 5.566a1.333 1.333 0 01-.013-1.898l.81-.81a1.333 1.333 0 011.991.119L5.333 3m5.417 7.988l1.179 1.178m0 0l-.138.138a.833.833 0 00.387 1.397v0a.833.833 0 00.792-.219l.446-.446a.833.833 0 00.176-.917v0a.833.833 0 00-1.355-.261l-.308.308z"
/>
</svg>
);
}
ConfigureIcon.defaultProps = {
width: 16,
height: 16,
fill: 'none',
};
export default ConfigureIcon;

View File

@@ -1,65 +0,0 @@
interface LogsIconProps {
width?: number;
height?: number;
fill?: string;
strokeColor?: string;
strokeWidth?: number;
}
function LogsIcon({
width,
height,
fill,
strokeColor,
strokeWidth,
}: LogsIconProps): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
fill={fill}
>
<path
stroke={strokeColor}
strokeWidth={strokeWidth}
d="M2.917 3.208v7.875"
/>
<ellipse
cx="6.417"
cy="3.208"
stroke={strokeColor}
strokeWidth={strokeWidth}
rx="3.5"
ry="1.458"
/>
<ellipse cx="6.417" cy="3.165" fill={strokeColor} rx="0.875" ry="0.365" />
<path
stroke={strokeColor}
strokeWidth={strokeWidth}
d="M9.917 11.083c0 .645-1.567 1.167-3.5 1.167s-3.5-.522-3.5-1.167"
/>
<path
stroke={strokeColor}
strokeLinecap="round"
strokeWidth={strokeWidth}
d="M5.25 6.417v1.117c0 .028.02.053.049.057l1.652.276A.058.058 0 017 7.924v1.993"
/>
<path
stroke={strokeColor}
strokeWidth={strokeWidth}
d="M9.917 3.208v3.103c0 .046.05.074.089.05L12.182 5a.058.058 0 01.088.035l.264 1.059a.058.058 0 01-.013.053l-2.59 2.877a.058.058 0 00-.014.04v2.018"
/>
</svg>
);
}
LogsIcon.defaultProps = {
width: 14,
height: 14,
fill: 'none',
strokeColor: '#C0C1C3',
strokeWidth: 1.167,
};
export default LogsIcon;

View File

@@ -1,39 +0,0 @@
interface SeverityCriticalIconProps {
width?: number;
height?: number;
fill?: string;
stroke?: string;
}
function SeverityCriticalIcon({
width,
height,
fill,
stroke,
}: SeverityCriticalIconProps): JSX.Element {
return (
<svg
width={width}
height={height}
fill={fill}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M.99707.666056.99707 2.99939M.99707 5.33337H.991237M3.00293.666056 3.00293 2.99939M3.00293 5.33337H2.9971M5.00879.666056V2.99939M5.00879 5.33337H5.00296"
stroke={stroke}
strokeWidth="1.16667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
SeverityCriticalIcon.defaultProps = {
width: 6,
height: 6,
fill: 'none',
stroke: '#F56C87',
};
export default SeverityCriticalIcon;

View File

@@ -1,42 +0,0 @@
interface SeverityErrorIconProps {
width?: number;
height?: number;
fill?: string;
stroke?: string;
strokeWidth?: string;
}
function SeverityErrorIcon({
width,
height,
fill,
stroke,
strokeWidth,
}: SeverityErrorIconProps): JSX.Element {
return (
<svg
width={width}
height={height}
fill={fill}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.00781.957845 1.00781 2.99951M1.00781 5.04175H1.00228"
stroke={stroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
SeverityErrorIcon.defaultProps = {
width: 2,
height: 6,
fill: 'none',
stroke: '#F56C87',
strokeWidth: '1.02083',
};
export default SeverityErrorIcon;

View File

@@ -1,46 +0,0 @@
interface SeverityInfoIconProps {
width?: number;
height?: number;
fill?: string;
stroke?: string;
}
function SeverityInfoIcon({
width,
height,
fill,
stroke,
}: SeverityInfoIconProps): JSX.Element {
return (
<svg
width={width}
height={height}
fill={fill}
xmlns="http://www.w3.org/2000/svg"
>
<rect
width={width}
height={height}
rx="3.5"
fill={stroke}
fillOpacity=".2"
/>
<path
d="M7 9.33346V7.00012M7 4.66675H7.00583"
stroke={stroke}
strokeWidth="1.16667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
SeverityInfoIcon.defaultProps = {
width: 14,
height: 14,
fill: 'none',
stroke: '#7190F9',
};
export default SeverityInfoIcon;

View File

@@ -1,42 +0,0 @@
interface SeverityWarningIconProps {
width?: number;
height?: number;
fill?: string;
stroke?: string;
strokeWidth?: string;
}
function SeverityWarningIcon({
width,
height,
fill,
stroke,
strokeWidth,
}: SeverityWarningIconProps): JSX.Element {
return (
<svg
width={width}
height={height}
fill={fill}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.00732.957845 1.00732 2.99951M1.00732 5.04175H1.00179"
stroke={stroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
SeverityWarningIcon.defaultProps = {
width: 2,
height: 6,
fill: 'none',
stroke: '#FFD778',
strokeWidth: '0.978299',
};
export default SeverityWarningIcon;

View File

@@ -1,27 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import { useIsDarkMode } from 'hooks/useDarkMode';
function GroupByIcon(): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g
clipPath="url(#prefix__clip0_4344_1236)"
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
strokeWidth="1.167"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4.667 1.167H2.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V2.333c0-.644-.522-1.166-1.166-1.166zM8.167 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M11.667 1.167a1.17 1.17 0 011.166 1.166v2.334a1.17 1.17 0 01-1.166 1.166M5.833 10.5H2.917c-.992 0-1.75-.758-1.75-1.75v-.583" />
<path d="M4.083 12.25l1.75-1.75-1.75-1.75M11.667 8.167H9.333c-.644 0-1.166.522-1.166 1.166v2.334c0 .644.522 1.166 1.166 1.166h2.334c.644 0 1.166-.522 1.166-1.166V9.333c0-.644-.522-1.166-1.166-1.166z" />
</g>
<defs>
<clipPath id="prefix__clip0_4344_1236">
<path fill="#fff" d="M0 0h14v14H0z" />
</clipPath>
</defs>
</svg>
);
}
export default GroupByIcon;

View File

@@ -1,14 +0,0 @@
.reset-button {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-ink-300);
border: 1px solid var(--bg-slate-400);
}
.lightMode {
.reset-button {
background: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-300);
}
}

View File

@@ -1,11 +0,0 @@
import './Filters.styles.scss';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
export function Filters(): JSX.Element {
return (
<div className="filters">
<DateTimeSelector showAutoRefresh={false} hideShareModal showResetButton />
</div>
);
}

View File

@@ -1,137 +0,0 @@
import { Button, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const { data: licenseData, isFetching } = useLicense();
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
if (data?.payload?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
updateCreditCardApi,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const { pathname } = useLocation();
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `intercom icon`,
page: pathname,
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
return (
<>
<div className="chat-support-gateway">
<Button
className="chat-support-gateway-btn"
onClick={(): void => {
logEvent('Disabled Chat Support: Clicked', {
source: `intercom icon`,
page: pathname,
});
setIsAddCreditCardModalOpen(true);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 28 32"
className="chat-support-gateway-btn-icon"
>
<path d="M28 32s-4.714-1.855-8.527-3.34H3.437C1.54 28.66 0 27.026 0 25.013V3.644C0 1.633 1.54 0 3.437 0h21.125c1.898 0 3.437 1.632 3.437 3.645v18.404H28V32zm-4.139-11.982a.88.88 0 00-1.292-.105c-.03.026-3.015 2.681-8.57 2.681-5.486 0-8.517-2.636-8.571-2.684a.88.88 0 00-1.29.107 1.01 1.01 0 00-.219.708.992.992 0 00.318.664c.142.128 3.537 3.15 9.762 3.15 6.226 0 9.621-3.022 9.763-3.15a.992.992 0 00.317-.664 1.01 1.01 0 00-.218-.707z" />
</svg>
</Button>
</div>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</>
);
}

View File

@@ -1,191 +0,0 @@
import './LaunchChatSupport.styles.scss';
import { Button, Modal, Tooltip, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CreditCard, HelpCircle, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def';
import { isCloudUser } from 'utils/app';
export interface LaunchChatSupportProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
intercomMessageDisabled?: boolean;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function LaunchChatSupport({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
intercomMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null {
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
const { notifications } = useNotifications();
const { data: licenseData, isFetching } = useLicense();
const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
const { pathname } = useLocation();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
useEffect(() => {
const activeValidLicense =
licenseData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
const handleFacingIssuesClick = (): void => {
if (showAddCreditCardModal) {
logEvent('Disabled Chat Support: Clicked', {
source: `facing issues button`,
page: pathname,
...attributes,
});
setIsAddCreditCardModalOpen(true);
} else {
logEvent(eventName, attributes);
if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
}
};
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
): void => {
if (data?.payload?.redirectURL) {
const newTab = document.createElement('a');
newTab.href = data.payload.redirectURL;
newTab.target = '_blank';
newTab.rel = 'noopener noreferrer';
newTab.click();
}
};
const handleBillingOnError = (): void => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
};
const { mutate: updateCreditCard, isLoading: isLoadingBilling } = useMutation(
updateCreditCardApi,
{
onSuccess: (data) => {
handleBillingOnSuccess(data);
},
onError: handleBillingOnError,
},
);
const handleAddCreditCard = (): void => {
logEvent('Add Credit card modal: Clicked', {
source: `facing issues button`,
page: pathname,
...attributes,
});
updateCreditCard({
licenseKey: activeLicense?.key || '',
successURL: window.location.href,
cancelURL: window.location.href,
});
};
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip
title={onHoverText}
autoAdjustOverflow
style={{ padding: 8 }}
overlayClassName="tooltip-overlay"
>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
{/* Add Credit Card Modal */}
<Modal
className="add-credit-card-modal"
title={<span className="title">Add Credit Card for Chat Support</span>}
open={isAddCreditCardModalOpen}
closable
onCancel={(): void => setIsAddCreditCardModalOpen(false)}
destroyOnClose
footer={[
<Button
key="cancel"
onClick={(): void => setIsAddCreditCardModalOpen(false)}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
icon={<CreditCard size={16} />}
size="middle"
loading={isLoadingBilling}
disabled={isLoadingBilling}
onClick={handleAddCreditCard}
className="add-credit-card-btn"
>
Add Credit Card
</Button>,
]}
>
<Typography.Text className="add-credit-card-text">
You&apos;re currently on <span className="highlight-text">Trial plan</span>
. Add a credit card to access SigNoz chat support to your workspace.
</Typography.Text>
</Modal>
</div>
) : null;
}
LaunchChatSupport.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
intercomMessageDisabled: false,
};
export default LaunchChatSupport;

View File

@@ -1,22 +1,14 @@
import { DrawerProps } from 'antd';
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { VIEWS } from './constants';
export type LogDetailProps = {
log: ILog | null;
selectedTab: VIEWS;
onGroupByAttribute?: (
fieldKey: string,
isJSON?: boolean,
dataType?: DataTypes,
) => Promise<void>;
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -2,23 +2,14 @@
import './LogDetails.styles.scss';
import { Color, Spacing } from '@signozhq/design-tokens';
import Convert from 'ansi-to-html';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LOCALSTORAGE } from 'constants/localStorage';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import JSONView from 'container/LogDetailedView/JsonView';
import Overview from 'container/LogDetailedView/Overview';
import {
aggregateAttributesResourcesToString,
removeEscapeCharacters,
unescapeString,
} from 'container/LogDetailedView/utils';
import { useOptionsMenu } from 'container/OptionsMenu';
import dompurify from 'dompurify';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import {
@@ -30,27 +21,21 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
import { VIEW_TYPES, VIEWS } from './constants';
import { LogDetailProps } from './LogDetail.interfaces';
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
const convert = new Convert();
function LogDetail({
log,
onClose,
onAddToQuery,
onGroupByAttribute,
onClickActionItem,
selectedTab,
isListViewPanel = false,
listViewPanelSelectedFields,
}: LogDetailProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
@@ -60,19 +45,6 @@ function LogDetail({
const [contextQuery, setContextQuery] = useState<Query | undefined>();
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const { initialDataSource, stagedQuery } = useQueryBuilder();
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
}, [stagedQuery]);
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const isDarkMode = useIsDarkMode();
@@ -99,17 +71,6 @@ function LogDetail({
}
};
const htmlBody = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(log?.body || ''), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),
}),
[log?.body],
);
const handleJSONCopy = (): void => {
copyToClipboard(LogJsonData);
notifications.success({
@@ -147,8 +108,8 @@ function LogDetail({
>
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
<Tooltip title={log?.body} placement="left">
<Typography.Text className="log-body">{log?.body}</Typography.Text>
</Tooltip>
<div className="log-overflow-shadow">&nbsp;</div>
@@ -230,10 +191,7 @@ function LogDetail({
logData={log}
onAddToQuery={onAddToQuery}
onClickActionItem={onClickActionItem}
onGroupByAttribute={onGroupByAttribute}
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

@@ -1,16 +1,3 @@
.addToQueryContainer {
cursor: pointer;
display: flex;
align-items: center;
&.small {
line-height: 16px;
}
&.medium {
line-height: 20px;
}
&.large {
line-height: 24px;
}
}

View File

@@ -1,16 +1,13 @@
import './AddToQueryHOC.styles.scss';
import { Popover } from 'antd';
import cx from 'classnames';
import { OPERATORS } from 'constants/queryBuilder';
import { FontSize } from 'container/OptionsMenu/types';
import { memo, MouseEvent, ReactNode, useMemo } from 'react';
function AddToQueryHOC({
fieldKey,
fieldValue,
onAddToQuery,
fontSize,
children,
}: AddToQueryHOCProps): JSX.Element {
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
@@ -24,7 +21,7 @@ function AddToQueryHOC({
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
<div className="addToQueryContainer" onClick={handleQueryAdd}>
<Popover placement="top" content={popOverContent}>
{children}
</Popover>
@@ -36,7 +33,6 @@ export interface AddToQueryHOCProps {
fieldKey: string;
fieldValue: string;
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
fontSize: FontSize;
children: ReactNode;
}

View File

@@ -4,7 +4,6 @@ import { ReactNode, useCallback, useEffect } from 'react';
import { useCopyToClipboard } from 'react-use';
function CopyClipboardHOC({
entityKey,
textToCopy,
children,
}: CopyClipboardHOCProps): JSX.Element {
@@ -12,15 +11,11 @@ function CopyClipboardHOC({
const { notifications } = useNotifications();
useEffect(() => {
if (value.value) {
const key = entityKey || '';
const notificationMessage = `${key} copied to clipboard`;
notifications.success({
message: notificationMessage,
message: 'Copied to clipboard',
});
}
}, [value, notifications, entityKey]);
}, [value, notifications]);
const onClick = useCallback((): void => {
setCopy(textToCopy);
@@ -39,7 +34,6 @@ function CopyClipboardHOC({
}
interface CopyClipboardHOCProps {
entityKey: string | undefined;
textToCopy: string;
children: ReactNode;
}

View File

@@ -6,21 +6,6 @@
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
&.small {
font-size: 11px;
line-height: 16px;
}
&.medium {
font-size: 13px;
line-height: 20px;
}
&.large {
font-size: 14px;
line-height: 24px;
}
}
.log-value {
color: var(--text-vanilla-400, #c0c1c3);
@@ -29,21 +14,6 @@
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
&.small {
font-size: 11px;
line-height: 16px;
}
&.medium {
font-size: 13px;
line-height: 20px;
}
&.large {
font-size: 14px;
line-height: 24px;
}
}
.log-line {
display: flex;
@@ -70,20 +40,6 @@
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
&.small {
font-size: 11px;
line-height: 16px;
}
&.medium {
font-size: 13px;
line-height: 20px;
}
&.large {
font-size: 14px;
line-height: 24px;
}
}
.selected-log-value {
@@ -96,37 +52,12 @@
line-height: 18px;
letter-spacing: -0.07px;
font-size: 14px;
&.small {
font-size: 11px;
line-height: 16px;
}
&.medium {
font-size: 13px;
line-height: 20px;
}
&.large {
font-size: 14px;
line-height: 24px;
}
}
.selected-log-kv {
min-height: 24px;
display: flex;
align-items: center;
&.small {
min-height: 16px;
}
&.medium {
min-height: 20px;
}
&.large {
min-height: 24px;
}
}
}

View File

@@ -3,11 +3,8 @@ import './ListLogView.styles.scss';
import { blue } from '@ant-design/colors';
import Convert from 'ansi-to-html';
import { Typography } from 'antd';
import cx from 'classnames';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog';
@@ -42,7 +39,6 @@ interface LogFieldProps {
fieldKey: string;
fieldValue: string;
linesPerRow?: number;
fontSize: FontSize;
}
type LogSelectedFieldProps = Omit<LogFieldProps, 'linesPerRow'> &
@@ -52,12 +48,11 @@ function LogGeneralField({
fieldKey,
fieldValue,
linesPerRow = 1,
fontSize,
}: LogFieldProps): JSX.Element {
const html = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(fieldValue), {
dompurify.sanitize(fieldValue, {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),
@@ -67,12 +62,12 @@ function LogGeneralField({
return (
<TextContainer>
<Text ellipsis type="secondary" className={cx('log-field-key', fontSize)}>
<Text ellipsis type="secondary" className="log-field-key">
{`${fieldKey} : `}
</Text>
<LogText
dangerouslySetInnerHTML={html}
className={cx('log-value', fontSize)}
className="log-value"
linesPerRow={linesPerRow > 1 ? linesPerRow : undefined}
/>
</TextContainer>
@@ -83,7 +78,6 @@ function LogSelectedField({
fieldKey = '',
fieldValue = '',
onAddToQuery,
fontSize,
}: LogSelectedFieldProps): JSX.Element {
return (
<div className="log-selected-fields">
@@ -91,22 +85,16 @@ function LogSelectedField({
fieldKey={fieldKey}
fieldValue={fieldValue}
onAddToQuery={onAddToQuery}
fontSize={fontSize}
>
<Typography.Text>
<span
style={{ color: blue[4] }}
className={cx('selected-log-field-key', fontSize)}
>
<span style={{ color: blue[4] }} className="selected-log-field-key">
{fieldKey}
</span>
</Typography.Text>
</AddToQueryHOC>
<Typography.Text ellipsis className={cx('selected-log-kv', fontSize)}>
<span className={cx('selected-log-field-key', fontSize)}>{': '}</span>
<span className={cx('selected-log-value', fontSize)}>
{fieldValue || "''"}
</span>
<Typography.Text ellipsis className="selected-log-kv">
<span className="selected-log-field-key">{': '}</span>
<span className="selected-log-value">{fieldValue || "''"}</span>
</Typography.Text>
</div>
);
@@ -119,7 +107,6 @@ type ListLogViewProps = {
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
activeLog?: ILog | null;
linesPerRow: number;
fontSize: FontSize;
};
function ListLogView({
@@ -129,7 +116,6 @@ function ListLogView({
onAddToQuery,
activeLog,
linesPerRow,
fontSize,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -142,7 +128,6 @@ function ListLogView({
onAddToQuery: handleAddToQuery,
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
onGroupByAttribute,
} = useActiveLog();
const isDarkMode = useIsDarkMode();
@@ -200,7 +185,6 @@ function ListLogView({
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleDetailedView}
fontSize={fontSize}
>
<div className="log-line">
<LogStateIndicator
@@ -208,28 +192,18 @@ function ListLogView({
isActive={
activeLog?.id === logData.id || activeContextLog?.id === logData.id
}
fontSize={fontSize}
/>
<div>
<LogContainer fontSize={fontSize}>
<LogContainer>
<LogGeneralField
fieldKey="Log"
fieldValue={flattenLogData.body}
linesPerRow={linesPerRow}
fontSize={fontSize}
/>
{flattenLogData.stream && (
<LogGeneralField
fieldKey="Stream"
fieldValue={flattenLogData.stream}
fontSize={fontSize}
/>
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
)}
<LogGeneralField
fieldKey="Timestamp"
fieldValue={timestampValue}
fontSize={fontSize}
/>
<LogGeneralField fieldKey="Timestamp" fieldValue={timestampValue} />
{updatedSelecedFields.map((field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
@@ -238,7 +212,6 @@ function ListLogView({
fieldKey={field.name}
fieldValue={flattenLogData[field.name] as never}
onAddToQuery={onAddToQuery}
fontSize={fontSize}
/>
) : null,
)}
@@ -259,7 +232,6 @@ function ListLogView({
onAddToQuery={handleAddToQuery}
selectedTab={VIEW_TYPES.CONTEXT}
onClose={handlerClearActiveContextLog}
onGroupByAttribute={onGroupByAttribute}
/>
)}
</>

View File

@@ -1,46 +1,21 @@
/* eslint-disable no-nested-ternary */
import { Color } from '@signozhq/design-tokens';
import { Card, Typography } from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
interface LogTextProps {
linesPerRow?: number;
}
interface LogContainerProps {
fontSize: FontSize;
}
export const Container = styled(Card)<{
$isActiveLog: boolean;
$isDarkMode: boolean;
fontSize: FontSize;
}>`
width: 100% !important;
margin-bottom: 0.3rem;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `margin-bottom:0.1rem;`
: fontSize === FontSize.MEDIUM
? `margin-bottom: 0.2rem;`
: fontSize === FontSize.LARGE
? `margin-bottom:0.3rem;`
: ``}
cursor: pointer;
.ant-card-body {
padding: 0.3rem 0.6rem;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `padding:0.1rem 0.6rem;`
: fontSize === FontSize.MEDIUM
? `padding: 0.2rem 0.6rem;`
: fontSize === FontSize.LARGE
? `padding:0.3rem 0.6rem;`
: ``}
${({ $isActiveLog, $isDarkMode }): string =>
$isActiveLog
? `background-color: ${
@@ -63,17 +38,11 @@ export const TextContainer = styled.div`
width: 100%;
`;
export const LogContainer = styled.div<LogContainerProps>`
export const LogContainer = styled.div`
margin-left: 0.5rem;
display: flex;
flex-direction: column;
gap: 6px;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `gap: 2px;`
: fontSize === FontSize.MEDIUM
? ` gap:4px;`
: `gap:6px;`}
`;
export const LogText = styled.div<LogTextProps>`

View File

@@ -9,24 +9,11 @@
border-radius: 50px;
background-color: transparent;
&.small {
min-height: 16px;
}
&.medium {
min-height: 20px;
}
&.large {
min-height: 24px;
}
&.INFO {
background-color: var(--bg-slate-400);
}
&.WARNING,
&.WARN {
&.WARNING, &.WARN {
background-color: var(--bg-amber-500);
}

View File

@@ -1,13 +1,10 @@
import { render } from '@testing-library/react';
import { FontSize } from 'container/OptionsMenu/types';
import LogStateIndicator from './LogStateIndicator';
describe('LogStateIndicator', () => {
it('renders correctly with default props', () => {
const { container } = render(
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
);
const { container } = render(<LogStateIndicator type="INFO" />);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('log-state-indicator')).toBe(true);
expect(indicator.classList.contains('isActive')).toBe(false);
@@ -18,30 +15,28 @@ describe('LogStateIndicator', () => {
});
it('renders correctly when isActive is true', () => {
const { container } = render(
<LogStateIndicator type="INFO" isActive fontSize={FontSize.MEDIUM} />,
);
const { container } = render(<LogStateIndicator type="INFO" isActive />);
const indicator = container.firstChild as HTMLElement;
expect(indicator.classList.contains('isActive')).toBe(true);
});
it('renders correctly with different types', () => {
const { container: containerInfo } = render(
<LogStateIndicator type="INFO" fontSize={FontSize.MEDIUM} />,
<LogStateIndicator type="INFO" />,
);
expect(containerInfo.querySelector('.line')?.classList.contains('INFO')).toBe(
true,
);
const { container: containerWarning } = render(
<LogStateIndicator type="WARNING" fontSize={FontSize.MEDIUM} />,
<LogStateIndicator type="WARNING" />,
);
expect(
containerWarning.querySelector('.line')?.classList.contains('WARNING'),
).toBe(true);
const { container: containerError } = render(
<LogStateIndicator type="ERROR" fontSize={FontSize.MEDIUM} />,
<LogStateIndicator type="ERROR" />,
);
expect(
containerError.querySelector('.line')?.classList.contains('ERROR'),

View File

@@ -1,7 +1,6 @@
import './LogStateIndicator.styles.scss';
import cx from 'classnames';
import { FontSize } from 'container/OptionsMenu/types';
export const SEVERITY_TEXT_TYPE = {
TRACE: 'TRACE',
@@ -29,31 +28,24 @@ export const SEVERITY_TEXT_TYPE = {
FATAL2: 'FATAL2',
FATAL3: 'FATAL3',
FATAL4: 'FATAL4',
UNKNOWN: 'UNKNOWN',
} as const;
export const LogType = {
TRACE: 'TRACE',
DEBUG: 'DEBUG',
INFO: 'INFO',
WARN: 'WARN',
WARNING: 'WARNING',
ERROR: 'ERROR',
FATAL: 'FATAL',
UNKNOWN: 'UNKNOWN',
} as const;
function LogStateIndicator({
type,
isActive,
fontSize,
}: {
type: string;
fontSize: FontSize;
isActive?: boolean;
}): JSX.Element {
return (
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
<div className={cx('line', type, fontSize)}> </div>
<div className={cx('line', type)}> </div>
</div>
);
}

View File

@@ -1,10 +1,9 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { ILog } from 'types/api/logs/log';
import { getLogIndicatorType, getLogIndicatorTypeForTable } from './utils';
describe('getLogIndicatorType', () => {
it('severity_number should be given priority over severity_text', () => {
it('should return severity type for valid log with severityText', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
@@ -21,57 +20,11 @@ describe('getLogIndicatorType', () => {
attributesInt: {},
attributesFloat: {},
severity_text: 'INFO',
severity_number: 2,
};
// severity_number should get priority over severity_text
expect(getLogIndicatorType(log)).toBe('TRACE');
expect(getLogIndicatorType(log)).toBe('INFO');
});
it('severity_text should be used when severity_number is absent ', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: 'INFO',
severityNumber: 2,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'FATAL',
severity_number: 0,
};
expect(getLogIndicatorType(log)).toBe('FATAL');
});
it('case insensitive severity_text should be valid', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: 'INFO',
severityNumber: 2,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'fatAl',
severity_number: 0,
};
expect(getLogIndicatorType(log)).toBe('FATAL');
});
it('should return log level if severityText and severityNumber is missing', () => {
it('should return log level if severityText is missing', () => {
const log: ILog = {
date: '2024-02-29T12:34:58Z',
timestamp: 1646115296,
@@ -83,16 +36,13 @@ describe('getLogIndicatorType', () => {
body: 'Sample log',
resources_string: {},
attributesString: {},
attributes_string: {
log_level: 'INFO' as never,
},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: 'some_random',
severity_text: 'FATAL',
severityText: '',
severity_number: 0,
};
expect(getLogIndicatorType(log)).toBe('INFO');
expect(getLogIndicatorType(log)).toBe('FATAL');
});
});
@@ -105,7 +55,6 @@ describe('getLogIndicatorTypeForTable', () => {
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 2,
severity_number: 2,
body: 'Sample log message',
resources_string: {},
@@ -115,7 +64,7 @@ describe('getLogIndicatorTypeForTable', () => {
attributesFloat: {},
severity_text: 'WARN',
};
expect(getLogIndicatorTypeForTable(log)).toBe('TRACE');
expect(getLogIndicatorTypeForTable(log)).toBe('WARN');
});
it('should return log level if severityText is missing', () => {
@@ -126,8 +75,7 @@ describe('getLogIndicatorTypeForTable', () => {
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityNumber: 0,
severity_number: 0,
severityNumber: 2,
body: 'Sample log message',
resources_string: {},
attributesString: {},
@@ -139,47 +87,3 @@ describe('getLogIndicatorTypeForTable', () => {
expect(getLogIndicatorTypeForTable(log)).toBe('INFO');
});
});
describe('logIndicatorBySeverityNumber', () => {
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
const logLevelExpectations = [
{ minSevNumber: 1, maxSevNumber: 4, expectedIndicatorType: 'TRACE' },
{ minSevNumber: 5, maxSevNumber: 8, expectedIndicatorType: 'DEBUG' },
{ minSevNumber: 9, maxSevNumber: 12, expectedIndicatorType: 'INFO' },
{ minSevNumber: 13, maxSevNumber: 16, expectedIndicatorType: 'WARN' },
{ minSevNumber: 17, maxSevNumber: 20, expectedIndicatorType: 'ERROR' },
{ minSevNumber: 21, maxSevNumber: 24, expectedIndicatorType: 'FATAL' },
];
logLevelExpectations.forEach((e) => {
for (let sevNum = e.minSevNumber; sevNum <= e.maxSevNumber; sevNum++) {
const sevText = (Math.random() + 1).toString(36).substring(2);
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
id: '123456',
traceId: '987654',
spanId: '54321',
traceFlags: 0,
severityText: sevText,
severityNumber: sevNum,
body: 'Sample log Message',
resources_string: {},
attributesString: {},
attributes_string: {},
attributesInt: {},
attributesFloat: {},
severity_text: sevText,
severity_number: sevNum,
};
it(`getLogIndicatorType should return ${e.expectedIndicatorType} for severity_text: ${sevText} and severity_number: ${sevNum}`, () => {
expect(getLogIndicatorType(log)).toBe(e.expectedIndicatorType);
});
it(`getLogIndicatorTypeForTable should return ${e.expectedIndicatorType} for severity_text: ${sevText} and severity_number: ${sevNum}`, () => {
expect(getLogIndicatorTypeForTable(log)).toBe(e.expectedIndicatorType);
});
}
});
});

View File

@@ -2,112 +2,56 @@ import { ILog } from 'types/api/logs/log';
import { LogType, SEVERITY_TEXT_TYPE } from './LogStateIndicator';
const getLogTypeBySeverityText = (severityText: string): string => {
const getSeverityType = (severityText: string): string => {
switch (severityText) {
case SEVERITY_TEXT_TYPE.TRACE:
case SEVERITY_TEXT_TYPE.TRACE2:
case SEVERITY_TEXT_TYPE.TRACE3:
case SEVERITY_TEXT_TYPE.TRACE4:
return LogType.TRACE;
return SEVERITY_TEXT_TYPE.TRACE;
case SEVERITY_TEXT_TYPE.DEBUG:
case SEVERITY_TEXT_TYPE.DEBUG2:
case SEVERITY_TEXT_TYPE.DEBUG3:
case SEVERITY_TEXT_TYPE.DEBUG4:
return LogType.DEBUG;
return SEVERITY_TEXT_TYPE.DEBUG;
case SEVERITY_TEXT_TYPE.INFO:
case SEVERITY_TEXT_TYPE.INFO2:
case SEVERITY_TEXT_TYPE.INFO3:
case SEVERITY_TEXT_TYPE.INFO4:
return LogType.INFO;
return SEVERITY_TEXT_TYPE.INFO;
case SEVERITY_TEXT_TYPE.WARN:
case SEVERITY_TEXT_TYPE.WARN2:
case SEVERITY_TEXT_TYPE.WARN3:
case SEVERITY_TEXT_TYPE.WARN4:
case SEVERITY_TEXT_TYPE.WARNING:
return LogType.WARN;
return SEVERITY_TEXT_TYPE.WARN;
case SEVERITY_TEXT_TYPE.ERROR:
case SEVERITY_TEXT_TYPE.ERROR2:
case SEVERITY_TEXT_TYPE.ERROR3:
case SEVERITY_TEXT_TYPE.ERROR4:
return LogType.ERROR;
return SEVERITY_TEXT_TYPE.ERROR;
case SEVERITY_TEXT_TYPE.FATAL:
case SEVERITY_TEXT_TYPE.FATAL2:
case SEVERITY_TEXT_TYPE.FATAL3:
case SEVERITY_TEXT_TYPE.FATAL4:
return LogType.FATAL;
return SEVERITY_TEXT_TYPE.FATAL;
default:
return LogType.UNKNOWN;
return SEVERITY_TEXT_TYPE.INFO;
}
};
// https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
const getLogTypeBySeverityNumber = (severityNumber: number): string => {
if (severityNumber < 1) {
return LogType.UNKNOWN;
}
if (severityNumber < 5) {
return LogType.TRACE;
}
if (severityNumber < 9) {
return LogType.DEBUG;
}
if (severityNumber < 13) {
return LogType.INFO;
}
if (severityNumber < 17) {
return LogType.WARN;
}
if (severityNumber < 21) {
return LogType.ERROR;
}
if (severityNumber < 25) {
return LogType.FATAL;
}
return LogType.UNKNOWN;
};
const getLogType = (
severityText: string,
severityNumber: number,
defaultType: string,
): string => {
// give priority to the severityNumber
if (severityNumber) {
const logType = getLogTypeBySeverityNumber(severityNumber);
if (logType !== LogType.UNKNOWN) {
return logType;
}
}
// is severityNumber is not present then rely on the severityText
if (severityText) {
const logType = getLogTypeBySeverityText(severityText);
if (logType !== LogType.UNKNOWN) {
return logType;
}
}
return defaultType;
};
export const getLogIndicatorType = (logData: ILog): string => {
const defaultType = logData.attributes_string?.log_level || LogType.INFO;
// convert the severity_text to upper case for the comparison to support case insensitive values
return getLogType(
logData?.severity_text?.toUpperCase(),
logData?.severity_number || 0,
defaultType,
);
if (logData.severity_text) {
return getSeverityType(logData.severity_text);
}
return logData.attributes_string?.log_level || LogType.INFO;
};
export const getLogIndicatorTypeForTable = (
log: Record<string, unknown>,
): string => {
const defaultType = (log.log_level as string) || LogType.INFO;
// convert the severity_text to upper case for the comparison to support case insensitive values
return getLogType(
(log?.severity_text as string)?.toUpperCase(),
(log?.severity_number as number) || 0,
defaultType,
);
if (log.severity_text) {
return getSeverityType(log.severity_text as string);
}
return (log.log_level as string) || LogType.INFO;
};

View File

@@ -4,7 +4,6 @@ import Convert from 'ansi-to-html';
import { DrawerProps } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
@@ -40,7 +39,6 @@ function RawLogView({
linesPerRow,
isTextOverflowEllipsisDisabled,
selectedFields = [],
fontSize,
}: RawLogViewProps): JSX.Element {
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
data.id,
@@ -56,7 +54,6 @@ function RawLogView({
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
onGroupByAttribute,
} = useActiveLog();
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
@@ -146,9 +143,7 @@ function RawLogView({
const html = useMemo(
() => ({
__html: convert.toHtml(
dompurify.sanitize(unescapeString(text), {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
dompurify.sanitize(text, { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS] }),
),
}),
[text],
@@ -165,7 +160,6 @@ function RawLogView({
$isActiveLog={isActiveLog}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
fontSize={fontSize}
>
<LogStateIndicator
type={logType}
@@ -174,7 +168,6 @@ function RawLogView({
activeContextLog?.id === data.id ||
isActiveLog
}
fontSize={fontSize}
/>
<RawLogContent
@@ -183,7 +176,6 @@ function RawLogView({
$isDarkMode={isDarkMode}
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
linesPerRow={linesPerRow}
fontSize={fontSize}
dangerouslySetInnerHTML={html}
/>
@@ -207,7 +199,6 @@ function RawLogView({
onClose={handleCloseLogDetail}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
onGroupByAttribute={onGroupByAttribute}
/>
)}
</RawLogViewContainer>

View File

@@ -1,8 +1,6 @@
/* eslint-disable no-nested-ternary */
import { blue } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens';
import { Col, Row, Space } from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
@@ -13,7 +11,6 @@ export const RawLogViewContainer = styled(Row)<{
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isHightlightedLog: boolean;
fontSize: FontSize;
}>`
position: relative;
width: 100%;
@@ -25,13 +22,6 @@ export const RawLogViewContainer = styled(Row)<{
.log-state-indicator {
margin: 4px 0;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `margin: 1px 0;`
: fontSize === FontSize.MEDIUM
? `margin: 1px 0;`
: `margin: 2px 0;`}
}
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
@@ -60,8 +50,8 @@ export const RawLogContent = styled.div<RawLogContentProps>`
margin-bottom: 0;
font-family: 'SF Mono', monospace;
font-family: 'Geist Mono';
letter-spacing: -0.07px;
padding: 4px;
font-size: 13px;
font-weight: 400;
text-align: left;
color: ${({ $isDarkMode }): string =>
$isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400};
@@ -76,15 +66,9 @@ export const RawLogContent = styled.div<RawLogContentProps>`
line-clamp: ${linesPerRow};
-webkit-box-orient: vertical;`};
font-size: 13px;
font-weight: 400;
line-height: 24px;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `font-size:11px; line-height:16px; padding:1px;`
: fontSize === FontSize.MEDIUM
? `font-size:13px; line-height:20px; padding:1px;`
: `font-size:14px; line-height:24px; padding:2px;`}
letter-spacing: -0.07px;
padding: 4px;
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};

View File

@@ -1,4 +1,3 @@
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
@@ -8,13 +7,11 @@ export interface RawLogViewProps {
isTextOverflowEllipsisDisabled?: boolean;
data: ILog;
linesPerRow: number;
fontSize: FontSize;
selectedFields?: IField[];
}
export interface RawLogContentProps {
linesPerRow: number;
fontSize: FontSize;
$isReadOnly?: boolean;
$isActiveLog?: boolean;
$isDarkMode?: boolean;

View File

@@ -1,10 +1,7 @@
/* eslint-disable no-nested-ternary */
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
interface TableBodyContentProps {
linesPerRow: number;
fontSize: FontSize;
isDarkMode?: boolean;
}
@@ -23,10 +20,4 @@ export const TableBodyContent = styled.div<TableBodyContentProps>`
-webkit-line-clamp: ${(props): number => props.linesPerRow};
line-clamp: ${(props): number => props.linesPerRow};
-webkit-box-orient: vertical;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `font-size:11px; line-height:16px;`
: fontSize === FontSize.MEDIUM
? `font-size:13px; line-height:20px;`
: `font-size:14px; line-height:24px;`}
`;

View File

@@ -1,5 +1,4 @@
import { ColumnsType, ColumnType } from 'antd/es/table';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
@@ -11,7 +10,6 @@ export type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
fontSize: FontSize;
onClickExpand?: (log: ILog) => void;
};

View File

@@ -5,21 +5,6 @@
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
&.small {
font-size: 11px;
line-height: 16px;
}
&.medium {
font-size: 13px;
line-height: 20px;
}
&.large {
font-size: 14px;
line-height: 24px;
}
}
.table-timestamp {
@@ -40,21 +25,3 @@
color: var(--bg-slate-400);
}
}
.paragraph {
padding: 0px !important;
&.small {
font-size: 11px !important;
line-height: 16px !important;
}
&.medium {
font-size: 13px !important;
line-height: 20px !important;
}
&.large {
font-size: 14px !important;
line-height: 24px !important;
}
}

View File

@@ -3,8 +3,6 @@ import './useTableView.styles.scss';
import Convert from 'ansi-to-html';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import cx from 'classnames';
import { unescapeString } from 'container/LogDetailedView/utils';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -33,7 +31,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
logs,
fields,
linesPerRow,
fontSize,
appendTo = 'center',
activeContextLog,
activeLog,
@@ -60,10 +57,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
: getDefaultCellStyle(isDarkMode),
},
children: (
<Typography.Paragraph
ellipsis={{ rows: linesPerRow }}
className={cx('paragraph', fontSize)}
>
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
{field}
</Typography.Paragraph>
),
@@ -93,9 +87,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
isActive={
activeLog?.id === item.id || activeContextLog?.id === item.id
}
fontSize={fontSize}
/>
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
<Typography.Paragraph ellipsis className="text">
{date}
</Typography.Paragraph>
</div>
@@ -116,12 +109,11 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
<TableBodyContent
dangerouslySetInnerHTML={{
__html: convert.toHtml(
dompurify.sanitize(unescapeString(field), {
dompurify.sanitize(field, {
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
}),
),
}}
fontSize={fontSize}
linesPerRow={linesPerRow}
isDarkMode={isDarkMode}
/>
@@ -138,7 +130,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
linesPerRow,
activeLog?.id,
activeContextLog?.id,
fontSize,
]);
return { columns, dataSource: flattenLogData };

View File

@@ -17,126 +17,17 @@
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.font-size-dropdown {
display: flex;
flex-direction: column;
.back-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 12px;
border: none !important;
box-shadow: none !important;
.icon {
flex-shrink: 0;
}
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: 0.14px;
}
}
.back-btn:hover {
background-color: unset !important;
}
.content {
display: flex;
flex-direction: column;
.option-btn {
display: flex;
align-items: center;
padding: 12px;
border: none !important;
box-shadow: none !important;
justify-content: space-between;
.icon {
flex-shrink: 0;
}
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal; /* 142.857% */
letter-spacing: 0.14px;
text-transform: capitalize;
}
.text:hover {
color: var(--bg-vanilla-300);
}
}
.option-btn:hover {
background-color: unset !important;
}
}
}
.font-size-container {
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
.title {
color: var(--bg-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
.value {
display: flex;
height: 20px;
padding: 4px 0px;
justify-content: space-between;
align-items: center;
border: none !important;
.font-value {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
text-transform: capitalize;
}
.icon {
}
}
.value:hover {
background-color: unset !important;
}
}
.menu-container {
padding: 12px;
.title {
font-family: Inter;
font-size: 11px;
font-weight: 500;
font-weight: 600;
line-height: 18px;
letter-spacing: 0.08em;
text-align: left;
color: var(--bg-slate-50);
color: #52575c;
}
.menu-items {
@@ -174,11 +65,11 @@
padding: 12px;
.title {
color: var(--bg-slate-50);
color: #52575c;
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
@@ -258,11 +149,11 @@
}
.title {
color: var(--bg-slate-50);
color: #52575c;
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
font-weight: 600;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
@@ -408,38 +299,6 @@
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
.font-size-dropdown {
.back-btn {
.text {
color: var(--bg-ink-400);
}
}
.content {
.option-btn {
.text {
color: var(--bg-ink-400);
}
.text:hover {
color: var(--bg-ink-300);
}
}
}
}
.font-size-container {
.title {
color: var(--bg-ink-100);
}
.value {
.font-value {
color: var(--bg-ink-400);
}
}
}
.horizontal-line {
background: var(--bg-vanilla-300);
}

View File

@@ -3,12 +3,12 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './LogsFormatOptionsMenu.styles.scss';
import { Button, Divider, Input, InputNumber, Tooltip, Typography } from 'antd';
import { Divider, Input, InputNumber, Tooltip } from 'antd';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Check, ChevronLeft, ChevronRight, Minus, Plus, X } from 'lucide-react';
import { Check, Minus, Plus, X } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
interface LogsFormatOptionsMenuProps {
@@ -24,16 +24,10 @@ export default function LogsFormatOptionsMenu({
selectedOptionFormat,
config,
}: LogsFormatOptionsMenuProps): JSX.Element {
const { maxLines, format, addColumn, fontSize } = config;
const { maxLines, format, addColumn } = config;
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
const maxLinesNumber = (maxLines?.value as number) || 1;
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
const [fontSizeValue, setFontSizeValue] = useState<FontSize>(
fontSize?.value || FontSize.SMALL,
);
const [isFontSizeOptionsOpen, setIsFontSizeOptionsOpen] = useState<boolean>(
false,
);
const [addNewColumn, setAddNewColumn] = useState(false);
@@ -94,12 +88,6 @@ export default function LogsFormatOptionsMenu({
}
}, [maxLinesPerRow]);
useEffect(() => {
if (fontSizeValue && config && config.fontSize?.onChange) {
config.fontSize.onChange(fontSizeValue);
}
}, [fontSizeValue]);
return (
<div
className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
@@ -108,213 +96,145 @@ export default function LogsFormatOptionsMenu({
event.stopPropagation();
}}
>
{isFontSizeOptionsOpen ? (
<div className="font-size-dropdown">
<Button
onClick={(): void => setIsFontSizeOptionsOpen(false)}
className="back-btn"
type="text"
>
<ChevronLeft size={14} className="icon" />
<Typography.Text className="text">Select font size</Typography.Text>
</Button>
<div className="horizontal-line" />
<div className="content">
<Button
onClick={(): void => {
setFontSizeValue(FontSize.SMALL);
}}
className="option-btn"
type="text"
>
<Typography.Text className="text">{FontSize.SMALL}</Typography.Text>
{fontSizeValue === FontSize.SMALL && (
<Check size={14} className="icon" />
)}
</Button>
<Button
onClick={(): void => {
setFontSizeValue(FontSize.MEDIUM);
}}
className="option-btn"
type="text"
>
<Typography.Text className="text">{FontSize.MEDIUM}</Typography.Text>
{fontSizeValue === FontSize.MEDIUM && (
<Check size={14} className="icon" />
)}
</Button>
<Button
onClick={(): void => {
setFontSizeValue(FontSize.LARGE);
}}
className="option-btn"
type="text"
>
<Typography.Text className="text">{FontSize.LARGE}</Typography.Text>
{fontSizeValue === FontSize.LARGE && (
<Check size={14} className="icon" />
)}
</Button>
</div>
<div className="menu-container">
<div className="title"> {title} </div>
<div className="menu-items">
{items.map(
(item: any): JSX.Element => (
<div
className="item"
key={item.label}
onClick={(): void => handleMenuItemClick(item.key)}
>
<div className={cx('item-label')}>
{item.label}
{selectedItem === item.key && <Check size={12} />}
</div>
</div>
),
)}
</div>
) : (
</div>
{selectedItem && (
<>
<div className="font-size-container">
<div className="title">Font Size</div>
<Button
className="value"
type="text"
onClick={(): void => {
setIsFontSizeOptionsOpen(true);
}}
>
<Typography.Text className="font-value">{fontSizeValue}</Typography.Text>
<ChevronRight size={14} className="icon" />
</Button>
</div>
<div className="horizontal-line" />
<div className="menu-container">
<div className="title"> {title} </div>
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</div>
</>
<div className="menu-items">
{items.map(
(item: any): JSX.Element => (
<div
className="item"
key={item.label}
onClick={(): void => handleMenuItemClick(item.key)}
>
<div className={cx('item-label')}>
{item.label}
<div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />}
{selectedItem === item.key && <Check size={12} />}
{addNewColumn && (
<div className="add-new-column-header">
<div className="title">
{' '}
columns
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
)}
<div className="item-content">
{!addNewColumn && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
)}
<div className="column-format">
{addColumn?.value?.map(({ key, id }) => (
<div className="column-name" key={id}>
<div className="name">
<Tooltip placement="left" title={key}>
{key}
</Tooltip>
</div>
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(id as string)}
/>
</div>
),
))}
</div>
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
{addNewColumn &&
addColumn &&
addColumn.value.length > 0 &&
addColumn.options &&
addColumn?.options?.length > 0 && (
<Divider className="column-divider" />
)}
{addNewColumn && (
<div className="column-format-new-options">
{addColumn?.options?.map(({ label, value }) => (
<div
className="column-name"
key={value}
onClick={(eve): void => {
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(value, { label, disabled: false });
}
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
)}
</div>
</div>
{selectedItem && (
<>
<>
<div className="horizontal-line" />
<div className="max-lines-per-row">
<div className="title"> max lines per row </div>
<div className="raw-format max-lines-per-row-input">
<button
type="button"
className="periscope-btn"
onClick={decrementMaxLinesPerRow}
>
{' '}
<Minus size={12} />{' '}
</button>
<InputNumber
min={1}
max={10}
value={maxLinesPerRow}
onChange={handleLinesPerRowChange}
/>
<button
type="button"
className="periscope-btn"
onClick={incrementMaxLinesPerRow}
>
{' '}
<Plus size={12} />{' '}
</button>
</div>
</div>
</>
<div className="selected-item-content-container active">
{!addNewColumn && <div className="horizontal-line" />}
{addNewColumn && (
<div className="add-new-column-header">
<div className="title">
{' '}
columns
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
<Input
tabIndex={0}
type="text"
autoFocus
onFocus={addColumn?.onFocus}
onChange={handleSearchValueChange}
placeholder="Search..."
/>
</div>
)}
<div className="item-content">
{!addNewColumn && (
<div className="title">
columns
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
</div>
)}
<div className="column-format">
{addColumn?.value?.map(({ key, id }) => (
<div className="column-name" key={id}>
<div className="name">
<Tooltip placement="left" title={key}>
{key}
</Tooltip>
</div>
<X
className="delete-btn"
size={14}
onClick={(): void => addColumn.onRemove(id as string)}
/>
</div>
))}
</div>
{addColumn?.isFetching && (
<div className="loading-container"> Loading ... </div>
)}
{addNewColumn &&
addColumn &&
addColumn.value.length > 0 &&
addColumn.options &&
addColumn?.options?.length > 0 && (
<Divider className="column-divider" />
)}
{addNewColumn && (
<div className="column-format-new-options">
{addColumn?.options?.map(({ label, value }) => (
<div
className="column-name"
key={value}
onClick={(eve): void => {
eve.stopPropagation();
if (addColumn && addColumn?.onSelect) {
addColumn?.onSelect(value, { label, disabled: false });
}
}}
>
<div className="name">
<Tooltip placement="left" title={label}>
{label}
</Tooltip>
</div>
</div>
))}
</div>
)}
</div>
</div>
</>
)}
</>
)}
</div>

View File

@@ -1,54 +0,0 @@
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
import VirtuosoOverlayScrollbar from 'components/VirtuosoOverlayScrollbar/VirtuosoOverlayScrollbar';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { PartialOptions } from 'overlayscrollbars';
import { CSSProperties, ReactElement, useMemo } from 'react';
type Props = {
children: ReactElement;
isVirtuoso?: boolean;
style?: CSSProperties;
options?: PartialOptions;
};
function OverlayScrollbar({
children,
isVirtuoso,
style,
options: customOptions,
}: Props): any {
const isDarkMode = useIsDarkMode();
const options = useMemo(
() =>
({
scrollbars: {
autoHide: 'scroll',
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
},
...(customOptions || {}),
} as PartialOptions),
[customOptions, isDarkMode],
);
if (isVirtuoso) {
return (
<VirtuosoOverlayScrollbar style={style} options={options}>
{children}
</VirtuosoOverlayScrollbar>
);
}
return (
<TypicalOverlayScrollbar style={style} options={options}>
{children}
</TypicalOverlayScrollbar>
);
}
OverlayScrollbar.defaultProps = {
isVirtuoso: false,
style: {},
options: {},
};
export default OverlayScrollbar;

View File

@@ -1,145 +0,0 @@
.checkbox-filter {
display: flex;
flex-direction: column;
padding: 12px;
gap: 12px;
border-bottom: 1px solid var(--bg-slate-400);
.filter-header-checkbox {
display: flex;
align-items: center;
justify-content: space-between;
.left-action {
display: flex;
align-items: center;
gap: 6px;
.title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
text-transform: capitalize;
}
}
.right-action {
display: flex;
align-items: center;
.clear-all {
font-size: 12px;
color: var(--bg-robin-500);
cursor: pointer;
}
}
}
.values {
display: flex;
flex-direction: column;
gap: 8px;
.value {
display: flex;
align-items: center;
gap: 8px;
.checkbox-value-section {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: pointer;
&.filter-disabled {
cursor: not-allowed;
.value-string {
color: var(--bg-slate-200);
}
.only-btn {
cursor: not-allowed;
color: var(--bg-slate-200);
}
.toggle-btn {
cursor: not-allowed;
color: var(--bg-slate-200);
}
}
.value-string {
}
.only-btn {
display: none;
}
.toggle-btn {
display: none;
}
.toggle-btn:hover {
background-color: unset;
}
.only-btn:hover {
background-color: unset;
}
}
.checkbox-value-section:hover {
.toggle-btn {
display: none;
}
.only-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
}
}
.value:hover {
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
}
}
.no-data {
align-self: center;
}
.show-more {
display: flex;
align-items: center;
justify-content: center;
.show-more-text {
color: var(--bg-robin-500);
cursor: pointer;
}
}
}
.lightMode {
.checkbox-filter {
border-bottom: 1px solid var(--bg-vanilla-300);
.filter-header-checkbox {
.left-action {
.title {
color: var(--bg-ink-400);
}
}
}
}
}

View File

@@ -1,503 +0,0 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './Checkbox.styles.scss';
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
import { OPERATORS } from 'constants/queryBuilder';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { useMemo, useState } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'nin'];
function setDefaultValues(
values: string[],
trueOrFalse: boolean,
): Record<string, boolean> {
const defaultState: Record<string, boolean> = {};
values.forEach((val) => {
defaultState[val] = trueOrFalse;
});
return defaultState;
}
interface ICheckboxProps {
filter: IQuickFiltersConfig;
}
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { filter } = props;
const [searchText, setSearchText] = useState<string>('');
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
const {
lastUsedQuery,
currentQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const { data, isLoading } = useGetAggregateValues(
{
aggregateOperator: 'noop',
dataSource: DataSource.LOGS,
aggregateAttribute: '',
attributeKey: filter.attributeKey.key,
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
tagType: filter.attributeKey.type || '',
searchText: searchText ?? '',
},
{
enabled: isOpen,
keepPreviousData: true,
},
);
const attributeValues: string[] = useMemo(
() =>
((Object.values(data?.payload || {}).find((el) => !!el) ||
[]) as string[]).filter((val) => !isEmpty(val)),
[data?.payload],
);
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
// derive the state of each filter key here in the renderer itself and keep it in sync with staged query
// also we need to keep a note of last focussed query.
// eslint-disable-next-line sonarjs/cognitive-complexity
const currentFilterState = useMemo(() => {
let filterState: Record<string, boolean> = setDefaultValues(
attributeValues,
false,
);
const filterSync = currentQuery?.builder.queryData?.[
lastUsedQuery || 0
]?.filters?.items.find((item) => isEqual(item.key, filter.attributeKey));
if (filterSync) {
if (SELECTED_OPERATORS.includes(filterSync.op)) {
if (isArray(filterSync.value)) {
filterSync.value.forEach((val) => {
filterState[val] = true;
});
} else if (typeof filterSync.value === 'string') {
filterState[filterSync.value] = true;
} else if (typeof filterSync.value === 'boolean') {
filterState[String(filterSync.value)] = true;
} else if (typeof filterSync.value === 'number') {
filterState[String(filterSync.value)] = true;
}
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
filterState = setDefaultValues(attributeValues, true);
if (isArray(filterSync.value)) {
filterSync.value.forEach((val) => {
filterState[val] = false;
});
} else if (typeof filterSync.value === 'string') {
filterState[filterSync.value] = false;
} else if (typeof filterSync.value === 'boolean') {
filterState[String(filterSync.value)] = false;
} else if (typeof filterSync.value === 'number') {
filterState[String(filterSync.value)] = false;
}
}
} else {
filterState = setDefaultValues(attributeValues, true);
}
return filterState;
}, [
attributeValues,
currentQuery?.builder.queryData,
filter.attributeKey,
lastUsedQuery,
]);
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
const isFilterDisabled = useMemo(
() =>
(currentQuery?.builder?.queryData?.[
lastUsedQuery || 0
]?.filters?.items?.filter((item) => isEqual(item.key, filter.attributeKey))
?.length || 0) > 1,
[currentQuery?.builder?.queryData, lastUsedQuery, filter.attributeKey],
);
// variable to check if the current filter has multiple values to its name in the key op value section
const isMultipleValuesTrueForTheKey =
Object.values(currentFilterState).filter((val) => val).length > 1;
const handleClearFilterAttribute = (): void => {
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => ({
...item,
filters: {
...item.filters,
items:
idx === lastUsedQuery
? item.filters.items.filter(
(fil) => !isEqual(fil.key, filter.attributeKey),
)
: [...item.filters.items],
},
})),
},
};
redirectWithQueryBuilderData(preparedQuery);
};
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
lastUsedQuery || 0
]?.filters?.items?.some((item) => isEqual(item.key, filter.attributeKey));
const onChange = (
value: string,
checked: boolean,
isOnlyOrAllClicked: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
const query = cloneDeep(currentQuery.builder.queryData?.[lastUsedQuery || 0]);
// if only or all are clicked we do not need to worry about anything just override whatever we have
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
if (isOnlyOrAllClicked && query?.filters?.items) {
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only';
query.filters.items = query.filters.items.filter(
(q) => !isEqual(q.key, filter.attributeKey),
);
if (isOnlyOrAll === 'Only') {
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.IN),
key: filter.attributeKey,
value,
};
query.filters.items = [...query.filters.items, newFilterItem];
}
} else if (query?.filters?.items) {
if (
query.filters?.items?.some((item) => isEqual(item.key, filter.attributeKey))
) {
// if there is already a running filter for the current attribute key then
// we split the cases by which particular operator is present right now!
const currentFilter = query.filters?.items?.find((q) =>
isEqual(q.key, filter.attributeKey),
);
if (currentFilter) {
const runningOperator = currentFilter?.op;
switch (runningOperator) {
case 'in':
if (checked) {
// if it's an IN operator then if we are checking another value it get's added to the
// filter clause. example - key IN [value1, currentSelectedValue]
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
} else {
// if the current state wasn't an array we make it one and add our value
const newFilter = {
...currentFilter,
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
}
} else if (!checked) {
// if we are removing some value when the running operator is IN we filter.
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: currentFilter.value.filter((val) => val !== value),
};
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
} else {
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
}
} else {
// if not an array remove the whole thing altogether!
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
}
}
break;
case 'nin':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
if (!checked) {
// in case of array add the currentUnselectedValue to the list.
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
} else {
// in case of not an array make it one!
const newFilter = {
...currentFilter,
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
}
} else if (checked) {
// opposite of above!
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
value: currentFilter.value.filter((val) => val !== value),
};
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
} else {
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
}
} else {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
}
}
break;
case '=':
if (checked) {
const newFilter = {
...currentFilter,
op: getOperatorValue(OPERATORS.IN),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
} else if (!checked) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
}
break;
case '!=':
if (!checked) {
const newFilter = {
...currentFilter,
op: getOperatorValue(OPERATORS.NIN),
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key, filter.attributeKey)) {
return newFilter;
}
return item;
});
} else if (checked) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key, filter.attributeKey),
);
}
break;
default:
break;
}
}
} else {
// case - when there is no filter for the current key that means all are selected right now.
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.NIN),
key: filter.attributeKey,
value,
};
query.filters.items = [...query.filters.items, newFilterItem];
}
}
const finalQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
...currentQuery.builder.queryData.map((q, idx) => {
if (idx === lastUsedQuery) {
return query;
}
return q;
}),
],
},
};
redirectWithQueryBuilderData(finalQuery);
};
return (
<div className="checkbox-filter">
<section className="filter-header-checkbox">
<section className="left-action">
{isOpen ? (
<ChevronDown
size={13}
cursor="pointer"
onClick={(): void => {
setIsOpen(false);
setVisibleItemsCount(10);
}}
/>
) : (
<ChevronRight
size={13}
onClick={(): void => setIsOpen(true)}
cursor="pointer"
/>
)}
<Typography.Text className="title">{filter.title}</Typography.Text>
</section>
<section className="right-action">
{isOpen && (
<Typography.Text
className="clear-all"
onClick={handleClearFilterAttribute}
>
Clear All
</Typography.Text>
)}
</section>
</section>
{isOpen && isLoading && !attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && (
<>
<section className="search">
<Input
placeholder="Filter values"
onChange={(e): void => setSearchText(e.target.value)}
disabled={isFilterDisabled}
/>
</section>
{attributeValues.length > 0 ? (
<section className="values">
{currentAttributeKeys.map((value: string) => (
<div key={value} className="value">
<Checkbox
onChange={(e): void => onChange(value, e.target.checked, false)}
checked={currentFilterState[value]}
disabled={isFilterDisabled}
rootClassName="check-box"
/>
<div
className={cx(
'checkbox-value-section',
isFilterDisabled ? 'filter-disabled' : '',
)}
onClick={(): void => {
if (isFilterDisabled) {
return;
}
onChange(value, currentFilterState[value], true);
}}
>
{filter.customRendererForValue ? (
filter.customRendererForValue(value)
) : (
<Typography.Text
className="value-string"
ellipsis={{ tooltip: { placement: 'right' } }}
>
{value}
</Typography.Text>
)}
<Button type="text" className="only-btn">
{isSomeFilterPresentForCurrentAttribute
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only'}
</Button>
<Button type="text" className="toggle-btn">
Toggle
</Button>
</div>
</div>
))}
</section>
) : (
<section className="no-data">
<Typography.Text>No values found</Typography.Text>{' '}
</section>
)}
{visibleItemsCount < attributeValues?.length && (
<section className="show-more">
<Typography.Text
className="show-more-text"
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
>
Show More...
</Typography.Text>
</section>
)}
</>
)}
</div>
);
}

View File

@@ -1,14 +0,0 @@
import './Slider.styles.scss';
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
interface ISliderProps {
filter: IQuickFiltersConfig;
}
// not needed for now build when required
export default function Slider(props: ISliderProps): JSX.Element {
const { filter } = props;
console.log(filter);
return <div>Slider</div>;
}

View File

@@ -1,93 +0,0 @@
.quick-filters {
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid var(--bg-slate-400);
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10.5px;
border-bottom: 1px solid var(--bg-slate-400);
.left-actions {
display: flex;
align-items: center;
gap: 6px;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
}
.sync-tag {
display: flex;
padding: 5px 9px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 2px;
border: 1px solid rgba(78, 116, 248, 0.2);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-500);
font-family: 'Geist Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
text-transform: uppercase;
}
}
.right-actions {
display: flex;
align-items: center;
gap: 12px;
.divider-filter {
width: 1px;
height: 14px;
background: #161922;
}
.sync-icon {
background-color: var(--bg-ink-500);
border: 0;
box-shadow: none;
}
}
}
}
.lightMode {
.quick-filters {
background-color: var(--bg-vanilla-100);
border-right: 1px solid var(--bg-vanilla-300);
.header {
border-bottom: 1px solid var(--bg-vanilla-300);
.left-actions {
.text {
color: var(--bg-ink-400);
}
.sync-icon {
background-color: var(--bg-vanilla-100);
}
}
.right-actions {
.sync-icon {
background-color: var(--bg-vanilla-100);
}
}
}
}
}

View File

@@ -1,124 +0,0 @@
import './QuickFilters.styles.scss';
import {
FilterOutlined,
SyncOutlined,
VerticalAlignTopOutlined,
} from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import Slider from './FilterRenderers/Slider/Slider';
export enum FiltersType {
SLIDER = 'SLIDER',
CHECKBOX = 'CHECKBOX',
}
export enum MinMax {
MIN = 'MIN',
MAX = 'MAX',
}
export enum SpecficFilterOperations {
ALL = 'ALL',
ONLY = 'ONLY',
}
export interface IQuickFiltersConfig {
type: FiltersType;
title: string;
attributeKey: BaseAutocompleteData;
customRendererForValue?: (value: string) => JSX.Element;
defaultOpen: boolean;
}
interface IQuickFiltersProps {
config: IQuickFiltersConfig[];
handleFilterVisibilityChange: () => void;
}
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
const { config, handleFilterVisibilityChange } = props;
const {
currentQuery,
lastUsedQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
// clear all the filters for the query which is in sync with filters
const handleReset = (): void => {
const updatedQuery = cloneDeep(
currentQuery?.builder.queryData?.[lastUsedQuery || 0],
);
if (!updatedQuery) {
return;
}
if (updatedQuery?.filters?.items) {
updatedQuery.filters.items = [];
}
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => ({
...item,
filters: {
...item.filters,
items: idx === lastUsedQuery ? [] : [...item.filters.items],
},
})),
},
};
redirectWithQueryBuilderData(preparedQuery);
};
const lastQueryName =
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
return (
<div className="quick-filters">
<section className="header">
<section className="left-actions">
<FilterOutlined />
<Typography.Text className="text">Filters for</Typography.Text>
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
</Tooltip>
</section>
<section className="right-actions">
<Tooltip title="Reset All">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</Tooltip>
<div className="divider-filter" />
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
</section>
</section>
<section className="filters">
{config.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return <Checkbox filter={filter} />;
case FiltersType.SLIDER:
return <Slider filter={filter} />;
default:
return <Checkbox filter={filter} />;
}
})}
</section>
</div>
);
}

View File

@@ -5,7 +5,7 @@ import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -96,7 +96,7 @@ function DynamicColumnTable({
return (
<div className="DynamicColumnTable">
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <LaunchChatSupport {...facingIssueBtn} />}
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}

View File

@@ -2,7 +2,7 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
import { TableDataSource } from './contants';
@@ -13,7 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: LaunchChatSupportProps;
facingIssueBtn?: FacingIssueBtnProps;
shouldSendAlertsLogEvent?: boolean;
}

View File

@@ -1,5 +0,0 @@
.tab-title {
display: flex;
gap: 4px;
align-items: center;
}

View File

@@ -1,41 +0,0 @@
import './Tabs.styles.scss';
import { Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import { History, Table } from 'lucide-react';
import { useState } from 'react';
import { ALERT_TABS } from '../constants';
export function Tabs(): JSX.Element {
const [selectedTab, setSelectedTab] = useState('overview');
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedTab(e.target.value);
};
return (
<Radio.Group className="tabs" onChange={handleTabChange} value={selectedTab}>
<Radio.Button
className={
selectedTab === ALERT_TABS.OVERVIEW ? 'selected_view tab' : 'tab'
}
value={ALERT_TABS.OVERVIEW}
>
<div className="tab-title">
<Table size={14} />
Overview
</div>
</Radio.Button>
<Radio.Button
className={selectedTab === ALERT_TABS.HISTORY ? 'selected_view tab' : 'tab'}
value={ALERT_TABS.HISTORY}
>
<div className="tab-title">
<History size={14} />
History
</div>
</Radio.Button>
</Radio.Group>
);
}

View File

@@ -1,18 +0,0 @@
@mixin flex-center {
display: flex;
justify-content: space-between;
align-items: center;
}
.tabs-and-filters {
@include flex-center;
margin-top: 1rem;
margin-bottom: 1rem;
.filters {
@include flex-center;
gap: 16px;
.reset-button {
@include flex-center;
}
}
}

View File

@@ -1,16 +0,0 @@
import './TabsAndFilters.styles.scss';
import { Filters } from 'components/AlertDetailsFilters/Filters';
import { Tabs } from './Tabs/Tabs';
function TabsAndFilters(): JSX.Element {
return (
<div className="tabs-and-filters">
<Tabs />
<Filters />
</div>
);
}
export default TabsAndFilters;

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